Ruby Science
Use Class as Factory
An Abstract Factory is an object that knows how to build something, such as one of several possible strategies for summarizing answers to questions on a survey. An object that holds a reference to an abstract factory doesn’t need to know what class is going to be used; it trusts the factory to return an object that responds to the required interface.
Because classes are objects in Ruby, every class can act as an abstract factory. Using a class as a factory allows us to remove most explicit factory objects.
Uses
- Removes duplicated code and shotgun surgery by cutting out crufty factory classes.
- Combines with convention over configuration to eliminate shotgun surgery and case statements.
Example
This controller uses one of several possible summarizer strategies to generate a summary of answers to the questions on a survey:
# app/controllers/summaries_controller.rb
class SummariesController < ApplicationController
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summarize(summarizer)
end
private
def summarizer
case params[:id]
when 'breakdown'
Breakdown.new
when 'most_recent'
MostRecent.new
when 'your_answers'
UserAnswer.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
endThe summarizer method is a Factory Method. It returns a
summarizer object based on params[:id].
We can refactor that using the abstract factory pattern:
def summarizer
summarizer_factory.build
end
def summarizer_factory
case params[:id]
when 'breakdown'
BreakdownFactory.new
when 'most_recent'
MostRecentFactory.new
when 'your_answers'
UserAnswerFactory.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
endNow the summarizer method asks the
summarizer_factory method for an abstract factory, and it
asks the factory to build the actual summarizer instance.
However, this means we need to provide an abstract factory for each summarizer strategy:
class BreakdownFactory
def build
Breakdown.new
end
end
class MostRecentFactory
def build
MostRecent.new
end
endclass UserAnswerFactory
def initialize(user)
@user = user
end
def build
UserAnswer.new(@user)
end
endThese factory classes are repetitive and don’t pull their weight. We
can rip two of these classes out by using the actual summarizer class as
the factory instance. First, let’s rename the build method
to new, to follow the Ruby convention:
def summarizer
summarizer_factory.new
end
class BreakdownFactory
def new
Breakdown.new
end
end
class MostRecentFactory
def new
MostRecent.new
end
endclass UserAnswerFactory
def initialize(user)
@user = user
end
def new
UserAnswer.new(@user)
end
endNow, an instance of BreakdownFactory acts exactly like
the Breakdown class itself, and the same is true of
MostRecentFactory and MostRecent. Therefore,
let’s use the classes themselves instead of instances of the factory
classes:
def summarizer_factory
case params[:id]
when 'breakdown'
Breakdown
when 'most_recent'
MostRecent
when 'your_answers'
UserAnswerFactory.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
endNow we can delete two of our factory classes.
Next Steps
- Use convention over configuration to remove manual mappings and possibly remove more classes.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.