Известно, что в 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
Но я пишу миксин (или, скорее, хотел бы написать), содержащий как методы экземпляра, так и методы класса:
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
нам действительно нужны миксины?) Есть ли глубокая причина, по которой наследование методов класса не работает из миксинов?
Ответы:
Распространенная идиома - использовать
included
оттуда методы класса hook и inject.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"
источник
include
добавляет методы экземпляра,extend
добавляет методы класса. Вот как это работает. Не вижу непоследовательности, только нереализованные ожидания :)included
определение метода в другой модуль и включить ЭТО в свой основной модуль;)Вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модулей работает так же, как в 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]
Как мне включить методы класса в модуль?
Как мы только что установили, методы класса на самом деле являются просто методами экземпляра в одноэлементном классе объекта класса. Означает ли это, что мы можем просто включить модуль в одноэлементный класс, чтобы добавить кучу методов класса? Да!
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
строка выглядит не очень красиво, поэтому добавлен RubyObject#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
источник
Как упоминал Серхио в комментариях, для парней, которые уже используют 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
источник
Вы можете съесть свой торт и съесть его, выполнив следующие действия:
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"]
Если вы намереваетесь добавить переменные экземпляра и класса, вы в конечном итоге вырветесь за волосы, поскольку столкнетесь с кучей неработающего кода, если вы не сделаете это таким образом.
источник