最近都没怎么更新博客,一来没什么时间学习新知识,二来平时积累的感觉还没到质变的程度。既然没时间学一些新东西,就研究一下基础吧。之所以选择ActiveSupport,是因为它是做基础支持工作的,很多都是对Ruby原生对象的hack,对外部的gem依赖较少。我打算挑几个自己感兴趣的模块来分析分析。第一个就是这个Concern模块。
虽然Concern只有不到50行代码,也没依赖其他的模块,但还是花了我半天时间才搞清楚它怎么运作的,惭愧……
Concern模块是用来解决module和module之间的依赖问题。这里只说大概用法,想进一步了解,请移步 这篇文章。
一般来说,要定义一个模块,为了更好的组织类方法和实例方法,以下这种写法几乎成为一种标准了:
module M def self.included(base) base.extend ClassMethod base.send :include, InstanceMethod base.class_eval do # 调用base类的方法,一般用来声明式地修改类 my_attr_reader :name, ;age end end module ClassMethods def class_method_1; end end module InstanceMethods def instance_method_1; end end end
上面例子中base就是混入模块的类,class_eval那一段代码中可以调用base类的类方法my_attr_reader。
但如果现在我有两个module,M1和M2,M1依赖于M2,而且它们都需要在被混入类C时执行my_attr_reader方法,似乎可以写成下面这样:
module M2 def self.included(base) base.class_eval do my_attr_reader :age end end end module M1 def self.included(base) base.class_eval do my_attr_reader :name end end include M2 end class C def self.my_attr_reader(*args); end include M1 end
但实际上,以上代码是错误的,因为M2被混入M1,所以M2的included中的参数base实际上指向的是module M1,不是class C。这样自然调用不了my_attr_reader方法。
ActiveSupport::Concern就是用来解决这类问题的,以上写法可以改成:
require 'rubygems' require 'active_support' module M2 extend ActiveSupport::Concern included do my_attr_reader :age end end module M1 extend ActiveSupport::Concern include M2 included do my_attr_reader :name end end class C def self.my_attr_reader(*args); end include M1 end
总体来说,ActiveSupport::Concern实际上是通过一些元编程手段,把module M2混入到class C中去了。甚至,如果module M2依赖了其他模块,ActiveSupport::Concern也会递归地把那些module混入到class C中。下面我们来看看它是怎么做的。
以下源码取自ActiveSupport 3.0.7:
module ActiveSupport module Concern def self.extended(base) 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.send :include, const_get("InstanceMethods") if const_defined?("InstanceMethods") base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") end end def included(base = nil, &block) if base.nil? @_included_block = block else super end end end end
29行代码,三个方法搞定。不得不佩服Rails core team……看看第一个方法 self.extended。它是一个回调方法,会在ActiveSupport::Concern被extend进其他module或class时触发。参数base就是被extend的module或class。
这个方法的作用是,为base类加入一个实例变量 @_dependencies。默认值是空数组。它用来保存这个模块依赖的其他模块的列表。
第二个方法 append_features。这也是一个回调方法。它是在一个module被include进入其他module或class时调用的。当一个class(或module) include另一个module时,class会按照include module相反的顺序去调用每个module的 append_features方法。这个方法的默认实现,是把module的变量,实例方法等东西copy到被混入的class中。如果我们重载一个module的append_features,又什么都不做的话,那么这个module就像没有被混入一样。下面是一个例子:
module M3 def self.append_features(base) puts "call M3 append_features" super end def m3_instance_method; end end module M4 def self.append_features(base) puts "call M4 append_features" end def m4_instance_method; end end class C include M3, M4 end # 这时会打印 # call M4 append_features # call M3 append_features c = C.new c.m3_instance_method # 这个方法成功的混入到C中 c.m4_instance_method # 对象c没有这个方法
ActiveSupport::Concern中的append_feature并不是在Concern模块被include进其他模块时调用的(这个模块只会被extend,而且这个append_features并不是类方法),而是对于extend了ActiveSupport::Concern的模块而言(如module M1),当它被混入类C时,会触发append_features方法。
这个方法的作用是,如果base类(或模块)有@_dependencies列表时,将自己记入base的@_dependencies中,然后直接return(就是不混入base)。如果base类没有@_dependencies列表(这种情况可以肯定base就是最终要混入的class),就循环自己的@_dependencies列表,依次把每个依赖的module混入base。
第三个方法 included。很简单,如果有block。就把block存进 @_included_block 变量。然后在append_featuers中传给base.class_eval。没有block。就和普通的included回调方法一样。
方法都说完了,但还是有点绕。下面说下整体流程,以上面的module M1,module M2,class C为例子: