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方法中所定义的块