ActiveSupport::Concern 小结
预备
先认识几个钩子方法, extended
included
和 append_features
, 他们是模块上的模块方法.
module M1
def self.extended(base)
puts "M1 extend by #{base}"
end
end
module M2
extend M1
def hi
"hi M2"
end
def self.append_features(base)
puts "M2 start append_features to base"
puts "before super: #{base} included method? #{base.instance_methods.include?(:hi)}"
super
puts "after super: #{base} included method? #{base.instance_methods.include?(:hi)}"
end
def self.included(base)
puts "M2 included by #{base}"
end
end
class Clazz
include M2
end
=begin
M1 extend by M2
M2 start append_features to base
before super: Clazz included method? false
after super: Clazz included method? true
M2 included by Clazz
=end
extended
和 included
钩子本身是空实现, 重载方法会在 extend
和 include
完成之后调用.
append_features 有本职工作, 负责检查 self 是不是在 base 的祖先链上, 如果不在, 就加进去. 所以重载方法中一定记得 super, 否则 include 无效.
super 之前, 模块的方法还没有引入到类中, super之后, 模块的方法就引入到类中了, 全部完成之后再调用 included
钩子.
http://ruby-doc.org/core-2.5.0/Module.html#method-i-append_features
为什么用 Concern
在 Ruby 中, 把不同的功能划分到不同的 module 中, 在需要的时候 include
或者 extend
.
这就导致 base 类(把必要的基础功能都组织到 base 类中)特别大特别复杂, 很可能出现多层引用的问题.
多层引用对实例方法没有影响, 无论实例方法散落在哪个模块里, 只要引入了就能使用. 但是类方法不一样(类方法本质上是该类的单件方法), 穿透多层向某个类添加类方法的时候, 中间层的模块会影响类方法的注入.
ActiveSupport::Concern
就解决了这个问题, 可以认为在任何层次的模块中, self 都指向了 base 类.
下面是基本用例:
第一步在 module 中 extend ActiveSupport::Concern
;
实例方法包在 included
的 block 中, 或者不包直接定义.
类方法包在 class_methods
的 block 中, 或者使用子模块 ClassMethods
.
require 'pry'
require "active_support/concern"
module ThirdLevel
extend ActiveSupport::Concern
included do
def third_level_instance_method
puts ">>>third_level_instance_method"
end
end
class_methods do
def third_level_class_method
puts ">>>third_level_class_method"
end
end
end
module SecondLevel
extend ActiveSupport::Concern
include ThirdLevel
included do
def second_level_instance_method
puts ">>>second_level_instance_method"
end
end
class_methods do
def second_level_class_method
puts ">>>second_level_class_method"
end
end
end
module FirstLevel
extend ActiveSupport::Concern
include SecondLevel
# 被添加为实例方法
def first_level_instance_method
puts ">>>first_level_instance_method"
# 依赖第二层的类方法
puts self.class.second_level_class_method
end
# 被添加为类方法
module ClassMethods
def first_level_class_method
puts ">>>first_level_class_method"
end
end
end
class Clazz
binding.pry
include FirstLevel
end
puts "---"
源码分析
先预览一下, 总共就40多行:
module ActiveSupport
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
false
else
return false if base < self
@_dependencies.each { |dep| base.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
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
明白了 Concern
的需求后看源码就清楚多了.
本质是想让 self 穿透引用链, 指向 base 类.
但是语言层级上没有这种支持, 我们需要自己维护一个引用关系,
在 base 类最后 include
的时候, 把维护好的实例方法/类方法一次性注入进来.
具体来说, 在模块 FirstLevel
中 extend ActiveSupport::Concern
时, 会触发一个钩子:
def self.extended(base) #:nodoc:
base.instance_variable_set(:@_dependencies, [])
end
这里的 self 是 ActiveSupport::Concern
.
这里的 base 是 FirstLevel
, 对其初始化一个实例变量 @_dependencies
, 设置为空数组.
extend 之后, append_features
included
class_methods
会以类方法注到 FirstLevel
.
included
和 class_methods
是可选的, 我们只看 append_features
:
首先明确一点, append_features
跟 included
钩子不同:
append_features
本身是有任务的(检查被引入的模块是否在base的祖先链上, 如果不在就加进去), 所以重载之后务必要 super
;
后者本身是空的, 流水都是利润.
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
@_dependencies.each { |dep| base.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
现在 append_features
是 FirstLevel
的类方法了, 这里的 self 是 FirstLevel
, base 是 Clazz
.
在执行 FirstLevel
的时候, 发现还需要引入 SecondLevel
.
我们再来看 SecondLevel
, 它 extend Concern 的时候, 在它上面初始化了 @_dependencies
为空数组.
SecondLevel
被 FirstLevel
引入的时候触发 append_features(base)
, 此时, self 是 SecondLevel
, base 是 FirstLevel
.
所以进入 if 条件, 将 self 加入 FirstLevel
的 @_dependencies
. 返回 false 表明已经在祖先链上了, 不是新的引入.
Clazz
上木有初始化过 @_dependencies
, 进入 else 条件: 如果 self 已经出现在 base 的祖先链上了, 就直接返回 false.
执行 @_dependencies.each { |dep| base.include(dep) }
这句的时候, base 总是 Clazz
, 达到穿越引用链指向 base 类的目的了.