Friday 21 September 2012

Keep Finders on Their Own Model

Keep Finders on Their Own Model

Moving the find calls out of the controller layer in your Rails application and into custom finders on the model is a strong step in the right direction of producing maintainable software.

For Example

Optimize(1)

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @members = @user.members.where(:active => true).limit(5).order("last_active_on DESC")
  end
end

We know that we diligently move that scope chain into a method on the User model.
Now If we write code in Model then

Optimize(2)

class UsersController < ApplicationController
  def index
    @user = User.find(params[:id])
    @recent_active_members = @user.find_recent_active_members
  end
end

In User Model

class User < ActiveRecord::Base
  has_many :members
  
  def find_recent_active_members
    memberships.where(:active=>true).limit(5).order("last_active_on DESC")
  end
end

This is definitely an improvement. UsersController is now much thinner, and the method name reveals intent nicely. But you can do more...

By making use of the power of Active Record associations, you can trim up this example even further. You can define a finder on member, and you can then acess that finder through the User#members association, as follows:

Optimize(3)

class User < ActiveRecord::Base
  has_many :members
  def find_recent_active_members
    member.find_recently_active
  end
end

class Member < ActiveRecord::Base
  belongs_to :user
  def self.find_recently_active
    where(:active=>true).limit(5).order("last_active_on DESC")
  end
end

Now, This is much better. The application now honors the MVC boundaries and delegates domain model responsibilities cleanly. But you can do more...

You can make use of scopes here to produce a variety of finders on Membership that you can then chain together to get the results you're looking for:

Optimize(4)

class User < ActiveRecord::Base
  has_many :members
  
  def find_recent_active_members
    members.only_active.order_by_activity.limit(5)
  end

end

class Member < ActiveRecord::Base
  belongs_to :user
  
  scope :only_active, where(:active => true)
  scope :order_by_activity, order('last_active_on DESC')

end

By making the re-factoring in this last step, you've generalized a lot of the code. Instead of only having a Member#find_recently_active method, you now have three class methods that you can mix and match to your heart's desire.


1 comment: