BLOG
Digging deeper into Rails Concerns
AI generated image of a big futuristic screen

Juan Aparicio

Engineering Manager

Dec 11, 2023

3 min read

Rails

Ruby

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:

Code screenshot

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).

Source Code | More Info

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:

Code screenshot

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:

Code screenshot

In both cases, the method is available, and Ruby recognizes that the flag! method is defined in the Flaggable concern.

Code screenshot

You might find yourself asking the following question:

Suppose bothwork and Ruby knows that both methods are defined in the Flaggable concern. Why should I care about placing methods inside or outside the included 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 the Product class and defining it in the Product class itself.
  • When the flag! method is defined outside the included block, it’s being defined in the Flaggable concern, meaning, one level up in the ancestors chain of the Product 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.

Juan Aparicio

Engineering Manager

Dec 11, 2023

3 min read

Rails

Ruby