Tuesday 16 September 2014

Concerns in Rails Part 1 : Using concerns in Models

With Rails 4, the concept of using concerns has been in highlights. Concerns provide a easy yet powerful technique to group similar code of a model or across multiple models  in a module (that extends ActiveSupport::Concern ). This concern module is then included in the required models. 

Concerns in models can be used both for 
1) DRYing up model codes (by domain based grouping of common codes between models )
2) Skin-nizing Fat Models (by dividing the code based on domains and creating a concern for each domain.)

1) DRYing up model codes
Consider a Article model, a Event model and a Comment Model. A article or A 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: true
end
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
  end
end
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
  end
end
As we can notice, there is a significant piece of code common to both Event and Article Model. Using concerns we can extract this common code in a separate module Commentable.
For this create a commentable.rb file in app/model/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
    end 
end

As you can see while using concerns:

     * The included block is executed within the context of the class that is including the module.
     *  All the class methods go into the module ClassMethods of the concern (no need to add
        'self.' to these methods).
     * The instance methods are written directly in the concern module.   

And Now your models look like this :
Comment Model:
    class Comment < ActiveRecord::Base
      belongs_to :commentable, polymorphic: true
    end
Article Model:
class Article < ActiveRecord::Base
    include Commentable
end
Event Model:
class Event < ActiveRecord::Base    
    include Commentable
end

2) Skin-nizing Fat Models.

Consider a Event model. A event has many notes 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
    end
end
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.rd and commentable.rb file in app/model/concern/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
    end
end
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
    end
end
And now using the above Concerns , your Event model reduces to
class Event < ActiveRecord::Base    
    include Commentable
    include Attendable
end


* It is advisable to make concerns domain based rather than technical based. For instance,  concerns like 'Commentable', 'Photoable' are examples of domain based concerns. While concerns like 'FinderMethods', 'ValidationMethods' are examples of technically grouped concerns.


Here are links of some write ups for Model Concerns that I found very useful :

No comments:

Post a Comment