Rails allows a module with extend ActiveSupport::Concern to be prepended


Rails has added support for prepending concerns.

Prepending

If we prepend a module to a class, the module will be inserted at the bottom of the inheritance chain even before the class itself.

Prepending instance methods in Ruby

module Population
  def preferred_transport
    "by walk"
  end
end

class Human
  prepend Population

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk

In the above example, module Population is prepended to the class Human.

Hence the method preferred_transport of the module Population got precedence over the preferred_transport instance method of the class Human in the lookup chain.

We can verify this by calling ancestors on the class Human as shown below:

Human.ancestors
 => [Population, Human, Object, Kernel, BasicObject]

Prepending class methods in Ruby

module Population
  def self.prepended(base)
    class << base
      prepend ClassMethods
    end
  end

  module ClassMethods
    def count
      5000
    end
  end
end

class Human
  prepend Population

  def self.count
    1000
  end
end

Human.count #=> 5000

In the above example, we extended the methods present in the module ClassMethods using the prepended hook, so that the class method count of the module Population got precedence over the class method count of the class Human.

Human.ancestors
 => [Population, Human, Object, Kernel, BasicObject]

Prepending concerns in Rails

With the prepending support added to the concerns, we can combine the above two examples in a much simpler way as shown below:

module Population
  extend ActiveSupport::Concern

  def preferred_transport
    "by walk"
  end

  class_methods do
    def count
      5000
    end
  end
end


class Human
  prepend Population

  def self.count
    1000
  end

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk
Human.count #=> 5000

In the above example, we did not have to manually add the class methods on a prepended hook as we did earlier.

The ancestor chain for Human will look like this:

[Population, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

We can also use the prepended block to run any custom code, when a module is prepended to a class.

For example:

module Population
  extend ActiveSupport::Concern

  prepended do
    puts "the module is prepended"
  end

  def preferred_transport
    "by walk"
  end
end

The prepended block will run after the module is prepended to a class, similar to how the included block works.

Note: We cannot have multiple prepended blocks in a module.

ActiveSupport::Concern::MultiplePrependBlocks exception will be raised if we try to declare multiple prepended blocks.


Prepending works with concerning as well, we need to define prepend: true just before the start of the concerning block as shown below:

class Human
  concerning :Preferences, prepend: true do
    def preferred_transport
      "by walk"
    end

    class_methods do
      def count
        5000
      end
    end
  end
end

In our case, the ancestor chain for the class Human will look like this:

 [Human::Preferences, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]