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]