Thinking About ActiveRecord::Concern

ActiveRecord::Concern通常在要被包含的模块中被extendextend ActiveSupport::Concern 先来直观的感受一下它的用法。

ActiveRecord::Concern 的用法

file person.rb


require 'active_support/concern'
module Person
  extend ActiveSupport::Concern
  included do
    attr_accessor :name
  end

  def eat
    "person eat"
  end

  def sleep
    "person sleep"
  end
end

file teacher.rb


require_relative 'person'

class Teacher
  def initialize(name)
    @name = name
    #code
  end
  include Person
end

t = Teacher.new("lily")

p t.eat
p t.name

# "person eat"
# "lily"

将这个两个文件放在同一个目录下,然后execute

$ruby teacher.rb
"person eat"
"lily"

ActiveRecord::Concern 的两个核心作用

根据前面的注释,可以总结出:

  1. 使代码简化并使之清晰化,当模块被包含时。例如将class_eval的语句可以放到 included do ... end 中,其实这个功能可有可无。
  2. 简化了模块链式包含时需要做的处理。当多个模块顺序链式包含时,只需要include最后一个模块, 即链头部的模块,因为根据这个模块可以沿着链一直找到所有被依赖的模块,所以只需要包含这个模块就可以了。

名词约定:

  • 被包含模块(通常为一个文件)
  • 主动包含模块(想要包含某个模块的模块或类文件)

代码中base为主动包含模块。

下面对源代码中进行了注释,以说明各处含义。

module ActiveSupport
  # A typical module looks like this:
  #
  #   module M
  #     def self.included(base)
  #       base.extend ClassMethods
  #       base.class_eval do
  #         scope :disabled, -> { where(disabled: true) }
  #       end
  #     end
  #
  #     module ClassMethods
  #       ...
  #     end
  #   end
  #
  # By using <tt>ActiveSupport::Concern</tt> the above module could instead be
  # written as:
  #
  #   require 'active_support/concern'
  #
  #   module M
  #     extend ActiveSupport::Concern
  #
  #     included do
  #       scope :disabled, -> { where(disabled: true) }
  #     end
  #
  #     class_methods do
  #       ...
  #     end
  #   end
  #
  # Moreover, it gracefully handles module dependencies. Given a +Foo+ module
  # and a +Bar+ module which depends on the former, we would typically write the
  # following:
  #
  #   module Foo
  #     def self.included(base)
  #       base.class_eval do
  #         def self.method_injected_by_foo
  #           ...
  #         end
  #       end
  #     end
  #   end
  #
  #   module Bar
  #     def self.included(base)
  #       base.method_injected_by_foo
  #     end
  #   end
  #
  #   class Host
  #     include Foo # We need to include this dependency for Bar
  #     include Bar # Bar is the module that Host really needs
  #   end
  #
  # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
  # could try to hide these from +Host+ directly including +Foo+ in +Bar+:
  #
  #   module Bar
  #     include Foo
  #     def self.included(base)
  #       base.method_injected_by_foo
  #     end
  #   end
  #
  #   class Host
  #     include Bar
  #   end
  #
  # Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
  # is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
  # module dependencies are properly resolved:
  #
  #   require 'active_support/concern'
  #
  #   module Foo
  #     extend ActiveSupport::Concern
  #     included do
  #       def self.method_injected_by_foo
  #         ...
  #       end
  #     end
  #   end
  #
  #   module Bar
  #     extend ActiveSupport::Concern
  #     include Foo
  #
  #     included do
  #       self.method_injected_by_foo
  #     end
  #   end
  #
  #   class Host
  #     include Bar # It works, now Bar takes care of its dependencies
  #   end
  module Concern
    class MultipleIncludedBlocks < StandardError #:nodoc:
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    def self.extended(base) #:nodoc:
      # the module used be included will always have this variable
      base.instance_variable_set(:@_dependencies, [])
    end

    def append_features(base)
      # 如果设置过@_dependencies变量,说明extend过concern,说明它是用来被包含的,
      # 则在该base模块中的@_dependencies变量中增加一个依赖,self就是要被包含的
      # 模块。
      if base.instance_variable_defined?(:@_dependencies)
        base.instance_variable_get(:@_dependencies) << self
        return false
      else
      # 如果没有设置过@_dependencies变量,那么说明没有extend过concern,
      #说明它基本上是主要的类了。
        # if base has included self, return false ?,  
        # self is module included by base
        return false if base < self
        # 如果没有被包含过,则逐一将依赖中的所有模块执行包含过程。
        @_dependencies.each { |dep| base.include(dep) }
        super
        # 执行其中的类方法
        # module ClassMethods ... end 中的方法
        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
        # 执行included do ... end 中的语句
        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
      end
    end

    # 这个是一个回调函数, 主要为了避免多次使用included do ... end块,如果再次使用该块,
    # 则会抛出MultipleIncludedBlocks异常
    def included(base = nil, &block)
      if base.nil?
        raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)

        @_included_block = block
      else
        super
      end
    end

    # 类方法获取,应该也是一个内部方法
    def class_methods(&class_methods_module_definition)
      mod = const_defined?(:ClassMethods, false) ?
        const_get(:ClassMethods) :
        const_set(:ClassMethods, Module.new)

      mod.module_eval(&class_methods_module_definition)
    end
  end
end