Oto cała historia, wyjaśniająca niezbędne koncepcje metaprogramowania potrzebne do zrozumienia, dlaczego włączanie modułów działa tak, jak w Rubim.
Co się dzieje, gdy dołączony jest moduł?
Dołączenie modułu do klasy powoduje dodanie modułu do przodków klasy. Możesz spojrzeć na przodków dowolnej klasy lub modułu, wywołując jego ancestors
metodę:
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
Kiedy wywołujesz metodę na instancji programu C
, Ruby spojrzy na każdy element tej listy przodków w celu znalezienia metody instancji o podanej nazwie. Ponieważ włączyliśmy M
do C
, M
jest teraz przodkiem C
, więc kiedy wywołasz foo
wystąpienie C
, Ruby znajdzie tę metodę w M
:
C.new.foo
Zwróć uwagę, że dołączenie nie kopiuje żadnej instancji ani metod klas do klasy - po prostu dodaje "uwagę" do klasy, że powinna ona również szukać metod instancji w dołączonym module.
A co z metodami „klasowymi” w naszym module?
Ponieważ włączanie zmienia tylko sposób wysyłania metod instancji, dołączenie modułu do klasy powoduje, że jego metody instancji są dostępne tylko w tej klasie. Metody "klasy" i inne deklaracje w module nie są automatycznie kopiowane do klasy:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
C.new.instance_method
C.class_method
W jaki sposób Ruby implementuje metody klas?
W Rubim klasy i moduły są zwykłymi obiektami - są instancjami klasy Class
i Module
. Oznacza to, że możesz dynamicznie tworzyć nowe klasy, przypisywać je do zmiennych itp .:
klass = Class.new do
def foo
"foo"
end
end
klass.new.foo
Również w Rubim masz możliwość definiowania tak zwanych metod singletonowych na obiektach. Te metody są dodawane jako nowe metody instancji do specjalnej, ukrytej klasy pojedynczej obiektu:
obj = Object.new
def obj.foo
"foo"
end
obj.singleton_class.instance_methods(false)
Ale czy klasy i moduły nie są również zwykłymi obiektami? W rzeczywistości są! Czy to oznacza, że mogą też mieć metody singletonowe? Tak! I tak rodzą się metody klasowe:
class Abc
end
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
Lub bardziej powszechnym sposobem definiowania metody klasy jest użycie self
w bloku definicji klasy, który odnosi się do tworzonego obiektu klasy:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
Jak dołączyć metody klasy do modułu?
Jak już ustaliliśmy, metody klas są w rzeczywistości tylko metodami instancji klasy pojedynczej obiektu klasy. Czy to oznacza, że możemy po prostu dołączyć moduł do klasy pojedynczej, aby dodać kilka metod klasowych? Tak!
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
Ta self.singleton_class.include M::ClassMethods
linia nie wygląda zbyt ładnie, więc dodał Ruby Object#extend
, który robi to samo - tj. Włącza moduł do pojedynczej klasy obiektu:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
Przenoszenie extend
wywołania do modułu
Ten poprzedni przykład nie jest dobrze zorganizowany z dwóch powodów:
- Musimy teraz wywołać oba elementy
include
oraz extend
w HostClass
definicji, aby poprawnie uwzględnić nasz moduł. Może to być bardzo kłopotliwe, jeśli musisz dołączyć wiele podobnych modułów.
HostClass
bezpośrednie odniesienia M::ClassMethods
, które są szczegółem implementacyjnym modułu M
, HostClass
o którym nie trzeba wiedzieć ani o nim dbać.
A co powiesz na to: kiedy wywołujemy include
pierwszą linię, w jakiś sposób powiadamiamy moduł, że został dołączony, a także nadajemy mu nasz obiekt klasy, aby mógł się wywołać extend
. W ten sposób zadaniem modułu jest dodanie metod klasy, jeśli chce.
Właśnie do tego służy ta specjalna self.included
metoda . Ruby automatycznie wywołuje tę metodę za każdym razem, gdy moduł jest włączony do innej klasy (lub modułu) i przekazuje obiekt klasy hosta jako pierwszy argument:
module M
def new_instance_method; "hi"; end
def self.included(base)
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
Oczywiście dodawanie metod klasowych nie jest jedyną rzeczą, którą możemy zrobić self.included
. Mamy obiekt klasy, więc możemy wywołać na nim dowolną inną metodę (klasę):
def self.included(base)
base.existing_class_method
end