Inheriting class methods from modules / mixins in Ruby

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

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:

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:

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:

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?

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?


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

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 – ie 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:

  • 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.
  • 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
    

    正如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/44844.html

    上一篇: Emacs,Cedet和语义

    下一篇: 在Ruby中继承modules / mixins中的类方法