ActiveSupport::Concern实现原理
在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 A
和module B
,其中module B
包含了module A
,然后我们在类Test
中include 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
51module 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
错误。
其实我们仔细思考下就知道问题出在哪里了:
我们在模块
B
中include
了模块A
的时候,B
充当了base
导致A::ClassMethods
模块中定义的方法变成了module B
的类方法
而我们的ActiveSupport::Concern
就完美的解决了链式包含的问题。接下来我们看下到底是怎么实现的吧。
实现原理
1 | module ActiveSupport |
我们发现Concern
通过复写了append_features
改变了默认的包含行为,我们包含一个模块时Concern
会通过@_dependencies
检测base
是否是一个Concern
如果是一个Concern
我们就把它加到@_dependencies
变量中,同时返回false
以指明该模块没有被真正被包含。如果不是一个Concern
此时分两种情况:
- 当
Concern
已经出现在包含类的祖先链中(if base < self
)我们返回false
- 当
Concern
没有出现在包含类的祖先链中,我们将@_dependencies
存储的依赖递归去包含(@_dependencies.each { |dep| base.include(dep) }
)
接下来我们也要把自身也加入祖先链中(super
)。
然后extend
方法class_methods
所定义的内容,以及使用class_eval
在base
类中执行included
方法中所定义的块