Researching ActiveSupport::Concern
在设计程序框架时,我们可能会定义很多类和模块。 并且这些类和模块之间有着很复杂的网状依赖关系。
通常, 我们这么定义一些依赖:
- 手动式模块横向依赖管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Ma
def self.included(base)
base.send(:do_host_something)
end
end
module Mb
def self.included(base)
base.send(:do_host_something)
end
end
class CHost
include Ma, Mb
end
类CHost依赖2个模块Ma, Mb。 那么, 问题来了, 当需要依赖1个模块, 20个模块, 100个模块时… 我们是否也需要一个一个的加入? 像 include Ma, Mb, Mc, … Mx。 显然这是模块依赖管理的一个小麻烦。 我把它叫做手动式模块横向依赖管理。
好了, 通常我们会想到另一种方法, 将modules的依赖写在module中, 用一个module管理这些依赖, 那么在类中只需要引用一个依赖的模块。 从结构上看这是一种纵向依赖结构。
- 模块纵向依赖结构
1
2
3
4
5
6
7
8
9
10
11
module Ma
include Mb, Mc, Md
def self.included(base)
base.send(:do_host_something)
end
end
class CHost
include Ma #仅仅需要include Ma即可。
end
这种结构看似没什么问题, 其实也没什么问题。 但是, 在有一种场景下, 它将使我们陷入尴尬境地。
类是一种抽象, 一个类描述一类特定的对象。 但是, 有时我们希望类能够拥有一些特别的功能或者特性化。 多态是一种方式, 但是会使类继承结构复杂化。
另一种方式, 是使用模块, 在模块中打开类,使用反射机制, 以使类能特性化。
模块纵向依赖结构中, 一个最大的问题是,由于Mb,Mc,Md变成被Ma所include, 在Ma的函数self.included(base)中, base变成了Mb,Mc,Md 三个模块, 导致Ma中无法对宿主类CHost进行扩展。
- 模块横向依赖结构
终于回到主题了。 我们需要一种方法, 使我们能够在多依赖模块的场景中,宿主类只需include一个模块就可以在其他所有依赖的模块中, 扩展自己。
ActiveSupport::Concern 就是为这种场景而生。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Ma
extend ActiveSupport::Concern
included do
self.send(:do_host_something)
end
end
module Mb
extend ActiveSupport::Concern
include Ma
included do
self.send(:do_host_something)
end
end
class CHost
include Mb #只需include Mb, 而不需要知道Mb依赖了哪些模块。
end
通过源码, 我们可以看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
我们可以看到, ActiveSupport::Concern 的included函数中, 如果有代码块对象, 则使用class_eval进行扩展。
另外, 如果我们定义module ClassMethods 和 module InstanceMethods, 它会自动帮我们将实例方法和类方法载入到宿主里去。
就不用写 send(:include, InstanceMethods) 和 send(:extend, ClassMethods)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Foo
extend ActiveSupport::Concern
included do
self.send(:do_host_something)
end
module ClassMethods
def bite
# do something
end
end
module InstanceMethods
def poke
# do something
end
end
end
另一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
module OuterFile
extend ActiveSupport::Concern
included do
validates :file_id, presence: true
def file
Photo.new(self.file_id)
end
end
end
最后,题外话, Concern最好命名以able结尾!
参考:
https://ruby-china.org/topics/19812 –ActiveSupport::Concern小结