从Ruby中的模块/ mixin继承类方法

时间:2021-01-20 23:22:36

It is known that in Ruby, class methods get inherited:

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

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

However, it comes as a surprise to me that it does not work with mixins:

然而,令我惊讶的是,它并不适用于mixin:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

I know that #extend method can do this:

我知道#extend方法可以做到:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

But I am writing a mixin (or, rather, would like to write) containing both instance methods and class methods:

但是我正在编写一个mixin(或者更确切地说,我想要编写),其中包含实例方法和类方法:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Now what I would like to do is this:

现在我想做的是:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

I want A, B inherit both instance and class methods from Common module. But, of course, that does not work. So, isn't there a secret way of making this inheritance work from a single module?

我想要A, B从公共模块继承了实例和类方法。当然,这是行不通的。那么,难道没有一种秘密的方法可以让这个继承从一个模块中工作吗?

It seems inelegant to me to split this into two different modules, one to include, the other to extend. Another possible solution would be to use a class Common instead of a module. But this is just a workaround. (What if there are two sets of common functionalities Common1 and Common2 and we really need to have mixins?) Is there any deep reason why class method inheritance does not work from mixins?

把它分成两个不同的模块,一个要包含,另一个要扩展,对我来说似乎不太合适。另一种可能的解决方案是使用公共类而不是模块。但这只是一个变通办法。(如果有两组公共功能,即公共功能1和公共功能2,我们真的需要使用mixin怎么办?)为什么类方法继承不能从mixin中进行呢?

4 个解决方案

#1


137  

A common idiom is to use included hook and inject class methods from there.

一个常见的习惯用法是使用包含的hook并从其中注入类方法。

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"

#2


13  

Here is the full story, explaining the necessary metaprogramming concepts needed to understand why module inclusion works the way it does in Ruby.

下面是完整的故事,解释了理解模块包含为何在Ruby中如此工作所需的元编程概念。

What happens when a module is included?

Including a module into a class adds the module to the ancestors of the class. You can look at the ancestors of any class or module by calling its ancestors method:

将模块包含到类中,将模块添加到类的祖先。您可以通过调用它的祖先方法来查看任何类或模块的祖先:

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!

When you call a method on an instance of C, Ruby will look at every item of this ancestor list in order to find an instance method with the provided name. Since we included M into C, M is now an ancestor of C, so when we call foo on an instance of C, Ruby will find that method in M:

当您在一个C实例上调用一个方法时,Ruby将查看这个祖先列表的每个条目,以便找到一个具有提供名称的实例方法。由于我们将M包含在C中,M现在是C的祖先,所以当我们在C实例上调用foo时,Ruby会在M中找到这个方法:

C.new.foo
#=> "foo"

Note that the inclusion does not copy any instance or class methods to the class – it merely adds a "note" to the class that it should also look for instance methods in the included module.

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

What about the "class" methods in our module?

Because inclusion only changes the way instance methods are dispatched, including a module into a class only makes its instance methods available on that class. The "class" methods and other declarations in the module are not automatically copied to the 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

How does Ruby implement class methods?

In Ruby, classes and modules are plain objects – they are instances of the class Class and Module. This means that you can dynamically create new classes, assign them to variables, etc.:

在Ruby中,类和模块是普通对象——它们是类和模块的实例。这意味着您可以动态地创建新的类,将它们分配给变量,等等:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Also in Ruby, you have the possibility of defining so-called singleton methods on objects. These methods get added as new instance methods to the special, hidden singleton class of the object:

同样在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]

But aren't classes and modules just plain objects as well? In fact they are! Does that mean that they can have singleton methods too? Yes, it does! And this is how class methods are born:

但是类和模块不也是普通的对象吗?事实上他们!这是否意味着他们也可以有单例方法?是的,它确实!这就是类方法的起源:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Or, the more common way of defining a class method is to use self within the class definition block, which refers to the class object being created:

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

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

How do I include the class methods in a module?

