Key Points about ActiveSupport::Concern

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中,并分别用 InstanceMethodsClassMethods包裹,并利用 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

解决 module 之间的依赖

下面示例来自于 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_featuresincluded

Note:
- 当一个 module 被 include 的时候,会自动调用该 module 的append_featuresincluded 方法:

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 的extendedextended_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::ConcernBar@dependencies 有定义,所以直接把 Foo 加到 Bar 的 @dependencies 中,然后直接返回,没有立即执行 mixing 操作。
- 另一种是没有@dependencies定义的时候,也就是被没有 extend ActiveSupport::Concern的类 include 时。例如,当 Host include Bar 时,将触发 Bar 的 append_features(base) 方法,此时 base 是 Host,self 是 BarHost 没有 extend ActiveSupport::Concern,所以 Host@dependencies 无定义,将执行下面的分支,首先 include Foo(通过 Bar 的 @dependencies 获得 ),然后 include Bar (通过 super),然后是后续操作。
3. included方法被重写。添加了新的功能 - 如果方法调用的时候没有参数,则将代码块的逻辑放入@_included_block中。之所以放入@_included_block是为了可以在发生依赖的时候,可以逐级调用所有模块的block方法。

你可能感兴趣的:(RUBY)