ActiveSupport::Concern 模块作用:让 rails 类包含模块之后同时获得实例方法和类方法。 例如:
require 'active_support'
module MyConcernA
extend ActiveSupport::Concern
def an_instance_method
"an_instance_method "
end
module ClassMethods
def a_class_method
"a_class_method"
end
end
end
class MyClass
include MyConcernA
end
MyClass.a_class_method
=> "a_class_method"
MyClass.new.an_instance_method
=> "an_instance_method "
那么这个是如何实现的呢?
让我们从头说起。
两个基本的概念:include 和 extend
include 让类获得实例方法
extend 让类获得类方法
最早使类同时获得实例方法和类方法的解决方法:
module A
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def a_class_method
puts "a_class_method"
end
end
def a_instance_method
puts "a_instance_method"
end
end
class W
include A
end
W.a_class_method
a_class_method
=> nil
模块 A 覆写了钩子方法 included,当在类 W 中 include A 的时候会调用覆写后的方法。
此时会把 W 作为一个参数传递进去,就是 included 钩子方法中的 base,所以就相当于 W 扩展了模块 ClassMethods,因此模块 ClassMethods 中的方法也就成为了类 A 的类方法。
但是这个技巧本身存在一些问题。每个需要定义类方法的模块都需要定义一个相似的 included 钩子方法。 除此之外还有一个更为关键的问题,例如:
module B
def self.included(base)
base.extend ClassMethods
#base.send :include, A
end
module ClassMethods
def b_class_method
puts "b_class_method"
end
end
def b_instance_method
puts "b_instance_method"
end
include A
end
class C
include B
end
C.b_class_method
b_class_method
=> nil
C.a_class_method
NoMethodError: undefined method `a_class_method' for C:Class
B.a_class_method
a_class_method
=> nil
本来 a_class_method 应该是类 C 的类方法才对,可是这里出了问题。原因在哪里呢?
在类C中 include B 此时参数 base 的值是 C
而在模块 B 中 include A 时候参数 base 的值是 B,所以 a_class_method 成为模块 B 的一个类方法。
为了解决这个问题,需要在模块 B 的钩子方法 included 中加入注释掉的代码
base.send :include, A
让类 C 调用一下模块 A 的钩子方法方法 included. 这样问题解决了,但是会造成其他的问题,如各个模块都包含了相似的代码,当超过一层模块包含的时候还有可能失败。
为了解决上述问题,ActiveSupport::Concern 出现了。其封装了包含并且扩展的技巧,同时解决了链式包含的问题。
一个模块可以通过扩展 concern 模块来定义自己的 ClassMethods 模块来实现包含并且扩展的功能,也就是文章最开始的例子。
那么这个是如何实现的呢?看源码
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
# 。。。
主要是两个方法 extend 和 append_features
当模块扩展 Concern 时候,会调用钩子方法 extended。在这个方法中为扩展它的类定义了一个@_dependencies.默认值是 [].
方法append_features是一个内核方法。和module#included类似。
相同点:当包含一个模块的时候被调用。
不同的:included 默认是空的,只有覆写之后才会有内容。而 append_features 则包含有实际的动作。用于检查被包含模块是否已经在包含类的祖先链上,如果不在则将该模块加入其祖先链。
当一个模块扩展 ActiveSupport::Concern,我们称其为一个 concern
接着我们来分析 append_features 的源码:
主要分为两种情况:
第一种情况
module MyConcernB
extend ActiveSupport::Concern
def an_instance_methodb
"an_instance_method "
end
module ClassMethods
def a_class_methodb
"a_class_method"
end
end
def self.instance_check
instance_variable_get(:@_dependencies)
end
include MyConcernA
end
代码会先判断 base 是否是一个 concern.
当前是一个 concern(MyConcernB) 在包含一个 concern(MyConcernA).此时包含的时候并没有真正的执行包含的动作,只是把链接放到一个依赖图中。也就是将 MyConcernA 放入数组@_dependencies.
MyConcernB.instance_check
=> [MyConcernA]
MyConcernB.a_class_method
NoMethodError: undefined method `a_class_method' for MyConcernB:Module
第二种情况
class MyClass
include MyConcernB
end
此时就是 else 后边的情况了。
首先会判断 MyClass 是否继承了 MyConcernB,也就是看 MyConcernB 是否在 MyClass 的继承链中。
如果没有就进行最关键的步骤:concern(MyConcernB) 中的依赖(也就是@_dependencies,上文已经打印出了值)会被递归包含到类 Myclass 中。这种最小化的依赖管理方式解决了之前链式包含的问题。
此处的
@_dependencies.each { |dep| base.send(:include, dep) }
就相当于在 MyClass 中
include MyConcernA
把所有的依赖的 concern(也就是@_dependencies中的值) 都加入类的祖先链之后,
需要把包含类自己(此处相当于 MyConcernB)也加入类的继承链中。这个通过调用 super 来实现。
最后是要通过 ClassMethods 模块来扩展类,就像最初做的事情一样。
以上内容全部来源于 ruby 元编程第十章。
第一次写,还请大家多多指导。