ActiveSupport::Concern 模块是 Ruby 中很常用,且很重要的一个模块。它鼓励抽取可重用逻辑放到不同的concern里面,规范了 module mix 的代码风格,并且解决了模块加载之间的依赖问题。
鼓励公用逻辑抽取规范代码风格解决 module 之间的依赖原理剖析例如我们有 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 #<C:0x007fc0f629b5d0> 对于 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 endBar模块依赖于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 endConcern 源代码非常简单,只有短短三十余行:
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方法。