< Return to Blog

A Treatise on Resolving Module Dependencies with ActiveSupport::Concern#append_features

Here's a quick script to play with which details the process by which ActiveSupport::Concern resolves module dependencies by hooking into Method#included, Method#extended, and Method#append_features.

I'll include a snippet of the full-source below, which at least contains the gory details on internal mechanics. You'll find it far more useful to read this and then compare it with the output of my script excuting rails runner lib/scripts/concerns.rb, which gives a sense of context as to which bits are eval'd first

# Notice how module B's dependency on D is resolved by `ActiveSupport::Concern`
module B
  extend ActiveSupport::Concern

  @_dependencies = [D]

  def self.append_features(mod)
    super
    puts "B.append_features: mod=#{mod}, self=#{self}\n\n"
  end

  def self.included(mod)
    super
    puts "B.included: #{self} included in #{mod}\n\n"
  end

  # 2. Since B is included in class C, the included hook is called.
  # `Module#included` hook (from Ruby) is overwritten by the `included` instance
  # method of `ActiveSupport::Concern`.
  #
  # Since the `base` is `nil`, `@included_block` is set to the block passed as
  # an instance variable in `Concern`.
  included(nil) do  # base is set as nil by default
    # the block passed
    puts "This is the included block in module #{self}!"

    # the call to `meaning_of_life_and_the_universe` is evaluated in the context
    # of this block, which is class C (see end of (3) for further details).
    puts "Meaning of life and the universe? => #{meaning_of_life_and_the_universe}\n\n"
  end

  # 3. When a module (B) is included in another (C),
  # `Method#append_features(mod)` is called in module (B),
  # passing it the receiving module (C) in `mod`
  #
  # Therefore, the arg `base` below is C.  Normally, the following
  # method is not defined in B, but having do so for clarity, we have
  # super, which would be `ActiveSupport::Concern#append_features`, which
  # has overwritten `Method#append_features`.
  #
  # `ActiveSupport::Concern#append_features` checks if C has the
  # `@_dependencies` instance variable defined.However, since
  # we did not extend `ActiveSupport::Concern` in class C, it does
  # not have this instance variable set.
  #
  #   # ActiveSupport::Concern
  #   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)
  #       base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
  #     end
  #   end
  #
  # Therefore, the else clause in `ActiveSupport::Concern#append_features`
  # is, in effect, triggered.
  #
  # This iterates through each of the modules stored in
  # `@_dependencies` and includes them in class C (base). We can have
  # multiple modules in the `@_dependencies` instance variable, and we have
  # them all included in `base` here, so that if one depends on another,
  # they are all available now in class C.
  #
  # Note, its call to `super`, effectively calling `Method#append_features`.
  #
  # It then extends ClassMethods if they are defined in module B. It
  # finally invokes `class_eval` method on base (class C), passing in the
  # instance variable `@_included_block` if it is defined.

end

class C

  attr_reader :ivar
  def initialize
    @ivar = "I'm instance #{self}"
  end

  class << self
    def meaning_of_life_and_the_universe
      '42'
    end
  end

  include A
  include B         # 1. Since module B extends `ActiveSupport::Concern`, when
                    # it is included into class C, it invokes the extended hook
                    # `ActiveSupport::Concern.extended` with module B as `base`.
                    #
                    # Here, the instance variable `@_dependencies` is set in B
                    # as []

end

You can fork my Github repo here https://github.com/bsodmike/concern-expose.