As we just established, class methods are really just instance methods on the singleton class of the class object. Does this mean that we can just include a module into the singleton class to add a bunch of class methods? Yes, it does!

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

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"

This self.singleton_class.include M::ClassMethods line does not look very nice, so Ruby added Object#extend, which does the same – i.e. includes a module into the singleton class of the object:

这self.singleton_class。包含M:::ClassMethods行看起来不太好,所以Ruby添加了对象#extend,它也做了同样的事情——例如,在对象的单例类中包含一个模块:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Moving the extend call into the module

This previous example is not well-structured code, for two reasons:

前面的示例不是结构良好的代码,原因有二:

  1. We now have to call both include and extend in the HostClass definition to get our module included properly. This can get very cumbersome if you have to include lots of similar modules.
  2. 现在,我们必须在HostClass定义中调用include和extend才能使模块正确地包含进来。如果您必须包含许多类似的模块,这将变得非常麻烦。
  3. HostClass directly references M::ClassMethods, which is an implementation detail of the module M that HostClass should not need to know or care about.
  4. HostClass直接引用M::ClassMethods,它是模块M的实现细节,HostClass不需要知道或者关心它。

So how about this: when we call include on the first line, we somehow notify the module that it has been included, and also give it our class object, so that it can call extend itself. This way, it's the module's job to add the class methods if it wants to.

那么这个如何:当我们在第一行调用include时,我们以某种方式通知模块它已经被包含,并给它我们的类对象,这样它就可以调用extend本身。通过这种方式,如果想要添加类方法,则是模块的工作。

This is exactly what the special self.included method is for. Ruby automatically calls this method whenever the module is included into another class (or module), and passes in the host class object as the first argument:

这就是特殊的自我。包括方法。每当模块被包含到另一个类(或模块)中时,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!

Of course, adding class methods is not the only thing we can do in self.included. We have the class object, so we can call any other (class) method on it:

当然,添加类方法并不是我们在self.include中可以做的惟一事情。我们有class对象,所以我们可以调用它的任何其他(class)方法:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

#3


2  

As Sergio mentioned in comments, for guys who are already in Rails (or don’t mind depending on Active Support), Concern is helpful here:

正如Sergio(塞尔吉奥)在评论中提到的,对于那些已经在Rails上的人(或者不介意依赖于积极的支持),关注在这里是有帮助的:

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

#4


0  

You can have your cake and eat it too by doing this:

你可以吃你的蛋糕,也可以这么做:

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

If you intend to add instance, and class variables, you will end up pulling out your hair as you will run into a bunch of broken code unless you do it this way.

如果您打算添加实例和类变量,那么您最终将会拉出您的头发,因为您将会遇到一堆损坏的代码,除非您这样做。

#1


137  

A common idiom is to use included hook and inject class methods from there.

一个常见的习惯用法是使用包含的hook并从其中注入类方法。

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"

#2


13  

Here is the full story, explaining the necessary metaprogramming concepts needed to understand why module inclusion works the way it does in Ruby.

下面是完整的故事,解释了理解模块包含为何在Ruby中如此工作所需的元编程概念。

What happens when a module is included?

Including a module into a class adds the module to the ancestors of the class. You can look at the ancestors of any class or module by calling its ancestors method:

将模块包含到类中,将模块添加到类的祖先。您可以通过调用它的祖先方法来查看任何类或模块的祖先:

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!

When you call a method on an instance of C, Ruby will look at every item of this ancestor list in order to find an instance method with the provided name. Since we included M into C, M is now an ancestor of C, so when we call foo on an instance of C, Ruby will find that method in M:

当您在一个C实例上调用一个方法时,Ruby将查看这个祖先列表的每个条目,以便找到一个具有提供名称的实例方法。由于我们将M包含在C中,M现在是C的祖先,所以当我们在C实例上调用foo时,Ruby会在M中找到这个方法:

C.new.foo
#=> "foo"

