Примечание переводчика: перед прочтением этого поста рекомендую сначала ознакомиться с постом Вникаем в метаклассы Ruby.
Все рубисты знакомы с формальными определениями для include и extend. Вы делаете include модуля, чтобы добавить методы экземпляра класса, и extend — чтобы добавить методы класса. К сожалению, данные определения не совсем точны. Они не могут объяснить почему мы используем instance.extend(Module), чтобы добавить методы объекту. Разве не должны мы в этом случае использовать instance.include(Module)? Чтобы разобраться в этом вопросе, начнем с выяснения где же хранятся методы.
Методы, которые я храню для тебя и парень, который хранит мои методы
Объекты в Ruby не хранят свои собственные методы. Вместо этого они создают синглтон-класс, чтобы он хранил их методы.
class A
def self.who_am_i
puts self
end
def speak_up(input)
puts input.upcase
end
end
Интерпретатор создаст класс A и прикрепленный к нему синглтон-класс (мы будем ссылаться на синглтон-класс объекта используя префикс ‘ перед именем объекта). Любые методы экземпляра класса (как speak_up) добавляются к методам, хранящимся в классе A. Методы класса (как who_am_i) хранятся в классе ‘A.
A.singleton_methods # методы в классе ‘A
#=> [:who_am_i]
Тоже самое происходит с экземплярами класса. Если у нас есть объект класса A и мы добавляем к нему метод, мы не можем хранить этот метод внутри самого объекта. Запомните — объекты в Ruby не хранят свои собственные методы.
a = A.new
def a.not_so_loud(input)
puts input.downcase
end
Здесь опять создается синглтон-класс для объекта "a", чтобы он хранил метод not_so_loud.
Теперь у нас есть метод, который принадлежит только объекту "a" и не затрагивает другие объекты класса A.
Я — свой отец?
Класс A содержит методы и информацию о цепи наследования, нужные для объекта "a" и всех остальных объектов класса A. Точно также синглтон-класс ‘A содержит методы и информацию о цепи наследования для класса A. Вы можете считать класс A объектом класса ‘A. Уловка в том, что мы не можем напрямую обращаться к синглтон-классу ‘A. Это значит, что нам нужно как-то различать добавление методов в A и в ‘A. Тут то include и extend и вступают в игру.
include
Когда вы делаете include модуля в объект, вы добавляете методы в цепь наследования объекта.
class A
include M
end
В этом легко убедиться, проверив предков класса A.
A.ancestors
#=> [A, M, Object, Kernel, BasicObject]
extend
extend — это тоже самое, что сделать include, но для синглтон-класса объекта.
class A
extend M
end
И снова мы можем подтвердить это, проверив предков класса ‘A.
A.singleton_class.ancestors
#=> [M, Class, Module, Object, Kernel, BasicObject]
Также мы можем использовать extend и для объекта.
a = A.new
a.extend(M)
a.singleton_class.ancestors
#=> [M, A, Object, Kernel, BasicObject]
Если вы думаете об extend как о способе просто добавить методы класса, то все что мы сейчас сделали не имеет особого смысла. Однако, если вы посмотрите на это как на способ добавить методы к синглтон-классу объекта, то приведенные выше примеры станут яснее.
Хук included
Каждый вызов include проверяет подключаемый модуль на наличие метода included. Этот метод исполняется, когда модуль подключается, используя include. Это как конструктор (initialize) для подключений. Как вы возможно догадались, у extend для этих целей есть собственный метод — extended. Поэтому когда вы хотите добавить сразу и методы класса и методы экземпляра класса, вы можете использовать для этого хук included.
module M
def self.included(base)
base.extend(ClassMethods)
end
def speak_up(input)
puts input.upcase
end
module ClassMethods
def who_am_i
puts self
end
end
end
class C
include M
end
c = C.new
Сначала мы включаем модуль M в цепь наследования класса C.
Затем мы расширяем класс C, добавляя методы в цепь наследования класса ‘C.
Заключение
Когда начинаешь копать глубже типичного использования include и extend, то обнаруживаешь нечто странное и пугающее. Однако стоит понять реализацию лежащую в основе и все сразу обретает смысл. Давайте теперь дадим определение include и extend еще раз.
include — добавляет методы модуля объекту.
extend — вызывает include для синглтон-класса объекта.
Если вас интересует еще больше деталей о том, как работает интерпретатор, то я рекомендую к просмотру выступление Patrick Farley на Ruby Internals.
Примечание переводчика: еще один момент почему мы не можем использовать instance.include(Module) — это то, что метод include является приватным методом класса Module. В целом же до прочтения этой статьи я тоже не так представлял себе работу extend и include, поэтому посчитал стоящим ее перевести.
Внесу еще некоторую ясность: то, что в статье названо «синглтон-класс» (singleton class), имеет и другие названия: метакласс (metaclass) и айгенкласс (eigenclass). Это все одна и та же сущность, для которой в сообществе Ruby пока нет «официального» названия. Я использовал «синглтон-класс», т.к. это ближе к оригиналу. Однако Matz'у (создателю языка) больше импонирует термин eigenclass.