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.