Rails中我们经常将可复用的代码放在Concern中以防止fat model具体用法如下(取自rubychina源码):

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
# 开启关闭帖子功能
module Closeable
extend ActiveSupport::Concern

included do
end

def closed?
closed_at.present?
end

def close!
transaction do
Reply.create_system_event(action: 'close', topic_id: self.id)
update!(closed_at: Time.now)
end
end

def open!
transaction do
update!(closed_at: nil)
Reply.create_system_event(action: 'reopen', topic_id: self.id)
end
end
end


class Topic < ApplicationRecord
include Closeable
end

我们今天就来看看ActiveSupport::Concern是如何实现的,以及它解决了以前的那些痛点?

解决了那些痛点?

假设我们有2个模块module Amodule B,其中module B包含了module A,然后我们在类Testinclude B这个时候类Test的行为是我们预想的不一样:

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
module A
def self.included(base)

# 这个base是包含这个模块的module/class
# 例如:
# class Demo
# include A
# end
# 此时的base就是Demo
#
base.extend ClassMethods #扩展类方法
end

def a_instance_method
"a_instance_method"
end

module ClassMethods
def a_class_method
"a_class_method"
end
end
end

module B
def self.included(base)
base.extend ClassMethods
end

def b_instance_method
"b_instance_method"
end

module ClassMethods
def b_class_method
"b_class_method"
end
end

include A # 注意这里包含了模块A
end

class Test
include B
end

Test.new.a_instance_method # => "a_instance_method"
Test.new.b_instance_method # => "b_instance_method"

Test.a_class_method # => NoMethodError: undefined method `a_class_method' for Test:Class
Test.b_class_method # => "b_class_method"

我们发现当Test调用a_class_method方法时候抛出了NoMethodError错误。

其实我们仔细思考下就知道问题出在哪里了:

我们在模块Binclude了模块A的时候,B充当了base导致A::ClassMethods模块中定义的方法变成了module B的类方法

而我们的ActiveSupport::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
45
46
47
48
49
module ActiveSupport
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end

# 这里设置了一个变量用来标识是否是一个`Concern`模块
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end

# append_features方法是在模块被包含的时候被调用的,里面包含了一个默认实现
# 用来检查被包含模块是否已经在包含类的祖先链上,如果不在则将该模块加入其祖先链
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.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

# 生成模块ClassMethod用来扩展类方法
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通过复写了append_features改变了默认的包含行为,我们包含一个模块时Concern会通过@_dependencies检测base是否是一个Concern如果是一个Concern我们就把它加到@_dependencies变量中,同时返回false以指明该模块没有被真正被包含。如果不是一个Concern此时分两种情况:

  1. Concern已经出现在包含类的祖先链中(if base < self)我们返回false
  2. Concern没有出现在包含类的祖先链中,我们将@_dependencies存储的依赖递归去包含(@_dependencies.each { |dep| base.include(dep) })

接下来我们也要把自身也加入祖先链中(super)。
然后extend方法class_methods所定义的内容,以及使用class_evalbase类中执行included方法中所定义的块