Ruby 是如何实现多重继承的

前言

Ruby 用了也接近一年了,有很多黑魔法让我感到非常有意思,刚开始学习的时候也经常让我不知所措。
其中很有意思的一点就是 includeextend 这两个方法来变相实现 多重继承,即代码的复用。但是用归用,却没有细细的研究其中的实现,故此,今天好好的看一看。

include

include 方法在 Ruby 中会被 include 模块的方法添加为当前模块下的实例方法

module Person
  def name
    p 'My Name Is Person'
  end
end

module User
  include Person
end

User.new.name
# My Name Is Person

这样,Person 就可以被随意引用,使用其中的实例方法 name
然而,这是怎么做到的呢,定义在 Person 中的 name 为什么能被 User 的实例调用呢?

那这就需要看看 include 的源码了

class Module
  # include(module, ...)    -> self
  # 
  # Invokes <code>Module.append_features</code> on each parameter in reverse order.
  def include(module1, *smth)
    # This is a stub, used for indexing
  end
...

append_features

Ruby 源码由 C 写成,所以我们只能看文档,文档中很清楚的指明了调用 include 方法会触发 append_features 方法,那我们就看看 append_features 的源码

# append_features(mod)   -> mod
# 
# When this module is included in another, Ruby calls
# <code>append_features</code> in this module, passing it the
# receiving module in _mod_. Ruby's default implementation is
# to add the constants, methods, and module variables of this module
# to _mod_ if this module has not already been added to
# _mod_ or one of its ancestors. See also <code>Module#include</code>.
def append_features(mod)
  # This is a stub, used for indexing
end
  1. 当一个 module 作为 mixininclude 时,会默认调用 append_features
  2. Rubyappend_features 的默认实现是将 mixin 中的 constantsmethodsmodule variables 添加到需要 includemodule 中。
  3. 所以当 append_features 执行后,Person module 下的 name 被添加到了 User

验证

重写 append_features 方法:

module Person
  extend ActiveSupport::Concern

  def name
    p 'My Name Is Person'
  end

  def self.append_features(base)
  end
end

User.new.name
# undefined method `name' for #<User:0x00007f84b918c898> (NoMethodError)

很显然,append_features 被覆盖,导致 name 没有被添加到 User

included:include 的回调

append_features 的实现满足了我们对实例方法的代码重用,但是没有实现对类方法的重用,如果我们想在 include 时同时导入类方法,或者其他操作,我们可以选择重写 append_features。但是在 Ruby 中并不建议直接重写 append_features,而是给我们提供了简单便捷的钩子方法 included

# included(othermod)
# 
# Callback invoked whenever the receiver is included in another
# module or class. This should be used in preference to
# <tt>Module.append_features</tt> if your code wants to perform some
# action when a module is included in another.
# 
#        module A
#          def A.included(mod)
#            puts "#{self} included in #{mod}"
#          end
#        end
#        module Enumerable
#          include A
#        end
#         # => prints "A included in Enumerable"
def included(othermod)
# This is a stub, used for indexing
end

如果我们想要导入类方法,重写刚刚的 Person 类:

module Person
  # 与 included 做对比
  def self.run
    p 'I Can Run'
  end

  def self.included(base)
    base.class_eval do
      def self.eat
        p 'I Can Eat'
      end
    end
  end
end

User.eat
# I Can Eat
User.run
# undefined method `run' for User:Class (NoMethodError)

Rails 是如何扩展的

Rails 中有一个非常好用的库 ActiveSupport,对 Ruby 语言的很多特性都实现了扩展,今天要看的就是其中 ActiveSupport::Concern 是如何扩展 include

# 所有注释都被我删除了,有兴趣的请查看源码
module ActiveSupport
  module Concern
    class MultipleIncludedBlocks < StandardError
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    def self.extended(base)
      base.instance_variable_set(:@_dependencies, [])
    end

    def append_features(base)
      if base.instance_variable_defined?(:@_dependencies)
        base.instance_variable_get(:@_dependencies) << self
        false
      else
        return false if base < self
        @_dependencies.each { |dep| base.include(dep) }
        super
        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
      end
    end

    def included(base = nil, &block)
      if base.nil?
        if instance_variable_defined?(:@_included_block)
          if @_included_block.source_location != block.source_location
            raise MultipleIncludedBlocks
          end
        else
          @_included_block = block
        end
      else
        super
      end
    end

    def class_methods(&class_methods_module_definition)
      mod = const_defined?(:ClassMethods, false) ?
        const_get(:ClassMethods) :
        const_set(:ClassMethods, Module.new)

      mod.module_eval(&class_methods_module_definition)
    end
  end
end

