P.S. I originally published this post last year, when I was just starting to learn Rails. Since then, I've learned a few more things and hence decided to edit, revise, polish, and re-publish it. Also, a ton of new subscribers have joined the email list, and I thought you all might find it helpful.
Many Rails applications, including the framework itself, make heavy use of concerns. However, if you are new to Rails, concerns can be confusing to understand. When I started learning Rails last year, concerns were one of the topics that took a really long time for me to understand.
The Rails guide on concerns mixes in unrelated concepts such as validations, migrations, and views, making it overwhelming to understand the purpose of concerns to someone new to Rails. The Rails API documentation doesn’t help either. It starts with the assumption that the reader is already familiar with the problem concerns are trying to solve, and goes on to provide the solution, only adding to the confusion.
So I did some reading and found the web has many interesting nuggets of concern wisdom scattered around, like this blog post from DHH demonstrating the usage of concerns, or these answers from the A May of WTFs thread from the Core Rails members explaining concerns with a metaphor of writing.
This blog post attempts to summarize these scattered pieces of information to paint a coherent picture, showing you how concerns can help you write better code.
Here’s what I want you to get from this article:
- What is a concern?
- Why and when should you use concerns?
- How to use a concern?
- The rationale behind concerns
Sounds interesting? Let's begin...
What is a Concern?
A concern is a module that you extract to split the implementation of a class or module in coherent chunks, instead of having one big class body. The API surface is the same one, they just help organize the code. A concern is a regular Ruby mixin, there’s nothing else, it is not a new concept. It is plain Ruby. - Xavier Noria
A Rails concern is just a plain Ruby module that extends the ActiveSupport::Concern
module provided by Rails. Here’s a concern named Taggable.
module Taggable
extend ActiveSupport::Concern
included do
# any code that you want inside the class
# that includes this concern
end
class_methods do
# methods that you want to create as
# class methods on the including class
end
end
In Ruby, when a class includes another module, it gets all the methods defined in that module as if it had defined them. You can still do this with a concern, which I repeat, is just another Ruby module.
However, turning a Ruby module into a Rails concern allows us to do the following interesting things:
- Include class methods in addition to instance methods
- Define additional code such as validations, callbacks, or constants on the including class.
A concern does this by providing the following two blocks:
included
- Any code inside this block is evaluated in the context of the including class. If
Post
class includes a concern, anything inside theincluded
block will be evaluated as if it was written inside thePost
class. - Typically, this block is used to define Rails macros like validations, associations, and scopes. Any methods you create here become instance methods of the including class.
class_methods
- Any methods that you add here become class methods on the including class.
- Alternatively, you can create a nested module named
ClassMethods
instead of theclass_methods
block to define the class methods. See the API docs for an example.
For more details on evaluating the code in the context of another class, please read the following article.
Okay, by now, you must have a pretty good understanding of what a concern is. In the next section, we'll learn why and when should you use one, instead of a plain Ruby module.
Why and When Should You Use Concerns?
Using a concern lets you extract the common logic from different classes into a reusable module.
Consider two common models: Post and Comment, which represent a blog post and its comments, just like the one you're reading right now. A post has rich text and it can have multiple comments.
In addition to the code specific to these models, both classes contain the code that handles their visibility, specifically:
visible_to
attribute,is_visible
instance method, andall_visible
class method
# post.rb
class Post < ApplicationRecord
belongs_to :author
has_many :comments, dependent: :delete_all
has_rich_text :content
validates :title, presence: true, length: { minimum: 2 }
validate :has_valid_content
# common data
attr_accessor :visible_to
# common instance method
def is_visible?
visible_to.present?
end
def has_valid_content
# some code
end
# common class method
def self.all_visible
all.select { |item| item.is_visible? }
end
end
# comment.rb
class Comment < ApplicationRecord
belongs_to :post
validates :commenter, :body, presence: true
# common data
attr_accessor :visible_to
# common instance method
def is_visible?
visible_to.present?
end
# common class method
def self.all_visible
all.select { |item| item.is_visible? }
end
end
You must have noticed the code that checks the model's visibility is duplicated in both classes. You can imagine there could be other similiar models that need to control their visibility in the same way.
It would be nice if there was a way to abstract the visibility-related code.
Concerns let you do exactly that.
How to Use a Concern?
Let’s create a Visible
concern to extract the visibility-related code from the Post
and Comment
models. We'll put the attribute and the instance method inside the included
block and the class method inside the class_methods
block.
Visible
concern deals with an entity’s visibility, its primary concern is to check if that entity is visible or not.# visible.rb
module Visible
extend ActiveSupport::Concern
# The code inside the included block is evaluated
# in the context of the class that includes the Visible concern.
# You can write class macros here, and
# any methods become instance methods of the including class.
included do
attr_accessor :visible_to
def is_visible?
visible_to.present?
end
end
# The methods added inside the class_methods block (or, ClassMethods module)
# become the class methods on the including class.
class_methods do
def all_visible
all.select { |item| item.is_visible? }
end
end
end
To use this concern, you include the module as usual.
For example, if the Post
model wants the visibility
functionality, it includes the Visible
concern. Including this concern adds the visible_to
attribute, is_visible
instance method, and the all_visible
class method to the Post
class.
Let's see how the Visible
concern simplifies both Post
and Comment
classes.
class Post < ApplicationRecord
include Visible
belongs_to :author
has_many :comments, dependent: :delete_all
has_rich_text :content
validates :title, presence: true, length: { minimum: 2 }
validate :has_valid_content
def has_valid_content
# some code
end
end
class Comment < ApplicationRecord
include Visible
belongs_to :post
validates :commenter, :body, presence: true
end
Let's run the following test to ensure none of the existing behavior in the Post
class was changed.
require "test_helper"
class PostTest < ActiveSupport::TestCase
test "Post can include the visible concern" do
post = Post.new
assert_not post.is_visible?
post.visible_to = "reader"
assert_equal "reader", post.visible_to
assert post.is_visible?
assert_equal [], Post.all_visible
end
end
It passes with flying colors.
If you're following along with me, here's your challenge: write the passing test for theComment
model that uses theVisible
concern.
The Rationale Behind Concerns
At this point you might be wondering: what’s the point of all this?
If we are trying to abstract some common behavior at a central location, can’t we simply create a new class, encapsulating the query in a stand-alone object? And you’d be right.
You can create a class containing the shared code, instantiate it, and use it.
visibility_manager = Visibilty.new
visibility_manager.is_visible?(post)
However, concerns are often just the right amount of abstraction, resulting in a friendlier API. All the methods you need are right there, on the primary object, and you don’t need to introduce an intermediate object like visibility_manager
to access the shared code.
Which code is more readable?
post.is_visible?
# vs.
visibility_manager.is_visible?(post)
I admit, not everyone will agree here. In fact, since writing this post, a few people raised concerns (no pun intended!) that using concerns is a bad practice, composition is better than inheritance, concerns make the code difficult to read and test, they bloat your models, and so on and so on.
Personally, I found concerns very refreshing; coming from the C#/.NET world, I was used to creating Managers, Services, Commands, etc. all the time. Now I mostly prefer concerns, as they result in cleaner APIs on the models and I haven’t run into any headaches with testing etc.
Regarding readability, Rails makes heavy use of concerns. They are everywhere! My experience reading the Rails codebase has been a delight. I’ve been in the Rails ecosystem for just over a year, and find Rails source code an absolute pleasure to read.
At first, I thought the concerns (and modules) were confusing (so much hidden code!), but now I just like to think of them as neatly organized drawers. Agree, the room (model) has too many drawers, but at least everything is neatly organized, and once I find the concerned file, I know what’s going on.
Nowadays, if I come across a new feature, I just open the Rails source code and see how it's implemented. Personally, I find reading the source much more efficient than the guides and sometimes, the API. Finally, I haven't yet come across code that was difficult to read and test due to concerns.
Also, no one is preventing you from using composition and creating services and managers and the whole lot to separate your dependencies. I have nothing against them. If you want a much more nuanced post on concerns and various debates surrounding them, I recommend you read Jason Swett's excellent article: When used intelligently, Rails concerns are great.
So if you're still confused between the two approaches, here's what I recommend: write both versions side-by-side, take a look at them both, and choose whichever one you like. It's that simple!
Here’s how DHH addresses this concern (no pun intended) ;)
So that’s another way of putting this: It’s a writing style. Like using subheads to explain subservient ideas within a broader context. You could probably extract all those subheads out, and turn them into little essays of their own. But that’s often just not the right level of extraction. - A May of WTFs
That’s what concerns are: A way to group related methods together, while still living under a single class.
It’s structured writing.
That's a wrap. I hope you liked this article and you learned something new.
If you want to go even deeper on concerns, I highly recommend you read this excellent article from Jorge Manrubia, a developer at 37signals. It's one of the best programming articles I've read in a long time.
If you want to read similar deep dives on my blog, check out these articles:
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.