程序人生

写优雅的程序,做优雅的人

ActiveSupport::Concern 的工作原理

| Comments

去年有一次的周记分享过 ActiveSupport::Concern (以下简称 Concern) 的用法,但是对它的实现原理一直不太明白,周末把它的实现源码仔细研究了一番,总算比较深入了解了它的工作原理,同时也让我对 Ruby 元编程有了更深的了解。

Concern 主要用于解决 module mix 的依赖问题,同时简化 module mix 的步骤。 关于依赖问题,必须用一个例子才好说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo
        ...
      end
    end
  end
end

module Bar
  include Foo
  def self.included(base)
    base.method_injected_by_foo
  end
end

class Host
  include Foo # We need to include this dependency for Bar
  include Bar # Bar is the module that Host really needs
end

上面例子中 Host 为了 include Bar,必须得先 include Bar 依赖的 Foo,这是因为 Bar 在 include Foo 时,只是为 Bar extend method_injected_by_foo 方法,所以 Host 必须显式的 include Foo,才能够 extend method_injected_by_foo 方法。

使用 Concern 后,代码可以简写为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'active_support/concern'

module Foo
  extend ActiveSupport::Concern
  included do
    class_eval do
      def self.method_injected_by_foo
        ...
      end
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo

  included do
    self.method_injected_by_foo
  end
end

class Host
  include Bar # works, Bar takes care now of its dependencies
end

Bar 和 Foo 都 extend ActiveSupport::Concern 后,Host include Bar 已经不需要事先 mix Foo。

这是怎么做到的呢,看看 Concern 的代码,其实代码很少:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module
  module Concern
    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
        return false
      else
        return false if base < self
        @_dependencies.each { |dep| base.send(:include, dep) }
        super
        base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
        if const_defined?("InstanceMethods")
          base.send :include, const_get("InstanceMethods")
          ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \
            "no longer included automatically. Please define instance methods directly in #{self} instead.", caller
        end
        base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
      end
    end

    def included(base = nil, &block)
      if base.nil?
        @_included_block = block
      else
        super
      end
    end
  end
end

关键部分是 append_features 方法,通过阅读 ruby 的文档和源码得知,ruby 在 include 一个 module 时,实际会触发两个方法,一个是 append_features,进行实际的 mixing 操作,包括增加常量,方法和变量到模块中,另外一个是 included 方法,也就是我们常用来作为 include 钩子的方法,默认的 included 是一个空方法,我们通过重载它使钩子起作用。

从代码可知,当一个模块 extend ActiveSupport::Concern 时,将产生 3 个影响:

  1. 为模块设置了一个实例变量

变量为 @_dependencies,其值为空数组,表示依赖的模块,将在 append_features 中用到;

  1. append_features 方法被重载;

重载后行为有了很大变化,它的处理分两种情况: 一种是当它被一个有 @dependencies 实例变量的模块被 include 时,直接把自身加到 @dependencies 中, 比如当 Bar include Foo 时,将触发 Foo 的 append_features(base) 方法,此时 base 是 Bar,self 是 Foo,由于 Bar 已经 extend ActiveSupport::Concern,Bar 的 @dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。

当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时 base 是 Host,self 是 Bar,Host 没有 extend ActiveSupport::Concern,所以 Host 的 @dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。

  1. included 方法被重载;

included 的动作比较简单,如果以没有参数形式调用,将把 block 存放到 @included_block 变量中,@included_block 的 block 将在 append_features 方法中使用。

通过施加的这 3 个影响,ActiveSupport::Concern 完成了它的全部功能,非常简洁精练。

从中学到的东西,包括:

  1. module 中 append_features 和 included 方法时 include 的核心;

  2. 可以通过覆盖这些方法改变操作的行为;

  3. 可以定义模块实例变量存储属于该模块的一些数据;

Comments