Key Points about ActiveSupport::Concern

Keywords: Ruby

ActiveSupport::Concern module is a very common and important module in Ruby. It encourages extractable reusable logic to be placed in different concerns, standardizes the code style of module mix, and solves the dependency problem between module loads.

Encourage Common Logic Extraction and Standardize Code Style

For example, we have Post and Advertiser classes, which have the same code logic to determine whether they are active, scop, instance method, class method:?

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

To reuse this part of the code, we extract it and put it into a module, as follows:

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

In ActAs Activable Model, in order to add these methods to the class when the module is included, we write the scope instance method and class method into the module, wrap them with Instance Methods and ClassMethods respectively, and use hook method to add new methods to the new class when it is included.

Be careful:
- For example methods, we can completely avoidInstanceMethodsModules come wrapped when they are wrapped include Or extend When they do, they automatically become instance methods or class methods of new classes.
- Class methods, however defined, cannot automatically become class methods of new classes. Look at the following examples:

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>
  • For instance methods defined in module, they can be made instance methods or class methods by include and extend. But if there are class methods and instance methods in the same module, simple include or extend can not satisfy the need to add both types of methods to the class at the same time. At this point, we can only do it by adding include hook.

The way to add include hook s is cumbersome and bloated. Using concern can solve these problems gracefully.
Through the ActAs Activable include Concern module, you just need to define the instance method in the normal way, wrap the class method into the ClassMethods module, write the scope method into the include do module, and use include Activable where you need it.

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

Resolve dependencies between module s

The following example is from 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

The Bar module depends on the Foo module. If we need to use Bar in Host, if we include Bar directly, we will not find the method_injected_by_foo error, so we must include the Foo module before it. And that's not what we want to see.
By introducing Concern modules, we can avoid worrying about module dependency.

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

Principle analysis

Concern source code is very simple, only about 30 lines:

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

As you can see, only three methods are defined: self.extended,append_features and included.

Note:
- When one module cover include When this is called automatically module Ofappend_featuresandincluded Method:

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;
}
  • When a module is extended, the extended and extended_object methods of the module are automatically called.
    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;
    }

Three things happen when the module Foo extends Concern:
1. extended: Set up an instance variable group @_dependencies for Foo, which is used to store all other modules that Foo depends on. Note that @_dependencies are instance variables, not class variables.
2. The append_features method is rewritten. Behavior has changed greatly after rewriting, and it can be dealt with in two ways:
- One is to add itself directly to @dependencies when it is included by a module with @dependencies instance variables, that is, an extended module over ActiveSupport::Concern. For example, when Bar includes Foo, the append_features(base) method of Foo will be triggered. Base is Bar and self is Foo. Because Bar has extended Active Support:: Concern, Bar's @dependencies are defined, Foo is added directly to Bar's @dependencies, and then returned directly without immediate mixing operation.
The other is when there is no @dependencies definition, that is, when there is no extended ActiveSupport:: Concern class included. For example, when Host includes Bar, Bar's append_features(base) method will be triggered, where base is Host, self is Bar, and Host does not extend ActiveSupport::Concern, so Host's @dependencies are undefined and the following branches will be executed, including Foo (obtained through Bar's @dependencies), Bar (via super), and then Bar (via super). Continued operation.
3. The included method is rewritten. New functionality has been added - if there are no parameters at the time of method invocation, the logic of the code block is put into @_included_block. The @_included_block is put in order to call the block method of all modules step by step when dependencies occur.

Posted by AshrakTheWhite on Tue, 01 Jan 2019 00:54:08 -0800