I discovered the Factory Pattern earlier this year. I was eager to apply it in a real life problem and last month I finally had the chance to use it.
I was introduced to this pattern by a code kata called Gilded Rose. This kata faces the problem of calculating the prices of different types of tickets. Once you go through a couple of refactors you end up with the Factory Pattern, which allows you to easily add more types of tickets.
I think it's a very handy asset to have in your mental toolbox.
The Factory is a module that fabricates different classes with common functionality. Each fabricated class responds to the same method but each execution is different.
You tell the Factory what you want to do, and it will return you a class that can do it.
Let's clear up the concept.
Let's say you're in this scenario: you've been working on a project that it's already in production and has a huge codebase. We are constantly asked new features, but we also need to handle maintenance.
The maintenance involves getting into the rails console
to fix some data issue caused by a user mistake or by the legacy code on the system. This fixes are very recurrent and you end up running the same scripts over and over again. This maintenance takes up to 70% of your day and you still want to deliver features. That's when you realize you could build an Assistant that can handle most of the maintenance for you.
Let's call it Tommy The Assistant
.
Let's think of it this way: Tommy
has a set of actions
that he knows how to do. You tell Tommy
what you want to do, he will look for the proper action
and he will perform
it.
Tommy is a Module
module TommyTheAssistant
end
He knows a set of ACTIONS
and how to perform
.
module TommyTheAssistant
ACTIONS = {}.freeze
end
Tommy is able to assist
you with some of the ACTIONS
he knows. You tell him the name of the action along with some context that he needs to know, and he'll do it for you.
module TommyTheAssistant
ACTIONS = {}.freeze
def self.assist(action_name = nil, params = {})
action = ACTIONS.fetch(action_name.try(:to_sym))
action.new(params)
end
end
So, what does an action
looks like? Tommy
is a trade of all cards, he is able to perform a wide variety of stuff: from making a sandwich, to writing a poem or building a plane. He is a quick learner.
Let's teach him how to make an avocado sandwich with some tuna. We will tell Tommy that we want a sandwich with tuna and avocado, the number of avocados and he will make it for us.
But first, I will explain you the anatomy of an action:
- Each action should implement the
#perform
method. - Each action should be included in
TommyTheAssistant.ACTIONS
. - Each action responds to a
#successes
method, this method should return an array of success messages assigned during#perform
. - Each action responds to an
#errors
method, this method should return an array of error messages assigned during#perform
. - Each action should have a
Validator
, this validator will check if you are indeed able to perform the action.
The validator should have a #valid?
method that will perform a series of checks to make sure the action can be performed.
In the FixMeASandwichWithAvocadoAndTuna
scenario, we will check that the number_of_avocados
is greater than 0.
module TommyTheAssistant
class FixMeASandwichWithAvocadoAndTuna
class Validator
attr_reader :errors
def initialize(number_of_avocados)
@number_of_avocados = number_of_avocados
@errors = []
end
def valid?
can_make_sandwich?
errors.empty?
end
private
def can_make_sandwich?
@errors << "I can't put negative number of avocados in your sandwich" if !valid_number_of_avocados?
@errors << "Seriously? only #{@number_of_avocados} avocados?! Nonsense, I will only make a sandwich with at least 5 avocados. Try again." if !decent_number_of_avocados?
end
def valid_number_of_avocados?
@number_of_avocados.present? && @number_of_avocados > 0
end
def decent_number_of_avocados?
@number_of_avocados.present? && @number_of_avocados > 4
end
end
end
end
And the action class.
module TommyTheAssistant
class FixMeASandwichWithAvocadoAndTuna
attr_reader :successes
def initialize(params = {})
@number_of_avocados = params["number_of_avocados"]
@avocados_validator = TommyTheAssistant::FixMeASandwichWithAvocadoAndTuna::Validator.new(@number_of_avocados)
@successes = []
end
def perform
make_sandwich! if @avocados_validator.valid?
successes.any? && errors.empty?
end
def errors
@eggs_validator.errors
end
private
def make_sandwich!
# You can perform anything you want here!
@successes << "You can find your sandwich here: #{sandwich_url}, Enjoy!"
end
def sandwich_url "http://www.deliciousavocados.co.uk/images/mega_tuna_avocado_sandwich.jpg"
end
end
end
Let's add this new action to Tommy's toolbox!
module TommyTheAssistant
ACTIONS = {
fix_me_a_sandwich_with_avocado_and_tuna: FixMeASandwichWithAvocadoAndTuna
}.freeze
def self.assist(action_name = nil, params = {})
action = ACTIONS.fetch(action_name.try(:to_sym))
action.new(params)
end
end
Are you getting hungry? Let's ask Tommy for a sandwich:
tommy_the_chef = TommyTheAssistant.assist(:fix_me_a_sandwich_with_avocado_and_tuna, { number_of_avocados: 4})
if tommy_the_chef.perform
puts tommy_the_chef.successes
else
puts tommy_the_chef.errors
end
Now, if we want to teach Tommy
to build a house, we just need to replicate what we did with FixMeASandwichWithAvocadoAndTuna
but with a class named BuildAHouse
.
But what if you ask Tommy
something that he does not know how to do? We can add a DEFAULT_ACTION
where he merely tells you he does not know how to perform what you asked, he is humble like that.
module TommyTheAssistant
DEFAULT_ACTION = ActionNotFound
ACTIONS = {
fix_me_a_sandwich_with_avocado_and_tuna: FixMeASandwichWithAvocadoAndTuna
}.freeze
def self.assist(action_name = nil, params = {})
action = ACTIONS.fetch(action_name.try(:to_sym), DEFAULT_ACTION)
action.new(params)
end
end
module TommyTheAssistant
class ActionNotFound
attr_reader :successes, :error
def initialize(params = {})
@successes = []
@errors = []
end
def perform
@errors << "Sorry, I don't know how to perform that, you can teach me though."
successes.any? && errors.empty?
end
end
end
tommy_with_limits = TommyTheAssistant.assist(:lick_your_elbow)
if tommy_with_limits.perform
puts tommy_with_limits.successes
else
puts tommy_with_limits.errors
end
These are specs that ensure the action is implementing the anatomy properly.
You can add it_behaves_like 'a tommy assistant action'
. To your action's spec!
RSpec.shared_examples "a tommy the assistant action" do
let(:action) { described_class.new({}) }
it "implements a Validator class" do
summary_class = "#{described_class}::Validator".constantize
expect(summary_class).to be_a_kind_of(Class)
end
it "responds to #perform" do
expect(action).to respond_to(:perform)
end
it "responds to #successes" do
expect(action).to respond_to(:successes)
end
it "responds to #errors" do
expect(action).to respond_to(:errors)
end
it "is added to TommyTheAssistant::ACTIONS" do
actions_found = TommyTheAssistant::ACTIONS.values.select do |value|
value[:class_name] == described_class
end
expect(actions_found.length).to eq 1
end
end
And that's it for the Factory Pattern! There will be a follow up post on how to use TommyTheAssistant
in a rails app. We will do a set up to automatically let users use actions, display success or error messages and add logs for every action performed.