Rails rails 之 concern 常见用法解读 和 源码原理解读

EricWJP · February 17, 2020 · Last by EricWJP replied at February 17, 2020 · 4134 hits

concern 只是一个 extend ActiveSupport::Concern 的 module。

既可以在 model 中使用,也可以在 controller 中使用。
使用方式: include ConcernName

concern 用途

一、用于把特定功能代码集合封装成一个 concern,提高代码可读性和方便代码重构;
二、把多个 model 或者 多个 controller 重复的功能集合代码封装成一个 concern。

concern 定义

#定义一
module MyConcern
  extend ActiveSupport::Concern
  ...
end

#定义二
concern MyConcern do 
  ...
end

以上两种定义 concern 的方式完全等同

常见 concern 内部定义

ActiveSupport::Concern 提供了一种约定配置:
  约定 ClassMethods 用来给类添加类方法(可选)
     module ClassMethods
     end
   定义 included 作为当当前 module(也就是子 concern) 被 include 的回调方法(可选)
     def self.included
     end

如果不使用 ClassMethods,concern 与普通 module 是一样的
module FirstConcern
  #把ActiveSupport::Concern中的方法 加入到 当前module模块方法(与class类方法原理相同)中
  extend ActiveSupport::Concern

  def self.included(base) 
    base.instance_eval do
      scope :jief, ->{...}
      has_many :table
    end
    # 或者
    #base.class_eval do
    #  ...  
    #end
    ...
  end
  #included 也可以这样
  #included do 
  #  这里作用域是base
  #  
  #  ...
  #end

  module ClassMethods
    #定义类方法
    def my_method
    end
    ...
  end
  #或者
  # class_methods do
  #   ...
  # end

end

ActiveSupport::Concern 代码详解

module Concern
    # 负责当定义多个 included 抛出异常
    class MultipleIncludedBlocks < StandardError #:nodoc:
      def initialize
        super "Cannot define multiple 'included' blocks for a Concern"
      end
    end

    # extended 是回调方法,当 当前module(这里指的是ActiveSupport::Concern) 被 extend 的时候执行
    # 参数--base是extend当前模块的module或者class
    # 用途,在base的作用域初始化@dependencies为空数组 []
    #@_dependencies是base的类实例变量(类实例变量只有类可以访问,他的实例无法访问),
      #用于记录base中include哪些module,顺序---从上到下,深度--1
    def self.extended(base) #:nodoc:
      base.instance_variable_set(:@_dependencies, [])
    end

    #这里的 append_features 是 对 Module.append_features 的扩展
    # Module#include 方法最终是 通过  Module.append_features 实现的(参数顺序是相反的)
    # append_features 只有在多个concern,include嵌套(即一个concern include 若干个其他的concern)时
      #使用此时扩展后的append_features。
    # 当子concern被include时 调用append_features(可理解成 include) 和 included,
        #append_features 在 included 之前调用
    # 参数--base是include当前模块的module或者class
    def append_features(base)
      if base.instance_variable_defined?(:@_dependencies)
        # instance_variable_defined? 用于判断实例变量是否存在。
        #如果 base是module,并且 extend ActiveSupport::Concern,
          # 也就是当 base是concern(这里称其为父concern)时,
          # @_dependencies 就会在上面extended回调方法中初始化
          #否则 这里不会执行。
        #把当前concern 加入到 base(父concern)的 类实例变量 @_dependencies中
        base.instance_variable_get(:@_dependencies) << self
        return false
      else
        # 当base不是concern(base 是 class 或者 是普通 module),才会执行

        #如果base 已经继承了self(子concern) 就不再执行,
        #这也就是 多次include 同一个module时,实际上只执行了第一次的原因
        return false if base < self      

        #遍历子concern 类实例变量 @_dependencies,base include 每一个子concern include的module
        @_dependencies.each { |dep| base.include(dep) }    
        super  #执行祖先链后面按序逐个查找并执行 append_features 方法

        #如果子concern 定义了 module ClassMethods,
        #  base 会 extend 该module-ClassMethods
        base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) 

        #在base作用域中执行 子concern中 included 方法的内容(Proc实例)
        base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)  
      end
    end

    #这里扩展了 Module#included 方法
    def included(base = nil, &block)
      if base.nil?
        #这里的处理 实现了 included do  ...  end
        raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)

        # 通过实例变量@_included_block,实现了  included 传入的块(这里变成了Proc对象) 共享,
            # 即append_features能够访问到
        @_included_block = block
      else
        super
      end
    end

    #这里实现了 class_methods do ... end
    def class_methods(&class_methods_module_definition)
      # mod 判断子concern中是否定义了 module ClassMethods,没有就用Module.new 初始化
      mod = const_defined?(:ClassMethods, false) ?
        const_get(:ClassMethods) :
        const_set(:ClassMethods, Module.new)

      # mod 把 传入的块(这里变成了Proc对象) 添加到 mod 中
      mod.module_eval(&class_methods_module_definition)
    end
  end

concern 用途 一、用于把特定功能代码集合封装成一个 concern,提高代码可读性和方便代码重构; 二、把多个 model 或者 多个 controller 重复的功能集合代码封装成一个 concern。

用普通的 include/module 办不到吗。

可以。这种更优雅。ruby 元编程这本书有讲到

Reply to QETHAN

如何定义“优雅”?我面试的时候经常问别人 😉

concern 用处有两个

  1. 为 mixin 提供语法糖。
  2. 解决多个 module 相互依赖问题。

解决依赖问题是通过 override Module#append_featuresModule#included两个钩子,希望多介绍这两个钩子。

@piecehealth
因为 concern 定义了这些约定
ActiveSupport::Concern 提供了一种约定配置:
约定 ClassMethods 用来给类添加类方法(可选)
module ClassMethods
end
定义 included 作为当当前 module(也就是子 concern) 被 include 的回调方法(可选)
def self.included
end
如果说 concern 的用途这样的描述:
为 mixin 提供语法糖。
解决多个 module 相互依赖问题。
不够直观,太书面,好的东西写的很隐晦的话,反而达不到他的效果。如果这篇文章给初学者来学习,那一种说法更直观呢,或者初学者更容易学习呢?

You need to Sign in before reply, if you don't have an account, please Sign up first.