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

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

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

对我来说,把它分成两个不同的模块,一个包含,另一个模块扩展,似乎不够高雅。 另一种可能的解决方案是使用类Common而不是模块。 但这只是一个解决方法。 (如果有两组常用的功能Common1Common2 ,我们真的需要mixin吗?)为什么类方法继承不能在mixin中工作?


一个常见的习惯用法是使用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会查看这个祖先列表中的每一项,以便找到一个提供名称的实例方法 。 由于我们将M包含在C ,因此M现在是C的祖先,因此当我们在C的实例上调用foo时,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中,类和模块是普通对象 - 它们是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]

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

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

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调用移入模块

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

  • 我们现在必须在HostClass定义中调用includeextend来正确地包含我们的模块。 如果你必须包含许多类似的模块,这会变得非常麻烦。
  • HostClass直接引用M::ClassMethods ,它是HostClass不需要知道或关心的模块M的实现细节。
  • 那么如何做到这一点:当我们在第一行调用include时,我们以某种方式通知模块它已被包含在内,并且也将它赋给我们的类对象,以便它可以调用extend 。 这样,如果需要的话,模块的工作就是添加类方法。

    这正是特殊的self.included方法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
    
    链接地址: http://www.djcxy.com/p/44843.html

    上一篇: Inheriting class methods from modules / mixins in Ruby

    下一篇: Why would we put a module inside a class in Ruby?