ActiveSupport::Concern 模块是 Ruby 中很常用,且很重要的一个模块。它鼓励抽取可重用逻辑放到不同的concern里面,规范了 module mix 的代码风格,并且解决了模块加载之间的依赖问题。
例如我们有 Post 和 Advertiser 两个类,这两个类拥有相同的判断是否是活跃状态的代码逻辑,scop、实例方法、类方法:
scope :active, -> {where(is_active: true)}
def active?
is_active
end
def self.all_active(reload = false)
@all_active = nil if reload
@all_active ||= active.all
end
为了重用这部分代码,我们将其抽取出来,放到一个module 中,如下:
module ActAsActivable
def self.included(base)
base.send(:include, InstanceMethods)
base.extend ClassMethods
base.class_eval do
scope :active, where(is_active: true)
end
end
module InstanceMethods
def active?
is_active
end
end
module ClassMethods
def all_active(reload = false)
@all_active = nil if reload
@all_active ||= active.all
end
end
end
在 ActAsActivable
model 中,为了使该module被 include 时可以为类添加这几个方法,我们将scope 实例方法和类方法写入module中,并分别用 InstanceMethods
和ClassMethods
包裹,并利用 hook 方法在被 include 的时候为新类添加新方法。
注意:
- 对于实例方法,我们完全可以不用InstanceMethods
模块来包裹,当它们被 include 的或者 extend 的时候,它们会自动成为新类的实例方法或类方法。
- 而类方法无论如何定义,都无法自动成为新类的类方法,看下面几个例子:
module A def self.test_a end end class B extend A end class C include A end A.test_a # nil B.test_a # NoMethodError: undefined method `test_a' for B:Class C.test_a # NoMethodError: undefined method `test_a' for C:Class C.new.test_a # NoMethodError: undefined method `test_a' for #
- 对于 module 中定义的实例方法,可以通过 include 和 extend 使其成为实例方法或者类方法。但是如果同一个module中,即有类方法,又有实例方法方法,此时简单的 include 或者 extend 无法满足为类同时添加这两类方法的需求。此时我们只能通过添加 include hook 方法来实现。
而添加 include hook 的方式显得十分繁琐和臃肿。而使用 concern 则能很优雅的解决这些。
通过在 ActAsActivable
include Concern
模块,只需要按正常的方式定义实例方法,并将类方法包裹到 ClassMethods
模块,scope 方法写入 include do 模块里,并在需要它的地方使用 include ActAsActivable
即可。
module ActAsActivable
extend ActiveSupport::Concern
included do |base|
scope :active, -> {where(is_active: true)}
end
module ClassMethods
def all_active(reload = false)
@all_active = nil if reload
@all_active ||= active.all
end
end
# instance methods
def active?
is_active
end
end
下面示例来自于 lib/active_support/concern.rb。
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
Bar
模块依赖于Foo
模块,如果我们需要在Host
中使用Bar
,如果直接 include Bar
, 会报找不到 method_injected_by_foo
的错误,所以我们必须在它之前 include Foo
模块。而这并不是我们希望看到的。
通过引入Concern
模块,我们可以不用担心模块依赖的问题。
require 'active_support/concern'
module Foo
extend ActiveSupport::Concern
included do
class_eval do
def self.method_injected_by_foo
...
end
end
end
end
module Bar
extend ActiveSupport::Concern
include Foo
included do
self.method_injected_by_foo
end
end
class Host
include Bar # works, Bar takes care now of its dependencies
end
Concern 源代码非常简单,只有短短三十余行:
module Concern
def self.extended(base)
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")
if const_defined?("InstanceMethods")
base.send :include, const_get("InstanceMethods")
ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \
"no longer included automatically. Please define instance methods directly in #{self} instead.", caller
end
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
end
end
def included(base = nil, &block)
if base.nil?
@_included_block = block
else
super
end
end
end
可以看到,只定义了三个方法:self.extended
,append_features
和included
。
Note:
- 当一个 module 被 include 的时候,会自动调用该 module 的append_features
和included
方法:
static VALUE rb_mod_include(int argc, VALUE *argv, VALUE module) { int i; ID id_append_features, id_included; CONST_ID(id_append_features, "append_features"); CONST_ID(id_included, "included"); rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); for (i = 0; i < argc; i++) Check_Type(argv[i], T_MODULE); while (argc--) { rb_funcall(argv[argc], id_append_features, 1, module); rb_funcall(argv[argc], id_included, 1, module); } return module; }
- 当一个 module 被 extend 的时候,会自动调用该 module 的
extended
和extended_object
方法。
static VALUE rb_obj_extend(int argc, VALUE *argv, VALUE obj) { int i; ID id_extend_object, id_extended; CONST_ID(id_extend_object, "extend_object"); CONST_ID(id_extended, "extended"); rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); for (i = 0; i < argc; i++) Check_Type(argv[i], T_MODULE); while (argc--) { rb_funcall(argv[argc], id_extend_object, 1, obj); rb_funcall(argv[argc], id_extended, 1, obj); } return obj; }
当模块 Foo
extends Concern 时,会发生三件事情:
1. extended
:为 Foo
设置了一个实例变量组 @_dependencies
,里面用来存放Foo
依赖的所有其他的模块。注意,@_dependencies
是实例变量,并不是类变量。
2. append_features
方法被重写。重写后行为有了很大变化,它的处理分两种情况:
- 一种是当它被一个有 @dependencies
实例变量的模块,也就是一个 extend 过ActiveSupport::Concern
的模块 include 时,直接把自身加到 @dependencies
中。 比如当 Bar
include Foo
时,将触发 Foo
的 append_features(base) 方法,此时 base 是 Bar,self 是 Foo,由于 Bar 已经 extend ActiveSupport::Concern
,Bar
的 @dependencies
有定义,所以直接把 Foo 加到 Bar 的 @dependencies
中,然后直接返回,没有立即执行 mixing 操作。
- 另一种是没有@dependencies
定义的时候,也就是被没有 extend ActiveSupport::Concern
的类 include 时。例如,当 Host
include Bar
时,将触发 Bar
的 append_features(base) 方法,此时 base 是 Host
,self 是 Bar
,Host
没有 extend ActiveSupport::Concern
,所以 Host
的 @dependencies
无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies
获得 ),然后 include Bar
(通过 super),然后是后续操作。
3. included
方法被重写。添加了新的功能 - 如果方法调用的时候没有参数,则将代码块的逻辑放入@_included_block
中。之所以放入@_included_block
是为了可以在发生依赖的时候,可以逐级调用所有模块的block
方法。