Concern 的源码非常简单,总共就定义了 3 个实例方法,1 个类方法,1 个错误,其中 append_featuresincluded 方法刚刚讲过,我们来看看 Rails 做了什么

included:更优雅的 included

def included(base = nil, &block)
  if base.nil?
    if instance_variable_defined?(:@_included_block)
      if @_included_block.source_location != block.source_location
        raise MultipleIncludedBlocks
      end
    else
      @_included_block = block
    end
  else
    super
  end
end

Rails 对 included 进行的扩展其实很简单

  1. included 继续作为 include 的回调使用时,base 不为 nil,则执行 super
  2. included 作为 mixin 中的类方法被调用,并传入一个 block,则将该 block 存入 @_included_block,并且不允许有多个 @_included_block 存在

这么做的目的是,使 mixin 可以通过 included 方法定义 def self.included,让代码更优雅

比如,如果使用 def slef.included 方法

module Student
  def self.included(base)
    base.class_eval do
      def self.study
        p 'I Can Study'
      end
    end
  end
end

class User
  include Student
end

User.study
# I Can Study

而使用 included 方法就可以这样写

require 'active_support/concern'

module Student
  extend ActiveSupport::Concern

  included do
    def self.study
      p 'I Can Study'
    end
  end
end

class User
  include Student
end

User.study
# I Can Study

class_methods:更简洁的类方法定义

在上面我们看见了 included 是如何更优雅的实现类方法的定义的,而 Rails 还顺便提供了一个更简单的 magic 方法 class_methods

使用:

require 'active_support/concern'

module Foo
  extend Concern

  class_methods do
    def run
      'I Can Run'
    end
  end
end

class Bar
  include Foo
end

p Bar.run
# I Can Run

源码:

def class_methods(&class_methods_module_definition)
  mod = const_defined?(:ClassMethods, false) ?
    const_get(:ClassMethods) :
    const_set(:ClassMethods, Module.new)

  mod.module_eval(&class_methods_module_definition)
end

class_methods 的实现比 included 更简单,就是将 block 转换成一个新的 module ClassMethods,而如何注入则依赖于 append_features 来实现

append_features:解决内部依赖

Rails 使用 append_features 解决了一个非常重要的问题,那就是内部依赖的引入

module Foo
  def self.included(base)
    base.class_eval do
      def self.test
        'test'
      end
    end
  end
end

module Bar
  def self.included(base)
    base.class_eval do
      p test
    end
  end
end

class User
  include Foo
  include Bar
end

如果 A mixin 依赖于 B mixinincluded 定义的方法,那引入的时候就必须同时引入 AB,这就要求开发者必须对每一个 mixin 的引入都要非常熟悉才能保证不出错。
ActiveSupport::Concern 通过 append_features 解决了这个问题

require 'active_support/concern'

module Foo
  extend ActiveSupport::Concern

  def self.included(base)
    base.class_eval do
      def self.test
        'test'
      end
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo

  def self.included(base)
    base.class_eval do
      p test
    end
  end
end

class User
  include Bar
end

# test

像这样的话,开发者只需要引入 Bar,而不再需要管 Barinclude 了几个模块。

Rails 中是如何这么聪明的实现的呢,源码如下:

# 1. Foo 和 Bar 使用 extend ActiveSupport::Concern 时,定义实例变量 @_dependencies = []
def self.extended(base) #:nodoc:
  base.instance_variable_set(:@_dependencies, [])
end

def append_features(base)
  if base.instance_variable_defined?(:@_dependencies)
    # 2. 当 Bar include Foo 时触发 Foo 的 append_features,发现 Bar 已定义实例变量 @_dependencies
    # 3. 将当前模块 Foo 添加入 Bar 的 @_dependencies
    base.instance_variable_get(:@_dependencies) << self
    false
  else
    return false if base < self
    # 4. 当 User include Bar 时 @_dependencies 中的所有模块(Foo)被 User 同时 include
    @_dependencies.each { |dep| base.include(dep) }
    super
    # 配合 class_methods 方法实现类方法的注入
    base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
    # 配合 included 方法实现 self.included
    base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
  end
end

append_features 除了解决了内部依赖的引入,还实现了两个 magic 方法 includedclass_methods 的注入

extend

extend 方法在 Ruby 中会被 extend 模块的方法添加为当前模块下的类方法

module Person
  def name
    "My name is Person"
  end
end

class User
  extend Person
end

p User.name
# My name is Person

extended:extend 的回调

extend 之后如果需要执行某些回调,则只需要定义 self.extended

module Person
  def self.extended(base)
    p "#{base} extended #{self}"
  end

  def name
    "My name is Person"
  end
end

class User
  extend Person
end
# User extended Person

参考

Ruby 中一些重要的钩子方法