Skip to content

Latest commit

 

History

History
181 lines (121 loc) · 4.88 KB

10_active_support_concern_module.md

File metadata and controls

181 lines (121 loc) · 4.88 KB

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.

Rails Before Concern

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.

The Include-and-Extend Trick

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:

  1. Instance methods such as #valid? become instance methods of Base.
  2. Ruby calls the included Hook Method on Validations, passing ActiveRecord::Base as an argument.
  3. 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)

The Problem of Chained Inclusions

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.

ActiveSupport::Concern

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'

A Look at Concern's Source Code

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.

Module#apppend_features

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

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.

Inside Concern#append_features

TODO

Concern Wrap-Up

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.