Ruby Active Support 的 Concern 模块来由探究

bajiudongfeng · 发布于 2017年03月20日 · 最后由 prothro 回复于 2017年03月30日 · 2097 次阅读
14935
本帖已被设为精华帖!

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 元编程第十章。
第一次写,还请大家多多指导。

共收到 3 条回复
De6df3 huacnlee 将本帖设为了精华贴 03月20日 23:41
10351

其实现在base.send :include, A,可以直接写成base.include A了。https://github.com/rails/rails/pull/18763#issuecomment-72349769

14935

@liukun_lk 元编程上也提到了,为了讲清楚来由,我就还按照以前的写法写了。

105f13

个人觉得老的写法很直观,关这一点就够了。 说实话,第一次看到concern 的时候,根本不知道这是啥东西……

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册