从Ruby中的modules / mixins继承类方法

时间:2012-05-21 21:34:11

标签: ruby mixins

众所周知,在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而不是模块。但这只是一种解决方法。 (如果有两组常见功能Common1Common2并且我们真的需要mixins会怎么样?)类方法继承在mixins中不起作用有什么深层原因吗?

3 个答案:

答案 0 :(得分:158)

一个常见的习惯用法是使用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"

答案 1 :(得分:33)

以下是完整的故事,解释了必要的元编程概念,以理解为什么模块包含的工作方式与它在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"

请注意包含不会将任何实例或类方法复制到类 - 它只是向类添加一个“注释”,它还应该在包含的模块中查找实例方法。 / p>

我们模块中的“类”方法怎么样?

因为包含只会改变调度实例方法的方式,包括将一个模块包含在类中,只使其实例方法可用。模块中的“类”方法和其他声明不会自动复制到类中:

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的实例。这意味着您可以动态创建新类,将它们分配给变量等:

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,它也做了同样的事情 - 即将一个模块包含在对象的单例类中:

class HostKlass
  include M
  extend M::ClassMethods
end

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

extend电话移至模块

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

  1. 我们现在必须在include定义中调用 extendHostClass来正确包含我们的模块。如果你必须包含许多类似的模块,这会非常麻烦。
  2. HostClass直接引用M::ClassMethods,这是M不应该知道或关心的模块HostClass实施细节
  3. 那么怎么样:当我们在第一行调用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
    

答案 2 :(得分:2)

你可以吃蛋糕并吃掉它:

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"]

如果你打算添加实例和类变量,你最终会拔掉你的头发,因为你会遇到一堆破碎的代码,除非你这样做。