on programming Rails for fun and profit...

Concerns in Rails: Everything You Need to Know

Concerns are an important concept in Rails that can be confusing to understand for those new to Rails as well as seasoned practitioners. This post explains why we need concerns, how they work, and how to use them to simplify your code.

Concerns in Rails
Concerns in Rails

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 got into Rails last year, concerns were one of the topics that took a 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 research 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:

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 Concern in Rails is just a Ruby module that extends the ActiveSupport::Concern module provided by Rails. In Ruby, when a class includes another module, it gets all the methods defined in that module as if it had defined them.

Turning a Ruby module into a Rails concern allows us to do the following:

  1. Include class methods in addition to instance methods
  2. Define additional code such as validations, callbacks, or constants into a class so that the including class can use them.

A concern does this by providing the following two blocks:

included

  • The code inside the included block is evaluated in the context of the including class. For example, if Post includes a concern, anything inside the included block will be evaluated as if it was written inside Post. For more details on evaluating the code in the context of another class, see my article class_eval vs. instance_eval in Ruby.
  • You can write class macros (validations, associations, scopes, etc.) here, and any methods become instance methods of the including class.

class_methods

  • The methods added inside this block become the class methods on the including class.
  • Instead of the class_methods block, you can create a nested module named ClassMethods.

Here’s a concern named Taggable.

module Taggable
  extend ActiveSupport::Concern

  included do
    # any code that you want inside your class
  end

  class_methods do
    # define class methods here
  end
end

Why Use Concerns?

Using a concern lets you extract the common logic from different classes into a reusable module.

Consider the following two models: Post and Comment. In addition to the code specific to them, both models contain the code that handles their visibility, i.e. the visible_to attribute, is_visible instance method, and count_all_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

  attr_accessor :visible_to

  def is_visible?
    visible_to.present?
  end

  def has_valid_content
    # some code
  end

  def self.count_all_visible
    all.select { |item| item.is_visible? }
  end
end

# comment.rb

class Comment < ApplicationRecord
  belongs_to :post
  validates :commenter, :body, presence: true

  attr_accessor :visible_to

  def is_visible?
    visible_to.present?
  end

  def self.count_all_visible
    all.select { |item| item.is_visible? }
  end
end

You can imagine there could be other models that need to control their visibility similarly. It would be nice if there was a way to abstract this 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. In general, the Visible concern deals with an entity’s visibility, concerning if it 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 count_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 wanted the visibility functionality, it would include the Visible concern. Including this concern adds the visible_to attribute, is_visible instance method, and the count_all_visible class method to the Post class.

This is how the Visible concern simplifies both the 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.count_all_visible
  end
end

It passes with flying colors.

The Rationale Behind Concerns

You might wonder: what’s the point of all this cleverness? 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)

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.


I hope you liked this article and you learned something new. I sure did. If you have any questions or feedback, please send me an email. I look forward to hearing from you.

You might also enjoy these articles:

If you liked this post, and would like to receive future posts directly in email, please subscribe below.

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe