Вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модулей работает так же, как в Ruby.
Что происходит при включении модуля?
Включение модуля в класс добавляет модуль к предкам класса. Вы можете посмотреть на предков любого класса или модуля, вызвав его ancestors
метод:
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
Когда вы вызываете метод для экземпляра C
, Ruby просматривает каждый элемент этого списка предков, чтобы найти метод экземпляра с предоставленным именем. Поскольку мы включили M
в C
, M
теперь предок C
, так что, когда мы называем foo
на примере C
, Ruby будет найти этот метод в M
:
C.new.foo
Обратите внимание, что включение не копирует в класс методы экземпляра или класса - оно просто добавляет к классу «примечание», что он также должен искать методы экземпляра во включенном модуле.
А как насчет "классовых" методов в нашем модуле?
Поскольку включение изменяет только способ отправки методов экземпляра, включение модуля в класс только делает его методы экземпляра доступными в этом классе. Методы "класса" и другие объявления в модуле не копируются автоматически в класс:
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
Как Ruby реализует методы класса?
В Ruby классы и модули - это простые объекты - они являются экземплярами класса Class
и Module
. Это означает, что вы можете динамически создавать новые классы, назначать их переменным и т. Д .:
klass = Class.new do
def foo
"foo"
end
end
klass.new.foo
Также в Ruby у вас есть возможность определять так называемые одноэлементные методы для объектов. Эти методы добавляются как новые методы экземпляра в специальный скрытый одноэлементный класс объекта:
obj = Object.new
def obj.foo
"foo"
end
obj.singleton_class.instance_methods(false)
Но разве классы и модули не являются просто объектами? На самом деле они есть! Означает ли это, что у них тоже могут быть одноэлементные методы? Да! Так рождаются методы класса:
class Abc
end
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
Или более распространенный способ определения метода класса - использование self
в блоке определения класса, который относится к создаваемому объекту класса:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
Как мне включить методы класса в модуль?
Как мы только что установили, методы класса на самом деле являются просто методами экземпляра в одноэлементном классе объекта класса. Означает ли это, что мы можем просто включить модуль в одноэлементный класс, чтобы добавить кучу методов класса? Да!
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
Эта self.singleton_class.include M::ClassMethods
строка выглядит не очень красиво, поэтому добавлен Ruby Object#extend
, который делает то же самое - т.е. включает модуль в одноэлементный класс объекта:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
Перенос extend
звонка в модуль
Этот предыдущий пример не является хорошо структурированным кодом по двум причинам:
- Теперь нам нужно вызвать оба
include
и extend
в HostClass
определении, чтобы правильно включить наш модуль. Это может стать очень обременительным, если вам нужно включить много похожих модулей.
HostClass
прямые ссылки M::ClassMethods
, что является деталью реализации модуля, M
о которой HostClass
не нужно знать или заботиться.
Итак, как насчет этого: когда мы вызываем include
первую строку, мы каким-то образом уведомляем модуль о том, что он был включен, а также передаем ему наш объект класса, чтобы он мог вызывать extend
сам себя. Таким образом, задача модуля - добавлять методы класса, если он этого хочет.
Именно для этого и предназначен специальный self.included
метод . Ruby автоматически вызывает этот метод всякий раз, когда модуль включается в другой класс (или модуль), и передает объект класса хоста в качестве первого аргумента:
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
Конечно, добавление методов класса - не единственное, что мы можем сделать self.included
. У нас есть объект класса, поэтому мы можем вызвать для него любой другой метод (класса):
def self.included(base)
base.existing_class_method
end