Note that the inclusion does not copy any instance or class methods to the class – it merely adds a "note" to the class that it should also look for instance methods in the included module.

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

What about the "class" methods in our module?

Because inclusion only changes the way instance methods are dispatched, including a module into a class only makes its instance methods available on that class. The "class" methods and other declarations in the module are not automatically copied to the 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

How does Ruby implement class methods?

In Ruby, classes and modules are plain objects – they are instances of the class Class and Module. This means that you can dynamically create new classes, assign them to variables, etc.:

在Ruby中,类和模块是普通对象——它们是类和模块的实例。这意味着您可以动态地创建新的类,将它们分配给变量,等等:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Also in Ruby, you have the possibility of defining so-called singleton methods on objects. These methods get added as new instance methods to the special, hidden singleton class of the object:

同样在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]

But aren't classes and modules just plain objects as well? In fact they are! Does that mean that they can have singleton methods too? Yes, it does! And this is how class methods are born:

但是类和模块不也是普通的对象吗?事实上他们!这是否意味着他们也可以有单例方法?是的,它确实!这就是类方法的起源:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Or, the more common way of defining a class method is to use self within the class definition block, which refers to the class object being created:

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

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

How do I include the class methods in a module?

As we just established, class methods are really just instance methods on the singleton class of the class object. Does this mean that we can just include a module into the singleton class to add a bunch of class methods? Yes, it does!

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

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"

This self.singleton_class.include M::ClassMethods line does not look very nice, so Ruby added Object#extend, which does the same – i.e. includes a module into the singleton class of the object:

这self.singleton_class。包含M:::ClassMethods行看起来不太好,所以Ruby添加了对象#extend,它也做了同样的事情——例如,在对象的单例类中包含一个模块:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Moving the extend call into the module

This previous example is not well-structured code, for two reasons:

前面的示例不是结构良好的代码,原因有二:

  1. We now have to call both include and extend in the HostClass definition to get our module included properly. This can get very cumbersome if you have to include lots of similar modules.
  2. 现在,我们必须在HostClass定义中调用include和extend才能使模块正确地包含进来。如果您必须包含许多类似的模块,这将变得非常麻烦。
  3. HostClass directly references M::ClassMethods, which is an implementation detail of the module M that HostClass should not need to know or care about.
  4. HostClass直接引用M::ClassMethods,它是模块M的实现细节,HostClass不需要知道或者关心它。

So how about this: when we call include on the first line, we somehow notify the module that it has been included, and also give it our class object, so that it can call extend itself. This way, it's the module's job to add the class methods if it wants to.

那么这个如何:当我们在第一行调用include时,我们以某种方式通知模块它已经被包含,并给它我们的类对象,这样它就可以调用extend本身。通过这种方式,如果想要添加类方法,则是模块的工作。

This is exactly what the special self.included method is for. Ruby automatically calls this method whenever the module is included into another class (or module), and passes in the host class object as the first argument:

这就是特殊的自我。包括方法。每当模块被包含到另一个类(或模块)中时,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!

Of course, adding class methods is not the only thing we can do in self.included. We have the class object, so we can call any other (class) method on it:

当然,添加类方法并不是我们在self.include中可以做的惟一事情。我们有class对象,所以我们可以调用它的任何其他(class)方法:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

#3


2  

As Sergio mentioned in comments, for guys who are already in Rails (or don’t mind depending on Active Support), Concern is helpful here:

正如Sergio(塞尔吉奥)在评论中提到的,对于那些已经在Rails上的人(或者不介意依赖于积极的支持),关注在这里是有帮助的:

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

#4


0  

You can have your cake and eat it too by doing this:

你可以吃你的蛋糕,也可以这么做:

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

If you intend to add instance, and class variables, you will end up pulling out your hair as you will run into a bunch of broken code unless you do it this way.

如果您打算添加实例和类变量,那么您最终将会拉出您的头发,因为您将会遇到一堆损坏的代码,除非您这样做。