I've observed numerous instances where the included do ... end
method is overused, with all the logic of the concerns being placed there. While this approach works and is more or less equally performant compared to putting some of the logic outside the included
block, there are different semantic situations where you should use it.
The included(&block)
method
This executes code within the given block, in the context of the included class.
For instance, let's consider a model named Flag
with a belongs_to :flaggable, polymorphic: true
. Now, I want to create a concern that can be included in all models capable of having many (has_many
) flags, i.e., models that are Flaggable
. To achieve this, I would create the following concern:
This concern uses the included do...end
method because it uses the has_many
method, available in the designated model.
Why use included(&block)?
The has_many
method is not available in the context of the Flaggable
module. However, it is available in the class that includes the Flaggable
module. Placing has_many
outside the included do ... end
block would result in Ruby indicating that the has_many
method is not defined in the module Flaggable
.
How does included(&block)
work?
The included
method evaluates all the code within the do ... end
block in the context of the included class using class_eval(&block)
.
How to organize methods inside a concern
Suppose I want the Flaggable
module to add the flag!(name, description=nil)
method. This method would only create a flag in the database with the provided name and description. To achieve this, some may be inclined to do the following:
While this approach works, it's important to remember that the concern is a module with some extra syntactic sugar. Therefore, all methods defined in the module become available when included in another class/module. Hence, the above code could (and should!) be changed to:
In both cases, the method is available, and Ruby recognizes that the flag!
method is defined in the Flaggable
concern.
You might find yourself asking the following question:
Suppose bothwork and Ruby knows that both methods are defined in theFlaggable
concern. Why should I care about placing methods inside or outside theincluded
block?
Well, there’s another catch that I still haven’t mentioned, and it has to do with how Ruby handles including modules in classes.
When a module is included in a class, the module is placed in the ancestors chain of the class, immediately before the class itself.
Let’s check the Product
class ancestors:
When calling the flag!
method on an instance of Product
, the Ruby interpreter will look for that method in the ancestors chain, starting from the beginning (Product
class), and then going up the chain, until it is found. If it’s not found, then the NoMethodError
is raised.
Why is this important?
Well, if a method is defined inside the Flaggable
included
block, it’s the same as defining it in the Product
class. If the method is defined in the Product
class, then you cannot redefine it and call super
to override the behavior.
It sounds complex, but this is the desired outcome in some cases.
Writing concerns is great. It lets us developers generalize behavior, wrap it in a module, and then include it in the desired models to give them that behavior. One of the cons of doing this, is that the generalized behavior can be too generic in some cases, and fine tuning must be done in some specific places.
To do that fine tuning, one of the first things that come to mind is overriding the method, writing the extra logic, calling super
, and calling it a day:
Doing this is only possible when the flag!
method that is defined in the concern is placed outside the included block.
Why?
- When the
flag!
method is defined inside the included block, it’s essentially reopening theProduct
class and defining it in theProduct
class itself. - When the
flag!
method is defined outside the included block, it’s being defined in theFlaggable
concern, meaning, one level up in the ancestors chain of theProduct
class; thus, being available for overwriting in the child classes (Product
in this case)
Conclusion
Whether you place methods inside or outside the included do ... end
block may seem inconsequential, as it works in both cases. However, the decision is more abstract, aligning with the idea that concerns encapsulate behavior to supercharge a model.
Nonetheless, there are edge cases where placing methods inside the included(&block)
block can affect the expected behavior, like the one mentioned before.
What to place in the included
block?
You should only place methods that are available in the class you are including the module into (e.g. has_many
, has_one
, belongs_to
, etc.)
Where should I place the methods injected by my concern?
Those should be placed outside the included block (usually below). Then use the private
/protected
modifiers to keep methods used internally by the module as private and not pollute the model with unnecessary methods.