How to use concerns in Rails 4 How to use concerns in Rails 4 ruby-on-rails ruby-on-rails

How to use concerns in Rails 4


So I found it out by myself. It is actually a pretty simple but powerful concept. It has to do with code reuse as in the example below. Basically, the idea is to extract common and / or context specific chunks of code in order to clean up the models and avoid them getting too fat and messy.

As an example, I'll put one well known pattern, the taggable pattern:

# app/models/product.rbclass Product  include Taggable  ...end# app/models/concerns/taggable.rb# notice that the file name has to match the module name # (applying Rails conventions for autoloading)module Taggable  extend ActiveSupport::Concern  included do    has_many :taggings, as: :taggable    has_many :tags, through: :taggings    class_attribute :tag_limit  end  def tags_string    tags.map(&:name).join(', ')  end  def tags_string=(tag_string)    tag_names = tag_string.to_s.split(', ')    tag_names.each do |tag_name|      tags.build(name: tag_name)    end  end  # methods defined here are going to extend the class, not the instance of it  module ClassMethods    def tag_limit(value)      self.tag_limit_value = value    end  endend

So following the Product sample, you can add Taggable to any class you desire and share its functionality.

This is pretty well explained by DHH:

In Rails 4, we’re going to invite programmers to use concerns with the default app/models/concerns and app/controllers/concerns directories that are automatically part of the load path. Together with the ActiveSupport::Concern wrapper, it’s just enough support to make this light-weight factoring mechanism shine.


I have been reading about using model concerns to skin-nize fat models as well as DRY up your model codes. Here is an explanation with examples:

1) DRYing up model codes

Consider a Article model, a Event model and a Comment model. An article or an event has many comments. A comment belongs to either Article or Event.

Traditionally, the models may look like this:

Comment Model:

class Comment < ActiveRecord::Base  belongs_to :commentable, polymorphic: trueend

Article Model:

class Article < ActiveRecord::Base  has_many :comments, as: :commentable   def find_first_comment    comments.first(created_at DESC)  end  def self.least_commented   #return the article with least number of comments  endend

Event Model

class Event < ActiveRecord::Base  has_many :comments, as: :commentable   def find_first_comment    comments.first(created_at DESC)  end  def self.least_commented   #returns the event with least number of comments  endend

As we can notice, there is a significant piece of code common to both Event and Article. Using concerns we can extract this common code in a separate module Commentable.

For this create a commentable.rb file in app/models/concerns.

module Commentable  extend ActiveSupport::Concern  included do    has_many :comments, as: :commentable  end  # for the given article/event returns the first comment  def find_first_comment    comments.first(created_at DESC)  end  module ClassMethods    def least_commented      #returns the article/event which has the least number of comments    end  endend

And now your models look like this :

Comment Model:

class Comment < ActiveRecord::Base  belongs_to :commentable, polymorphic: trueend

Article Model:

class Article < ActiveRecord::Base  include Commentableend

Event Model:

class Event < ActiveRecord::Base  include Commentableend

2) Skin-nizing Fat Models.

Consider a Event model. A event has many attenders and comments.

Typically, the event model might look like this

class Event < ActiveRecord::Base     has_many :comments  has_many :attenders  def find_first_comment    # for the given article/event returns the first comment  end  def find_comments_with_word(word)    # for the given event returns an array of comments which contain the given word  end   def self.least_commented    # finds the event which has the least number of comments  end  def self.most_attended    # returns the event with most number of attendes  end  def has_attendee(attendee_id)    # returns true if the event has the mentioned attendee  endend

Models with many associations and otherwise have tendency to accumulate more and more code and become unmanageable. Concerns provide a way to skin-nize fat modules making them more modularized and easy to understand.

The above model can be refactored using concerns as below:Create a attendable.rb and commentable.rb file in app/models/concerns/event folder

attendable.rb

module Attendable  extend ActiveSupport::Concern  included do     has_many :attenders  end  def has_attender(attender_id)    # returns true if the event has the mentioned attendee  end  module ClassMethods    def most_attended      # returns the event with most number of attendes    end  endend

commentable.rb

module Commentable  extend ActiveSupport::Concern  included do     has_many :comments  end  def find_first_comment    # for the given article/event returns the first comment  end  def find_comments_with_word(word)    # for the given event returns an array of comments which contain the given word  end  module ClassMethods    def least_commented      # finds the event which has the least number of comments    end  endend

And now using Concerns, your Event model reduces to

class Event < ActiveRecord::Base  include Commentable  include Attendableend

* While using concerns its advisable to go for 'domain' based grouping rather than 'technical' grouping. Domain Based grouping is like 'Commentable', 'Photoable', 'Attendable'. Technical grouping will mean 'ValidationMethods', 'FinderMethods' etc


It's worth to mention that using concerns is considered bad idea by many.

  1. like this guy
  2. and this one

Some reasons:

  1. There is some dark magic happening behind the scenes - Concern is patching include method, there is a whole dependency handling system - way too much complexity for something that's trivial good old Ruby mixin pattern.
  2. Your classes are no less dry. If you stuff 50 public methods in various modules and include them, your class still has 50 public methods, it's just that you hide that code smell, sort of put your garbage in the drawers.
  3. Codebase is actually harder to navigate with all those concerns around.
  4. Are you sure all members of your team have same understanding what should really substitute concern?

Concerns are easy way to shoot yourself in the leg, be careful with them.