Skip to content

Instantly share code, notes, and snippets.

@walreyes
Created January 16, 2017 01:18
Show Gist options
  • Save walreyes/2b705234f60e1c9ac91d04360bc0f0d7 to your computer and use it in GitHub Desktop.
Save walreyes/2b705234f60e1c9ac91d04360bc0f0d7 to your computer and use it in GitHub Desktop.

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.

What is the Factory Pattern?

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.

Tommy The Assistant

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.

image

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 Action's Validator

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
And Specs.

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

Summary

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment