inheritanceRuby中的modules / mixins的类方法

众所周知,在Ruby中,类方法被inheritance:

class P def self.mm; puts 'abc' end end class Q < P; end Q.mm # works 

然而,对于我来说,它并不适用于mixin:

 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模块inheritance实例和类方法。 但是,当然,这是行不通的。 那么,是不是有一个秘密的方法来使这个inheritance工作从一个单一的模块?

对我来说,把它分成两个不同的模块,一个包括另一个模块。 另一个可能的解决scheme是使用类Common而不是模块。 但这只是一个解决方法。 (如果有两套常见的functionCommon1Common2 ,我们真的需要mixin吗?)为什么类方法inheritance不能从mixins中工作?

一个常见的习惯是使用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方法来查看任何类或模块的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会查看这个祖先列表中的每一个项目,以便find一个提供名字的实例方法 。 因为我们把M包含到C ,所以M现在是C的祖先,所以当我们在C一个实例上调用foo时,Ruby会在Mfind这个方法:

 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中,类和模块是普通对象 – 它们是ClassModule类的实例。 这意味着你可以dynamic地创build新的类,将它们赋值给variables等。

 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级方法的诞生:

 class Abc end # define singleton method def Abc.foo "foo" end Abc.singleton_class.instance_methods(false) #=> [:foo] 

或者,更常见的定义类方法的方法是在类定义块中使用self ,它指向正在创build的类对象:

 class Abc def self.foo "foo" end end Abc.singleton_class.instance_methods(false) #=> [:foo] 

如何将类方法包含在模块中?

就像我们刚刚build立的那样,类方法实际上只是类对象的单例类中的实例方法。 这是否意味着我们可以在单例类中添加一个模块来添加一堆类方法? 是的,它确实!

 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 ,它也是这样做的 – self.singleton_class.include M::ClassMethods一个模块包含到对象的单例类中:

 class HostKlass include M extend M::ClassMethods end HostKlass.singleton_class.included_modules #=> [M::ClassMethods, Kernel] # ^ there it is! 

extend调用移入模块

前面的例子不是结构良好的代码,原因有两个:

  1. 我们现在必须在HostClass定义中调用includeextend来正确地包含我们的模块。 如果你必须包含许多类似的模块,这可能会变得非常麻烦。
  2. HostClass直接引用M::ClassMethods ,它是HostClass不需要知道或关心的模块M实现细节

那么怎么样呢:当我们在第一行调用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 

正如Sergio在评论中提到的那样,对于已经在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