Hier ist die ganze Geschichte, in der die notwendigen Metaprogrammierungskonzepte erläutert werden, die erforderlich sind, um zu verstehen, warum die Moduleinbeziehung so funktioniert wie in Ruby.
Was passiert, wenn ein Modul enthalten ist?
Durch das Einfügen eines Moduls in eine Klasse wird das Modul den Vorfahren der Klasse hinzugefügt. Sie können die Vorfahren jeder Klasse oder jedes Moduls anzeigen, indem Sie die folgende ancestors
Methode aufrufen :
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
# ^ look, it's right here!
Wenn Sie eine Methode für eine Instanz von aufrufen C
, überprüft Ruby jedes Element dieser Ahnenliste, um eine Instanzmethode mit dem angegebenen Namen zu finden. Da wir M
in aufgenommen haben C
, M
ist jetzt ein Vorfahr von C
. Wenn wir also foo
eine Instanz von aufrufen C
, findet Ruby diese Methode in M
:
C.new.foo
#=> "foo"
Beachten Sie, dass durch die Aufnahme keine Instanz- oder Klassenmethoden in die Klasse kopiert werden. Sie fügt der Klasse lediglich einen "Hinweis" hinzu, nach dem auch Instanzmethoden im enthaltenen Modul gesucht werden sollen.
Was ist mit den "Klassen" -Methoden in unserem Modul?
Da durch die Aufnahme nur die Art und Weise geändert wird, in der Instanzmethoden versendet werden, werden durch das Einschließen eines Moduls in eine Klasse nur die Instanzmethoden für diese Klasse verfügbar . Die "Klassen" -Methoden und andere Deklarationen im Modul werden nicht automatisch in die Klasse kopiert:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
#=> "bar"
C.new.instance_method
#=> "foo"
C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class
Wie implementiert Ruby Klassenmethoden?
In Ruby sind Klassen und Module einfache Objekte - sie sind Instanzen der Klasse Class
und Module
. Dies bedeutet, dass Sie dynamisch neue Klassen erstellen, sie Variablen zuweisen können usw.:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Auch in Ruby haben Sie die Möglichkeit, sogenannte Singleton-Methoden für Objekte zu definieren. Diese Methoden werden als neue Instanzmethoden zur speziellen, versteckten Singleton-Klasse des Objekts hinzugefügt :
obj = Object.new
# define singleton method
def obj.foo
"foo"
end
# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]
Aber sind Klassen und Module nicht auch nur einfache Objekte? In der Tat sind sie! Bedeutet das, dass sie auch Singleton-Methoden haben können? Ja tut es! Und so entstehen Klassenmethoden:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Die üblichere Methode zum Definieren einer Klassenmethode ist die Verwendung self
innerhalb des Klassendefinitionsblocks, der sich auf das zu erstellende Klassenobjekt bezieht:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Wie füge ich die Klassenmethoden in ein Modul ein?
Wie wir gerade festgestellt haben, sind Klassenmethoden wirklich nur Instanzmethoden für die Singleton-Klasse des Klassenobjekts. Bedeutet dies, dass wir nur ein Modul in die Singleton-Klasse aufnehmen können , um eine Reihe von Klassenmethoden hinzuzufügen? Ja tut es!
module M
def new_instance_method; "hi"; end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
self.singleton_class.include M::ClassMethods
end
HostKlass.new_class_method
#=> "hello"
Diese self.singleton_class.include M::ClassMethods
Zeile sieht nicht sehr gut aus, also fügte Ruby hinzu Object#extend
, was dasselbe tut - dh ein Modul in die Singleton-Klasse des Objekts einschließt:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Bewegen des extend
Anrufs in das Modul
Dieses vorherige Beispiel ist aus zwei Gründen kein gut strukturierter Code:
- Wir müssen jetzt beide
include
und extend
in der HostClass
Definition aufrufen , damit unser Modul richtig aufgenommen wird. Dies kann sehr umständlich werden, wenn Sie viele ähnliche Module einbinden müssen.
HostClass
Verweise direkt M::ClassMethods
, dies ist ein Implementierungsdetail des Moduls M
, HostClass
das nicht bekannt sein oder sich darum kümmern sollte.
Wie wäre es damit: Wenn wir include
in der ersten Zeile aufrufen , benachrichtigen wir das Modul irgendwie darüber, dass es enthalten ist, und geben ihm auch unser Klassenobjekt, damit es sich extend
selbst aufrufen kann. Auf diese Weise ist es Aufgabe des Moduls, die Klassenmethoden hinzuzufügen, wenn dies gewünscht wird.
Genau dafür ist die spezielle self.included
Methode gedacht. Ruby ruft diese Methode automatisch auf, wenn das Modul in einer anderen Klasse (oder einem anderen Modul) enthalten ist, und übergibt das Hostklassenobjekt als erstes Argument:
module M
def new_instance_method; "hi"; end
def self.included(base) # `base` is `HostClass` in our case
base.extend ClassMethods
end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
def self.existing_class_method; "cool"; end
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ still there!
Natürlich ist das Hinzufügen von Klassenmethoden nicht das einzige, was wir tun können self.included
. Wir haben das Klassenobjekt, also können wir jede andere (Klassen-) Methode darauf aufrufen:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end