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 avoidInstanceMethods
Modules 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_features
andincluded
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.