Larry’s Blog

Reading Rails: ActiveSupport::Concern

| Comments

ActiveSupport::Concern 模块除去注释外只有29行代码,但是却有着不可忽视的作用。它使得 modules 之间的依赖关系处理起来更加得优雅。

假设我们有两个模块 Foo 和 Bar,Bar 模块依赖于 Foo 模块。然后 Host 类希望包含 Bar 模块获取其中的实例方法。

module_dependency_sample.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo  # 在 base 模块中定义一个类方法
        ...
      end
    end
  end
end

module Bar
  def self.included(base)
    base.method_injected_by_foo  # 调用 base 模块的类方法,该方法是在包含 Foo 模块时动态定义的
  end
end

class Host
  include Foo  # 我们需要包含 Foo 模块,因为 Bar 模块依赖于 Foo 模块
  include Bar  # 但是 Bar 模块才是 Host 类真正需要的模块
end

也许你会想:既然是 Bar 模块依赖于 Foo 模块,为什么不直接在 Bar 模块中包含一下 Foo 模块呢?就像下面这样:

module_dependency_sample_wrong_solution.rb
1
2
3
4
5
6
7
8
9
10
module Bar
  include Foo
  def self.included(base)
    base.method_injected_by_foo
  end
end

class Host
  include Bar
end

上面看似ok的代码实际上是有问题的。当 Bar 模块包含 Foo 模块时,会执行 Foo 的 self.included 方法,然后在 base 也就是 Bar 中定义一个模块方法 - method_injected_by_foo,最后当 Host 模块 包含 Bar 时,会调用 Host 的类方法 method_injected_by_foo,显然会报 undefined method 的错误。

现在有了 ActiveSupport::Concern 这个模块,就可以很优雅的解决上述的模块依赖问题。

module_dependency_sample_solution.rb
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
end

下面来看一下 ActiveSupport::Concern 内部是如何实现的。先简单修改下active_support/concern.rb 来看看在上述代码运行时都发生了些什么?

concern_experiment.rb
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
module ActiveSupport
  module Concern
    def self.extended(base) #:nodoc:
      puts "### base: #{base}, Concern#extended being called ###"
      base.instance_variable_set("@_dependencies", [])
    end

    def append_features(base)
      if base.instance_variable_defined?("@_dependencies")
        puts "### base: #{base}, self: #{self}, append_features_0 being called ###"
        base.instance_variable_get("@_dependencies") << self
        return false
      else
        return false if base < self
        puts "### base: #{base}, self: #{self}, append_features_1 being called ###"
        @_dependencies.each { |dep| base.send(: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)
      puts "### base: #{base}, Concern.included being called ###"
      if base.nil?
        @_included_block = block
      else
        super
      end
    end
  end
end

module Foo
  extend ActiveSupport::Concern
  included do
    class_eval do
      def self.method_injected_by_foo
        puts "### self: #{self}, Foo#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
end

输出结果为:

concern_experiment.rb
1
2
3
4
5
6
7
8
9
10
11
### base: Foo, self: ActiveSupport::Concern, extended being called ###
### base: , self: Foo, included being called ###
### base: Bar, self: ActiveSupport::Concern, extended being called ###
### base: Bar, self: Foo, append_features_0 being called ###
### base: Bar, self: Foo, included being called ###
### base: , self: Bar, included being called ###
### base: Host, self: Bar, append_features_1 being called ###
### base: Host, self: Foo, append_features_1 being called ###
### base: Host, self: Foo, included being called ###
### self: Host, Foo#method_injected_by_foo ###
### base: Host, self: Bar, included being called ###
  1. Foo extend Concern(line 35), Concern#extended 方法被调用,Foo 的实例变量 @_dependencies 被设置为 [].
  2. Foo 模块中调用 included 方法(line 36),因为 base 为 nil,所以 Foo 的实例变量 @_included_block 值为定义模块方法 method_injected_by_foo 那一坨。
  3. Bar extend Concern(line 46), Concern#extended 方法被调用,Bar 的实例变量 @_dependencies 被设置为 [].
  4. 这步很关键。Bar 模块包含 Foo 模块时(line 47),会调用 Foo 的 append_features 模块方法,而由于 Foo 扩展了 Concern 模块,所以调用的实际上是 Concern 的 append_features 方法。因为 Bar(base) 中定义了 @dependencies 变量,将 Foo(self) 加入 Bar 的 @dependencies 中。 (line 11)
  5. 执行完上一步后,调用 Foo#included 方法,将 Foo 模块 mixin 进 Bar。从这点可以看出,当 Foo 被 Bar 包含时,会先执行 Foo#append_features 方法,而后才执行 Foo#included hook 方法。
  6. Bar 模块中调用 included 方法(line 49),因为 base 为 nil, 所以 Bar 的实例变量 @_included_block 值为调用 method_injected_by_foo 方法那一坨。
  7. Host 类包含 Bar 模块(line 55),会调用 Bar 的 append_features 模块方法,又由于 Bar 的 @_dependencies 当前值为 [Foo], Host 会继续 include Foo 模块 (line 16)
  8. Foo 的 append_features 模块方法被调用
  9. 在 Host 的 context 下执行 Foo 的实例变量 @_included_block, 即定义 method_injected_by_foo 方法(line 38); 最后执行 Foo#included hook方法。
  10. 在 Host 的 context 下执行 Bar 的实例变量 @_included_block, 即调用 method_injected_by_foo 方法(line 50)
  11. Host include Bar 整个过程结束,执行 Bar#included hook 方法。

整个流程就是这样的。不得不说 ActiveSupport::Concern 这个 module 的整个设计都非常的巧妙。

  1. 首先,将所有 extend Concern module 的类/模块的 @_dependencies 都设置为 []。
  2. 根据 base 类(模块)是否定义了 @_dependencies 来区分 host 和被包含的模块。
  3. included 方法通过判断 base 是否为 nil,来区分是 hook 方法还是普通的类(模块)方法。

另外,从第18行代码可以看出,Concern 会自动将 module 中定义的 ClassMethods 加入到 Host 中,这是 ActiveSupport::Concern 的第二个作用。

Comments