Topic: reusable calculations with rails?

Hi all,

I've been wanting to set up some reusable calculations, needed for a couple of controllers, but I'm not too sure of the proper way to do this.

I have the following calculations:

  def calc_points
    @points = Point.find(:first, :conditions => ['user_id = ?', session[:user_id]])
    @score = Score.find(:first, :conditions => ['user_id = ?', session[:user_id]])
    @success = @points.start_success + (@score.total_points * 10)
    @successful = TRUE if @success > rand(@points.decider)
  end

  def calc_score
    @score = Score.find(:first, :conditions => ['user_id = ?', session[:user_id]])
   @total_score = @score.total_points * @score.worth
end


So where do these method calculations belong if they were to be reusable in a couple of controllers? With rails, where does reusable code go anyway, that's all too unclear to me. And also, as you can see I'm calling Score.find() in both calculations. I believe that doesn't follow the DRY rules, does it? How would I best call Score.find() and where, so that I could then re-use it?

Thanks in advance

Last edited by maestro (2007-01-05 18:20:36)

Re: reusable calculations with rails?

For a quick solution, place the methods in the ApplicationController. this way they can be accessed by all controllers.

class ApplicationController < ActionController::Base
 
  private
 
  def calc_points
    @points = Point.find(:first, :conditions => ['user_id = ?', session[:user_id]])
    @score = Score.find(:first, :conditions => ['user_id = ?', session[:user_id]])
    @success = @points.start_success + (@score.total_points * 10)
    @successful = TRUE if @success > rand(@points.decider)
  end

  def calc_score
    @score = Score.find(:first, :conditions => ['user_id = ?', session[:user_id]])
    @total_score = @score.total_points * @score.worth
  end
end


Notice the call to "private", this makes it so the methods aren't considered actions that can be accessed through an HTTP request.

If you want to remove some more of the find duplication, I recommend creating a current_user method which you can reference the score/points through.

class ApplicationController < ActionController::Base
  helper_method :current_user
 
  private
 
  def current_user
    @current_user ||= User.find_by_id(session[:user_id]) if session[:user_id]
  end
 
  def calc_points
    # assuming User has_many scores/points
    current_user.scores.first
    @points = current_user.points.first
    @score = current_user.scores.first
    @success = @points.start_success + (@score.total_points * 10)
    @successful = (@success > rand(@points.decider))
  end

  def calc_score
    @score = current_user.scores.first
    @total_score = @score.total_points * @score.worth
  end
end


Notice the current_user method is defined as a helper_method at the top? This way it will be available in the views too. How cool is that?

You can refactor this further by moving most of the code into the models. It's a little difficult since I don't know what data you need in the view, but I'll make some guesses so you can get an idea.

class ApplicationController < ActionController::Base
 
  private
 
  def current_user
    @current_user ||= User.find_by_id(session[:user_id]) if session[:user_id]
  end
end

class Score < ActiveRecord::Base
  def total
    total_points * worth
  end
end

class User < ActiveRecord::Base
  def success_amount
    points.first.start_sucess + scores.first.total
  end
 
  def successful?
    success_amount > rand(points.first.decider)
  end
end


Then you can call current_user.successful? in the view or controller.

Railscasts - Free Ruby on Rails Screencasts

Re: reusable calculations with rails?

Wow. To me, that is one great post. Learned a lot about where stuff should go, and the way they should be set up. Thanks ryanb.