Prefer Objects as Method Parameters, Not Class Names

Joe Ferris

In an application we worked on, we presented users with multiple choice questions and then displayed summaries of the answers. Users could see one of several summary types. You could view the percentage of users who selected the correct answer, or see a breakdown of the percentage of users who selected each answer.

Some of these summary classes were simple:

class MostRecentAnswer
  def summary_for(question)
    question.most_recent_answer_text
  end
end

We allowed the user to select which summary to view, so we accepted a summary_type as a parameter. We needed to pass the summarizer around, so we accepted a class name in the parameters and that name directly to our model.

class SummariesController < ApplicationController
  def index
    @survey = Survey.find(params[:survey_id])
    @summaries = @survey.summaries_using(params[:summary_type])
  end
end

class Survey < ActiveRecord::Base
  has_many :questions

  def summaries_using(summarizer_type)
    summarizer = summarizer_type.constantize.new
    questions.map do |question|
      summarizer.summary_for(question)
    end
  end
end

This works, but it set us up for trouble later.

The Survey#summaries_using method accepts a class name, which means it can only reference constants instead of objects.

I’ve come to call this “class-oriented programming,” because it results in an over-emphasis on classes. Because code like this can only reference constants, it results in classes which use inheritance instead of composition.

Runtime vs Static State

Some Rails applications live with much of their data trapped in static state. Anything that isn’t a local or instance variable is static state. Here are some examples:

VERSION = 2

cattr_accessor :version
self.version = 2

@@version = 2

We don’t usually talk about “static” methods and attributes in Ruby, but all of the information contained in the above example is static state, because only one reference can exist at one time for the entire program.

This becomes a problem when you want to mix static state and runtime state, because static state is viral, as static state can only compose other static state.

Runtime State in Rails Applications

In our original example, you would be able to get away with using a class-based solution, because the MostRecentAnswer summarizer doesn’t need any information besides the question to summarize.

Here’s a new challenge: after the summary of each answer, also include the current user’s answer. Such a summarizer could be implemented in a decorator:

class WithUserAnswer
  def initialize(base_summarizer, user)
    @base_summarizer = base_summarizer
    @user = user
  end

  def summary_for(question)
    user_answer = question.answer_text_for(@user)
    base_summary = @base_summarizer.summary_for(question)
    "#{base_summary} (Your answer: #{user_answer})"
  end
end

This won’t work with a class-based solution, though, because the parameters to the initialize method vary for different summarizers. These parameters may have little in common and may be initialized far away from where they’re used, so it doesn’t make sense to pass all of them all of the time.

We can rewrite our example to pass an object instead of a class name:

class SummariesController < ApplicationController
  def index
    @survey = Survey.find(params[:survey_id])
    @summaries = @survey.summaries_using(summarizer)
  end

  private

  def summarizer
    if params[:include_user_answer]
      WithUserAnswer.new(base_summarizer, current_user)
    else
      base_summarizer
    end
  end

  def base_summarizer
    params[:summary_type].constantize.new
  end
end

class Survey < ActiveRecord::Base
  has_many :questions

  def summaries_using(summarizer)
    questions.map do |question|
      summarizer.summary_for(question)
    end
  end
end

Now that Survey accepts a summarizer object instead of a class name, we can pass objects which combine static and runtime state, like the current user.

The controller still uses constantize, because it’s not possible to pass an object as an HTTP parameter. However, by avoiding class names as much as possible, this example has become more flexible.

What’s next

You can learn more about factories, composition, decorators and more in Ruby Science.