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

众所周知,在 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!

我知道扩展方法可以做到这一点:

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而不是模块。但这只是个权宜之计。(如果有两组通用功能 Common1Common2,并且我们真的需要 Mixin,那该怎么办?)为什么类方法继承不能从 Mixin 中工作,有什么深层次的原因吗?

51530 次浏览

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

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"

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

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

Here is the full story, explaining the necessary metaprogramming concepts needed to understand why module inclusion works the way it does in 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.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.:

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:

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:

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:

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 directly references M::ClassMethods, which is an implementation detail of the module M that HostClass should not need to know or care about.

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.

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:

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:

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

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.