Наследование методов класса из модулей /mixins в Ruby
Известно, что в Ruby методы класса наследуются:
class P
def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works
Однако для меня неожиданно для меня, что он не работает с mixins:
module M
def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!
Я знаю, что метод #extend может сделать это:
module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works
Но я пишу mixin (или, скорее, хотел бы написать), содержащий как методы экземпляра, так и методы класса:
module Common
def self.class_method; puts "class method here" end
def instance_method; puts "instance method here" end
end
Теперь я хотел бы сделать следующее:
class A; include Common
# custom part for A
end
class B; include Common
# custom part for B
end
Я хочу, чтобы A, B наследовали методы экземпляра и класса из модуля Common
. Но, конечно, это не работает. Итак, разве нет секретного способа сделать эту работу наследования одним модулем?
Мне кажется нецелесообразным разбить его на два разных модуля, один из которых будет включать, а другой - расширить. Другим возможным решением было бы использовать класс Common
вместо модуля. Но это всего лишь обходной путь. (Что, если есть два набора общих функциональных возможностей Common1
и Common2
, и нам действительно нужно иметь mixins?) Есть ли какая-то серьезная причина, почему наследование класса методов не работает из mixins?
Ответы
Ответ 1
Общая идиома - использовать included
hook и вводить методы класса оттуда.
module Foo
def self.included base
base.send :include, InstanceMethods
base.extend ClassMethods
end
module InstanceMethods
def bar1
'bar1'
end
end
module ClassMethods
def bar2
'bar2'
end
end
end
class Test
include Foo
end
Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"
Ответ 2
Вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модуля работает так же, как в 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 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 у вас есть возможность определить так называемые методы singleton для объектов. Эти методы добавляются как новые методы экземпляра в специальный скрытый singleton class объекта:
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]
Как включить методы класса в модуль?
Как мы только что установили, методы класса - это действительно просто методы экземпляра для одноэлементного класса объекта класса. Означает ли это, что мы можем просто включить модуль в одноэлементный класс, чтобы добавить кучу методов класса? Да, это так!
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
, что делает такой же – то есть включает модуль в одноэлементный класс объекта:
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
Ответ 3
Как сказал Серджио в комментариях, для парней, которые уже находятся в Rails (или не возражают в зависимости от Active Support), Concern
полезен здесь:
require 'active_support/concern'
module Common
extend ActiveSupport::Concern
def instance_method
puts "instance method here"
end
class_methods do
def class_method
puts "class method here"
end
end
end
class A
include Common
end
Ответ 4
Вы можете получить свой торт и съесть его, сделав это:
module M
def self.included(base)
base.class_eval do # do anything you would do at class level
def self.doit #class method
@@fred = "Flintstone"
"class method doit called"
end # class method define
def doit(str) #instance method
@@common_var = "all instances"
@instance_var = str
"instance method doit called"
end
def get_them
[@@common_var,@instance_var,@@fred]
end
end # class_eval
end # included
end # module
class F; end
F.include M
F.doit # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]
Если вы намерены добавить экземпляр и переменные класса, вы в конечном итоге вытащите свои волосы, когда вы столкнетесь с кучей сломанного кода, если вы не сделаете это таким образом.