免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 3460 | 回复: 1
打印 上一主题 下一主题

Active Support 源码研究 -- Concern [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2011-05-18 21:18 |只看该作者 |倒序浏览
转:darkbaby123

Active Support 源码研究 -- Concern




前言


最近都没怎么更新博客,一来没什么时间学习新知识,二来平时积累的感觉还没到质变的程度。既然没时间学一些新东西,就研究一下基础吧。之所以选择ActiveSupport,是因为它是做基础支持工作的,很多都是对Ruby原生对象的hack,对外部的gem依赖较少。我打算挑几个自己感兴趣的模块来分析分析。第一个就是这个Concern模块。


虽然Concern只有不到50行代码,也没依赖其他的模块,但还是花了我半天时间才搞清楚它怎么运作的,惭愧……


Concern模块是用来解决module和module之间的依赖问题。这里只说大概用法,想进一步了解,请移步 这篇文章。

  

ActiveSupport::Concern的作用


一般来说,要定义一个模块,为了更好的组织类方法和实例方法,以下这种写法几乎成为一种标准了:



Ruby代码
  1. module M
  2.   def self.included(base)
  3.     base.extend ClassMethod
  4.     base.send :include, InstanceMethod
  5.     base.class_eval do
  6.       # 调用base类的方法,一般用来声明式地修改类
  7.       my_attr_reader :name, ;age
  8.     end
  9.   end
  10.   
  11.   module ClassMethods
  12.     def class_method_1; end
  13.   end

  14.   module InstanceMethods
  15.     def instance_method_1; end
  16.   end
  17. end
复制代码
上面例子中base就是混入模块的类,class_eval那一段代码中可以调用base类的类方法my_attr_reader。


但如果现在我有两个module,M1和M2,M1依赖于M2,而且它们都需要在被混入类C时执行my_attr_reader方法,似乎可以写成下面这样:


Ruby代码
  1. module M2
  2.   def self.included(base)
  3.     base.class_eval do
  4.       my_attr_reader :age
  5.     end
  6.   end
  7. end

  8. module M1
  9.   def self.included(base)
  10.     base.class_eval do
  11.       my_attr_reader :name
  12.     end
  13.   end
  14.   
  15.   include M2
  16. end

  17. class C
  18.   def self.my_attr_reader(*args); end

  19.   include M1
  20. end
复制代码
但实际上,以上代码是错误的,因为M2被混入M1,所以M2的included中的参数base实际上指向的是module M1,不是class C。这样自然调用不了my_attr_reader方法。


ActiveSupport::Concern就是用来解决这类问题的,以上写法可以改成:



Ruby代码
  1. require 'rubygems'
  2. require 'active_support'

  3. module M2
  4.   extend ActiveSupport::Concern
  5.   
  6.   included do
  7.     my_attr_reader :age
  8.   end
  9. end

  10. module M1
  11.   extend ActiveSupport::Concern

  12.   include M2
  13.   
  14.   included do
  15.     my_attr_reader :name
  16.   end
  17. end

  18. class C
  19.   def self.my_attr_reader(*args); end

  20.   include M1
  21. end
复制代码
总体来说,ActiveSupport::Concern实际上是通过一些元编程手段,把module M2混入到class C中去了。甚至,如果module M2依赖了其他模块,ActiveSupport::Concern也会递归地把那些module混入到class C中。下面我们来看看它是怎么做的。



源码分析


以下源码取自ActiveSupport 3.0.7:



Ruby代码
  1. module ActiveSupport
  2.   module Concern
  3.     def self.extended(base)
  4.       base.instance_variable_set("@_dependencies", [])
  5.     end

  6.     def append_features(base)
  7.       if base.instance_variable_defined?("@_dependencies")
  8.         base.instance_variable_get("@_dependencies") << self
  9.         return false
  10.       else
  11.         return false if base < self
  12.         @_dependencies.each { |dep| base.send(:include, dep) }
  13.         super
  14.         base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
  15.         base.send :include, const_get("InstanceMethods") if const_defined?("InstanceMethods")
  16.         base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
  17.       end
  18.     end

  19.     def included(base = nil, &block)
  20.       if base.nil?
  21.         @_included_block = block
  22.       else
  23.         super
  24.       end
  25.     end
  26.   end
  27. end
  28.   
复制代码
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就像没有被混入一样。下面是一个例子:


Ruby代码
  1. module M3
  2.   def self.append_features(base)
  3.     puts "call M3 append_features"
  4.     super
  5.   end

  6.   def m3_instance_method; end
  7. end

  8. module M4
  9.   def self.append_features(base)
  10.     puts "call M4 append_features"
  11.   end

  12.   def m4_instance_method; end
  13. end

  14. class C
  15.   include M3, M4
  16. end

  17. # 这时会打印
  18. # call M4 append_features
  19. # call M3 append_features

  20. c = C.new
  21. c.m3_instance_method  # 这个方法成功的混入到C中
  22. 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为例子:


1.module M2和module M1都extend了ActiveSupport::Concern。ActiveSupport::Concern的extended方法被调用,为module M2和module M1类加入实例变量@_dependencies。同时修改了它们的append_features和included两个回调方法。
2.module M2被混入module M1,module M2的append_features被调用,base参数为module M1。因为module M1有@_dependencies,module M2并没有真的被混入,只是被加入到module M1的@_dependencies列表中;
3.module M1被混入class C,因为class C没有@_dependencies列表,所以M1遍历自己的@_dependencies列表,将module M2混入class C;
4.此时module M2的append_features会再次被调用,但base变成了class C。所以module M2会先把自己的@_dependencies列表中的module加入到base(其实没有可加的),然后把调用append_features的默认行为,然后执行included中的block。调用base的方法。最后混入ClassMethods和InstanceMethods两个模块(如果有)
5.回到module M1,它处理完@_dependencies后,会调用super方法,执行append_features的默认行为,将自己混入到class C中,然后执行included的block,最后混入ClassMethods和InstanceMethods两个模块。


总结


基本上,ActiveSupport::Concern的思路就是使用append_features回调,去修改module的被include时的默认行为,延迟module被实际include的时机。这个模块中使用了相当一部分元编程方法。有些细节因为比较直观所以没讲,比如用instance_variable_defined?判断@_dependencies变量定义了没有,用instance_variable_get和instance_variable_set来获取和设置实例变量,等等。
这段源码中,还有一部分我没讲,就是源码第12行的 if base < self 。这个判断是说,当base是self的子类时,返回true,否则返回nil。我没有想到什么情况会有base是self的子类的,除非自定义一个类继承自module……有兴趣的可以自己推演下。

论坛徽章:
0
2 [报告]
发表于 2011-05-19 08:49 |只看该作者
支持村艹啊。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP