Наследование методов класса от модулей / миксинов в Ruby
известно, что в Ruby методы класса наследуются:
class P
def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works
однако меня удивляет, что он не работает с миксинами:
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 и нам действительно нужно есть миксины?) Есть ли какая-то глубокая причина, почему наследование метода класса не работает из миксинов?
4 ответов:
общая идиома заключается в использовании
includedкрючок и ввести методы класса оттуда.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"
вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модуля работает так, как это происходит в 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"обратите внимание, что включение не копирует ни один экземпляр или методы класса в класс - он просто добавляет "примечание" к классу, что он следует также искать методы экземпляра в включенном модуле.
как насчет методов "класса" в нашем модуле?
потому что включение только изменяет способ отправки методов экземпляра, включая модуль в класс только делает его методы экземпляра доступными на этом классе. Методы" class " и другие объявления в модуле не копируются автоматически в класс:
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-методы на объекты. Эти методы добавляются как новые методы экземпляра в специальный, скрытый синглтон класс из объект:
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линия выглядит не очень красиво, поэтому Руби добавил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метод is для. Ruby автоматически вызывает этот метод всякий раз, когда модуль входит в другой класс (или модуль), и передает объект класса host в качестве первого аргумента: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
Как упоминал Серхио в комментариях, для парней, которые уже находятся в Rails (или не возражают в зависимости от Активную Поддержку),
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
вы можете иметь свой торт и съесть его тоже, сделав это:
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"]Если вы собираетесь добавить экземпляр и переменные класса, вы в конечном итоге вытащите свои волосы, поскольку вы столкнетесь с кучей сломанного кода, если вы не сделаете это таким образом.
Comments