Ось повна історія, що пояснює необхідні концепції метапрограмування, необхідні для розуміння того, чому включення модулів працює так, як це робиться в Ruby.
Що відбувається при включенні модуля?
Включення модуля до класу додає модуль до предків класу. Ви можете подивитися на предків будь-якого класу або модуля, викликавши його ancestors
метод:
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!
Коли ви викликаєте метод на екземплярі C
, Ruby перегляне кожен елемент цього списку предків, щоб знайти метод екземпляра із зазначеним іменем. Оскільки ми включили M
в C
, M
тепер є предком C
, тому, коли ми звертаємось foo
до екземпляра C
, Ruby знайде цей метод у M
:
C.new.foo
#=> "foo"
Зауважте, що включення не копіює жодного примірника чи методу класу до класу - воно лише додає класу "примітку", що він також повинен шукати, наприклад, методи у включеному модулі.
Що з методами "класу" в нашому модулі?
Оскільки включення змінює лише спосіб надсилання методів екземпляра, включення модуля до класу лише робить його методи екземпляра доступними для цього класу. Методи "класу" та інші декларації в модулі не копіюються автоматично в клас:
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
Як Ruby реалізує методи класу?
У Ruby класи та модулі є простими об'єктами - вони є екземплярами класу Class
та Module
. Це означає, що ви можете динамічно створювати нові класи, присвоювати їм змінні тощо:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Також у Ruby ви маєте можливість визначати так звані односторонні методи на об'єктах. Ці методи додаються як нові методи екземпляра до спеціального, прихованого класу одиночних об'єктів:
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]
Але хіба класи та модулі також не є звичайними об'єктами? Насправді вони є! Чи означає це, що вони можуть також мати одномісні методи? Так! І ось як народжуються методи класу:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Або, найпоширеніший спосіб визначення методу класу полягає у використанні self
всередині блоку визначення класу, який відноситься до створюваного об’єкта класу:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Як включити методи класу в модуль?
Як ми щойно встановили, методи класу насправді є лише методами екземпляра для класу-одиночки об'єкта класу. Чи означає це, що ми можемо просто включити модуль в клас singleton, щоб додати купу методів класу? Так!
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"
Цей self.singleton_class.include M::ClassMethods
рядок виглядає не дуже приємно, тому Ruby додав Object#extend
, що робить те саме - тобто включає модуль в клас singleton об'єкта:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Переміщення 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` 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!
Звичайно, додавання методів класів - це не єдине, що ми можемо зробити self.included
. У нас є об’єкт класу, тому ми можемо викликати на ньому будь-який інший (клас) метод:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end