Rails Concern 源码研究

ActiveSupport::Concern 是为了更方便地 include 模块而推出的工具类。

使用方法

首先来看下它的使用方法。

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
# 传统的 Module 引入
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end

module ClassMethods
...
end
end

# 使用 Concern
module M
extend ActiveSupport::Concern

included do
scope :disabled, -> { where(disabled: true) }
end

class_methods do
...
end
end

可见,通过 included 和 class_method 两个类方法使得 Module 的写法更加清晰。
你可能有些疑问,对比传统的引入,使用 Concern 虽然更加清晰了,但没什么巨大的优点。而传统的引入也可以通过将方法分成两个 module,如 InstanceMethods、ClassMethods,来达到同样的效果。
在这个简单的例子上,确实如此。但在一些嵌套的 include 上 Concern 的优势就体现出来了。

嵌套的 include

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

module Bar
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 中引入 Bar,但由于 Bar 又需要 Foo,导致必须在 Host 中引入 Foo。也就是说,当我们 include module 时也必须把它的依赖同时 include 进来。这将随着依赖关系的复杂而变得艰难。
为什么不让 Bar 来负责自己的依赖呢?如以下的代码。

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

然而愿望很美好,实际运行时会报错,让我们来分析下流程。
当 Bar include Foo 时,Foo.included 方法被回调,而此时的 base 为 Bar。也就是说,method_injected_by_foo 会被添加到 Bar 上而不是 Host。当 Host include Bar 时,Bar.included 会调用 Host.method_injected_by_foo,而 Host 上没有相关方法,导致报错。

但是使用 Concern 就可以完美地解决这个问题。

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

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

module Bar
extend ActiveSupport::Concern
include Foo

included do
self.method_injected_by_foo
end
end

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

源码分析

Concern 的源码很短,不过 40 多行,但涉及到不少元编程的知识,要看明白得花点功夫。
复习下基本的知识,A extend B,B.extended(base) 会被回调,参数 base 为 A。A include B,B#included(base)、B#append_features(base) 都会被回调,参数 base 为 A。

接下来,以上文 Concern 代码为例子来说明下实现原理。
我们先从 module Foo extend ActiveSupport::Concern 开始,此时 Concern.extended 会被回调,初始化 Foo 类实例变量 @_dependencies。

1
2
3
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end

之后第 5 行调用 included 时,会将 @_included_block 设置为传入的 block。
注意这里的 included 是显式调用的,而不是被回调的,参数 base 为 nil。

1
2
3
4
5
6
7
8
9
def included(base = nil, &block)
if base.nil?
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)

@_included_block = block
else
super
end
end

第 13 行 module Bar extend ActiveSupport::Concern 与 Foo extend ActiveSupport::Concern 同理。
第 14 行 include Foo,使得 Foo 的 append_features 和 included 被调用。included 由于 base 不为空只是简单地调用 super。

1
2
3
4
5
6
7
8
9
10
11
12
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)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end

append_features 是 Concern 实现其 magic 最重要的部分。
由于 Bar 初始化了 @_dependencies,base.instance_variable_get(:@_dependencies) << self 会被运行,所以 Bar @_dependencies 就变为了 [Foo]。
Bar 调用 included 和 Foo 同理。

第 22 行,Host include Bar 使得 Bar 的 append_features 被回调。注意,重头戏来了。
@_dependencies.each { |dep| base.send(:include, dep) } 会被执行,通过之前的分析 Bar.@_dependencies 为 [Foo],所以也就是 base.send(:include, Foo),这里的 base 为 Host。Host include Foo 会回调 Foo#append_features,此时 Host 会 extend Foo::ClassMethods 和 class_eval(&@_included_block),从而实现了 Host 在 include Bar 时自动 include Foo。

简单来说,Bar include Foo 时并没有产生 include 原本的效果,而是把 Foo 添加到 Bar 的 @_dependencies 中。当 Host include Bar 时, Bar 中 @_dependencies 的依赖才一一生效。

最后附上 Concern 的源码。

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
34
35
36
37
38
39
40
41
42
43
44
module ActiveSupport
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end

def self.extended(base) #:nodoc:
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)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end

def included(base = nil, &block)
if base.nil?
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)

@_included_block = block
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

参考