Concerns are something that every Ruby on Rails developer has probably heard of. Whenever you create a brand new project in ruby on rails, a default directory named `app/controllers/concerns` and `app/models/concerns` is created, yet many people find concerns troublesome. In this article, we’ll discuss whether developers should be apprehensive about the concern file or not, and why many have negative perceptions regarding the concern file.
Here is a brief idea of what we are going to cover in this post.
What are Rails’ Concerns?
In Rails, ActiveComponent is a very large component and is responsible for many functionalities, including language extensions and utilities. Many features like pluralization of different strings and RailsInfector provide various patterns to transform simple ruby strings.
In Rails 4, the Active support has been released first time in the market. Concerns are used for making code more reusable and less complicated. If we use concerns wisely, then we can do it the easy way.
Let us consider an example of an application where we are logging in to some user. For this, we would need to have a user model for taking the data of any users.
If we want to perform some actions based on user details, we could easily do it using the information submitted. For example, now we want to have a registration feature that takes all the information as log in.
Logically we have to separate the model for registration, called the Registration model. At this point, we may need to rewrite a user model, but that would be more time-consuming. Now you can wrap the part of code in the “registration” model and make it a different class to use it anywhere in the project. This is where ActiveSupport::Concern helps us.
What is Mixin?
Set of codes that can be added to other classes are called mixins. Whenever you see something like include SomeModule or extend AnotherModule, these are called mixins. As we know, a module is a mixture of constants and functions. Here we are encapsulating them into different classes so other classes can use them. This is exactly what we did in the following example, where we have encapsulated the logic of trashing into one module and can be used in different places. Hence mixin is a design pattern used in ruby and rails.
How Rails Concerns Work
Concerns are just a module that extends the ActiveSupport::Concern module. Suppose we have to encapsulate some cohesive chunk of functionality into a mixin that can extend the target class’s behavior by annotating the class’ ancestor chain, annotating the class’ singleton class’ ancestor chain, and directly manipulating the target class through the included() hook. This is where ActiveSupport::Concern comes into play. It provides us the mechanics to encapsulate the functionality into a mixin. A basic example is given below.
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
In the above example, ActiveSupport::Concern allows you to wrap up the concerned code for evaluation inside the block. In the above example, you are wrapping up the trashed logic out of your model. To use your model anywhere in the project, use the include tag to include the model’s concern. See the code below.
class Song < ApplicationRecord
include Trashable
has_many :authors
# ...
end
Difference between Rails concerns and modules
After seeing the logic and workflow of Rails concerns, it may look very similar to modules in Rails, but some differences need mentioning.
The first and foremost difference is that we have to use #included and #class_method in concerns; we can also use the `self.included` hook with the additional module `ClassMethods` creation. The dependency resolution for modules included in each other is better in the case of Concerns. In the case of concerns, you are making one module. You are creating your own module rather than using the pre-built library or module.
Let us practically understand the difference between modules and concerns with the help of some code.
Example for a typical module is:
module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
module ClassMethods
...
end
end
After using ActiveSupport::Concern the module will look like the following:
require "active_support/concern"
module M
extend ActiveSupport::Concern
included do
scope :disabled, -> { where(disabled: true) }
end
class_methods do
...
end
end
As you can see, we do not have to use module dependencies every time; it handles it intelligently. Take a module Tom and a module Jerry, which depends on Tom. In the case of the module you may write it as follows:
module Tom
def self.included(base)
base.class_eval do
def self.method_injected_by_tom
...
end
end
end
end
module Jerry
def self.included(base)
base.method_injected_by_tom
end
end
class Host
include Tom # We need to include this dependency for Jerry
include Jerry # Jerry is the module that the Host really needs
end
But we could use Jerry directly in the host by including Tom in Jerry, as written below:
module Bar
include Tome
def self.included(base)
base.method_injected_by_tom
end
end
class Host
include Jerry
end
But above code will not work because when Tom is included, its base is Jerry, not the Host class. We have to use ActiveSupport::Concern to handle dependencies correctly.
require "active_support/concern"
module Tom
extend ActiveSupport::Concern
included do
def self.method_injected_by_tom
...
end
end
end
module Bar
extend ActiveSupport::Concern
include Tom
included do
self.method_injected_by_tom
end
end
class Host
include Jerry # It works, now Jerry takes care of its dependencies
end
How to Use Concerns in Rails
While concerns in Rails are used in many ways, it can also have side effects. Many people consider it a tool whenever reducing the size of your app. But this is not true in most cases, using concerns randomly just for the sake of reducing size can be harmful to your app.
In some cases, the concern could be extremely helpful in improving the performance of the app.
Inline concerns are also allowed in Rails. Let us look at one example of using inline concerns and whether it is profitable for our code or not. Below is a code example:
class Todo
# Other todo implementation
# ...
module EventTracking
extend ActiveSupport::Concern
included do
has_many :events
before_create :track_creation
after_destroy :track_deletion
end
def notify_next!
#
end
private
def track_creation
# ...
end
end
include EventTracking
end
At first sight, you would say it looks nice but let us first analyze the code. The first thing to observe is we have the include tag in only one place (that is on the second last line). You must be wondering why it’s best to use concerns when those modules can be written in just a few lines of code. According to the Rails docs, we want to encapsulate the code in a different module to reuse that code in different places when that same functionality is needed.
We use concerns when we feel that one file is overwhelmed with the functionality, so we extract some part of the code and save it into another file or module. Now we have two files with us.
First :
class Todo < ActiveRecord::Base
# Other todo implementation
# ...
include EventTracking
end
Second:
module EventTracking
extend ActiveSupport::Concern
included do
has_many :events
before_create :track_creation
after_destroy :track_deletion
end
def notify_next!
#
end
private
def track_creation
# ...
end
end
Now the real problems start.
Bad Readability
Periodically you want to search for a file or functionality in your code, but when you look into your code, you don’t find any such string of functionality directly in your main file. You would possibly end up searching “app/models/todo.rb”; however, you may not find anything worthwhile because the todo file will be large because of the vast codebase.
Eventually, you will find this line:
include EventTracking
When you run a global search, you will find all the usage of this string. But suppose there are too many similar lines, making it difficult for you to search for any method or string. For example, imagine a case where you have to find the below lines in 400 lines of code:
include EventPlanning
include EventTracking
include EventCoordination
include EventWorker
include Evanescence
include EvanIsAName
You have to search each of them one by one to find a specific functionality. As a result, it does not make sense to split the data as it will be complicated to find the functionality again. It is better to have one file, so if any problem or bug occurs, we only need to search one file splitting it into N parts and searching in N files.
When to use concern?
In this section, we have provided some basic use cases where concern should not be used. When something is miswritten, it’s easy to see the flaws, but it’s much harder to detect abnormalities when something appears correct. The concern is ideally meant to work in isolation, not third-party dependencies, and the type of responsibilities should be only infrastructure and framework related. Other than that, business logic and algorithms should not be modeled as concerns but rather as abstract classes.
The testing of concerns is a bit harder; that is why good concerns can reproduce some issues. Another important problem that concerns arises is the “is a” problem. In this issue, the object of a class directly inherits the behavior, so more and more properties and responsibilities are aggregated in the concerts as we add more concerns.
People use Rails concern for mainly for two things:
First, to reduce the size of a model that could be separated into several models, and therefore reducing the size.
Second, to avoid declaring the same class over and over again. We define a separate model for some functionality that may be reused in some other activities in concerns. So concerns reduce the duplicity of classes.
Both of the problems could be solved in multiple ways, including concerns. According to the use, all the processes have their pros and cons and can either be appropriate or not. But we will stay focused on Rail’s concern.
Inheritance better than composition
Concerns work like inheritance, but many prefer composition over inheritance. This is because compositions are more flexible and can be changed easily when dependency injection is needed. In contrast, inheritance is more rigid as most languages do not allow a change in type. However, that doesn’t mean that inheritance shouldn’t be used. There are many cases where inheritance is preferable to composition. While concerns use inheritance, concerns are not always bad. Concerns can be a better option than inheritance in certain places, just as inheritance can be the better option in other instances.
Circular dependency problem
Circular dependency is a matter of concern while using concern created. Inheritance does not create this because the parent class doesn’t know anything about child class, meaning it is not dependent. It is also true that concern does not behave similarly, which may cause a problem. But not using concern does not guarantee you that you will not face that problem, so use concern appropriately.
Code spread across multiple files
Another famous problem noted while using concerns is that code spreads in different files, making searching any function or variable difficult. If it is a problem for concern, then it must be a problem with inheritance. So it is not just a problem of concern but also inheritance. You can solve this problem by simply searching or making fewer modules when using concern.
When to Avoid Using Concerns
It’s important to know when we should avoid using rails concerns as what should not be done is more than what should be done.
Many users misuse rails concerns when making class sizes smaller. This is most common in projects where Rubocop is used in the CI process. With this in mind, it’s not completely the user’s fault.
Anyone looks for the easiest solution, and whenever the size of the file exceeds a threshold value the quickest solution is extracting concern from the file. Extraction wraps a certain amount of line or functionality in a separate class, reducing the number of lines of code and hence size.
The truth is, wrapping some function into a separate class is ineffective. Even when we move code to a separate file while we compile and during runtime, it is still used in the original file. This means modules are just implementation; actual code is still there at runtime.
The second mistake is by using concern in rails is taking losing explicitness. You can check out the following example:
class Document
include Emailable, Storable, Erasable
def archive
@archived = true
deliver({to: 'me@mydomain.com', subject: 'Document archived', body: @content})
remove
end
end
The method deliver looks a bit confusing because of its arguments and can be assumed it is implemented through Emailable concern. But there is one function (i.e., remove), and are two concerns in the program: Storable and Erasable. So it is difficult to predict which concern has implemented the remove function.
Now suppose there 10-20 implemented methods and the same amount of concerns. It would be very difficult to recognize the program’s flow as we can not easily see which concern has implemented which function.
Let us take another case when concerns are used incorrectly. Look at the following code:
module Printable
include ActiveSupport::Concern
def print
raise UnknownFormatError unless ['pdf', 'doc'].include?(@format)
# do print @content
end
end
class Document
include Printable
def initialize(format, content)
@format = format
@content = content
end
def export
# ...
print
end
end
In the above example, the Document uses the Printable concern. But if you look carefully, the @content and @format instance attributes are also there, which indicates the Printable concern also knows about the implementation detail of the Document class.
This is called Bi-directional dependencies, in which the superclass knows the child class’s implementation details.
Always remove the Bi-directional dependencies. The flow of data should only go one way, and even if communication is required, it should be declared explicitly through the public interface.
But how will you fix these Bi-directional dependencies? The fixed version of the above code is given below:
module Printable
include ActiveSupport::Concern
def print(format, content)
raise UnknownFormatError unless ['pdf', 'doc'].include?(format)
# do print content
end
end
class Document
include Printable
def initialize(format, content)
@format = format
@content = content
end
def export
# ...
print(@format, @content)
end
end
Closing Thoughts
After discussing concerns in rails in detail, we can say that concerns are similar to Rails modules used to extract some code parts and make it cleaner. And while no problem is perfect for concerns in Ruby, as there can be other methods, it is still beneficial.
We hope you enjoyed this article and have a better understanding of which problems can be solved by concerns in ruby and which ones cannot. Now it’s your turn to explore concerns in ruby. Happy Coding.