0

Code Concerns in Rails 4 Models

Warm up

Different models in your Rails application will often share a set of cross-cutting concerns and you may have not noticed that there's a folder called 'concerns' was added from Rails 4. In fact, this feature has been here for a long time and it is pretty simple but powerful concept. Basically, every chunks of code which are common and show up in multiple models should be extracted and reused as much as possible, it helps to prevent models from getting fat and messy.

Here is a simple example. Let say we have a model called User and typically, it would have some methods like below:

class User < ActiveRecord::Base
  has_secure_password

  def self.authenticate(email, password)
    user = find_by_email(email)
    user if !user.nil? && user.authenticate(password)
  end

  def authenticate(password)
    self.password == password
  end
end

Then we can create new file like app/models/concerns/authenticate.rb and extract those methods of User model inside:

module Authentication
  extend ActiveSupport::Concern

  included do
    has_secure_password
  end

  module ClassMethods
    def authenticate(email, password)
      user = find_by_email(email)
      user if user && user.authenticate(password)
    end
  end

  def authenticate(password)
    self.password == password
  end
end-

Now, let's see how far the model can be refactored with new code:

class User < ActiveRecord::Base
  include Authentication
end

So the functionality hasn't changed while the code was in other fashion and you may noticed that app/models/concerns is automatically part of load path, including its children can't be easier. This is just one of many benefits concerns bring for us, as we can see, it is DRYing up model codes and of course, it is useful for one want to have skinny models like Kate Moss.

How It Works

Let's walk through what happens when you use Concern, starting with extend Concern:


module Concern
  def self.extended(base) #:nodoc:
    base.instance_variable_set("@_dependencies", [])
  end
end

Just like included, modules have an extended callback that is called whenever the module extends something. Concern uses this to stash an instance variable on the base class being extended. The instance_variable_set and instance_variable_get methods allow you to access an object's internal instance variables:

rank = Ranking.new

rank.instance_variables_
#=> [:@ranking_type, :@period, :@category_id, :@num_found, :@products, ...]
rank.instance_variable_get(:@ranking_type)
#=> nil

# Update the internal state:
rank.instance_variable_set(:@rank_type, "Diamond")
rank.rank_type
#=> "Diamond"

We'll see that @_dependencies is used to identify modules using Concern, and to hold information needed later.

Another Point Of View

Clothes don't make a man! Let's take a deep breath and isn't ActiveSupport::Concern just an alternate syntax for include/extend? ?

  • Using concerns, it means seperating your concerns, definitely, codebase will be actually harder to navigate with old those concerns around.
  • 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.
  • Honestly, are the concerns some kinds of service, module or whatever layer in fancy describe. Does it help to improve performance of Rails app or just code/visualization performance ?

I don't use them. I find that they add complexity to my system without providing too much value. Adding dependencies and hiding complexity is a choice that you should make according to your desires and experience. Just ask yourself those above questions, try to find the answers and watch my next series about Rails's facts. Hopefully I can find something interesting to represent to you.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí