In Rails, you can include modules and you gain both instance and class methods. How does that happen?
ActiveSupport::Concern allows you to add both and makes it easy to roll that functionality into other modules.
ActiveRecord::Base is an assembly of dozens of modules that define both instance methods and class methods.
Before ActiveSupport::Concern there was another way to roll those methods into Base.
In Rails 2, all validation methods were defined in ActiveRecord::Validations (there was no Active Model at this time).
module ActiveRecord
module Validations
# ...
def self.included(base)
base.extend ClassMethods
# ...
end
module ClassMethods
def validates_length_of(*attrs) # ...
# ...
end
def valid?
# ...
end
# ...
end
end
When ActiveRecord::Base includes Validations, three things happen:
- Instance methods such as
#valid?
become instance methods of Base. - Ruby calls the included Hook Method on Validations, passing ActiveRecord::Base as an argument.
- The hook extends Base with the ActiveRecord::Validations::ClassMethods module.
As a result, Base gets both instance methods (#valid?
) and class methods (#validates_length_of
)
What happens when you include a module that includes another module?
module SecondLevelModule
def self.included(base)
base.extend ClassMethods
end
def second_level_instance_method; 'ok'; end
module ClassMethods
def second_level_class_method; 'ok'; end
end
end
module FirstLevelModule
def self.included(base)
base.extend ClassMethods
end
def first_level_instance_method; 'ok'; end
module ClassMethods
def first_level_class_method; 'ok'; end
end
include SecondLevelModule
end
class BaseClass
include FirstLevelModule
end
BaseClass inlcudes FirstLevelModule, which in turn includes SecondLevelModule, so you can call both modules' instance methods on an instance of BaseClass.
BaseClass.new.first_level_instance_method # => 'ok'
BaseClass.new.second_level_instance_method # => 'ok'
For the class methods, however:
BaseClass.first_level_instance_method # => 'ok'
BaseClass.second_level_instance_method # => NoMethodError
When Ruby calls SecondLevelModule#included, the base parameter is not BaseClass, but FirstLevelModule. As a result, SecondLevelModule#ClassMethods become class methods on FirstLevelModule. ActiveSupport::Concern was meant to solve this.
require 'active_support'
module MyConcern
extend ActiveSupport::Concern
def an_instance_method; 'an instance method'; end
module ClassMethods
def a_class_method; 'a class method'; end
end
end
MyClass.new.an_instance_method # => 'an instance method'
MyClass.a_class_method # => 'a class method'
Two important methods: extended
and append_features
. Here is extended
:
module ActiveSupport
module Concern
class MultipleIncludedBlocks < StandardError
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
def self.extended(base)
base.instance_variable_set(:@_dependencies, [])
end
# ...
When a module extends Concern, it calls the extended Hook Method, and defines an @_dependencies
(initialized to an empty array) class instance variable on the includer.
This is a core Ruby method that gets called whenever you include a module (similar behaivor to module#included). However, there is a big difference between the two:
included
is a Hook method that is normally empty and exists to be overwritten.
append_features
is not empty and checks whether the included module is already in the includer's chain of ancestors, and if not, adds it.
If you override append_features
, you can get some surprising results. For example,
module M
def self.append_features(base); end
end
Class C
include M
end
# No module M in the ancestor chain
C.ancestors # => [C, Object, Kernel, BasicObject]
Concern#append_features overrides Module#append_features. append_features
is an instance method on Concern, so it becomes a class method on modules that extend Concern.
For example, if a module Validations extends Concern, then it gains a Validation.append_features class method.
TODO
ActiveSupport::Concern is a minimalistic dependency management system wrapped into a single module with just a few lines of code.
The code is complex, but using Concern is easy.
Is it too clever? That depends on who you ask. There are many success stories using metaprogramming, but there is a dark side to it as well.