Factory Pattern Revisited


Recently I read here that there are GOF patterns which cannot be applied for every language (in Ruby, metaprogramming takes place so that some patterns are unnecessary). But I’m sure that factories are still sexy, no matter what programming language is used. That’s simply an intelligent way to create objects in one place instead of scatter “new Type()” everywhere in your code. That’s true for Ruby as well.

The most famous gem for that is FactoryGirl. Since I want always know what happens behind the scenes I just perform some experiments to detect how could this mechanism work.

Look at this example copied from FactoryGirl:

FactoryGirl.define do
  factory :user do
    first_name "John"
    last_name  "Doe"
    admin false
  end
end

# Returns a User instance that's not saved
user = FactoryGirl.build(:user)

With a few tricks you can do this awesome job as well:

  • use class instance methods for declaring methods define, factory and create.
  • the block passed for define is executed immediately
  • the block passed for factory is stored as Template for deferred evaluation
  • the call of create picks the right template from the hash and evaluates it.
    the several calls of non-existent methods (like color,…) will cause that our call will be caught up as Ghost Method by method_missing. Landed here, we can perform accessor lookup and call the right writer method on the created instance of the object.

Here are some disadvatages of this mechanism:

  • create won’t store the instance in the database (persistence isn’t object of this post)
  • you need attr_accessor for the affected fields

Your classes look like this:

# class: Fabric
class Fabric
  attr_accessor :color
  attr_accessor :material
end

# class: Car
class Car
  attr_accessor :brand
  attr_accessor :horsepower
end

FactoryBoy implementation:

# primitive factory implementation
#
#
class FactoryBoy

  #Template object storing the_type, the_class and the_block
  class Template
    attr_accessor :the_type
    attr_accessor :the_class
    attr_accessor :the_block
  end

  class << self

    # singleton attribute
    attr_accessor :templates
  end

  # executes the block immediately
  def self.define(&block)
    self.instance_eval &block
  end

  # detect the class from the passed symbol
  # adds a new template to templates. A template consists of string, class and block.
  def self.factory(*args,&block)
    type = args[0]
    type = type.to_s if type.class == Symbol
    raise "first parameter should be symbol or string" if type.class != String
    clazz = Kernel.const_get(type.to_s.capitalize)

    tpl = Template.new
    tpl.the_type = type
    tpl.the_class = clazz
    tpl.the_block = Proc.new(&block)
    self.templates = {} if self.templates.nil?
    self.templates[type] = tpl
  end

  # create instance of the type
  def self.create(type)
    tpl = self.templates[type.to_s]
    the_clazz = tpl.the_class
    the_block = tpl.the_block

    # define the_instance attribute as result of the creation
    class << the_clazz
      attr_accessor :the_instance

      # set val of the attribute name
      def set_value(name,val)
        self.the_instance = self.new if self.the_instance.nil?
        setter = "#{name}="
        if self.the_instance.methods.include?( setter.to_sym )
          self.the_instance.send(setter,val)
        end
      end

    end

    # this is needed because of not existing methods. We have
    # to convert them to appropriate write accessors
    def the_clazz.method_missing(id, *args, &block)
      self.set_value(id,*args[0])
    end

    # reset the_instance
    the_clazz.the_instance = nil

    # perform deferred evaluation
    the_clazz.instance_eval &the_block

    # return the result
    the_clazz.the_instance
  end

  # create the instance and override the attribute value
  def self.build(type,*attrs)
    the_instance = self.create(type)

    attrs.each do |e|
      e.each do |k,v|
        the_instance.class.set_value(k,v)
      end
    end
    the_instance
  end
end

Usage of FactoryBoy:

FactoryBoy.define do

  factory :fabric do
    color 'red'
    material 'cotton'
  end

  factory :car do
    brand 'Ford'
    horsepower 150
  end

end

Check it out!

f0 = FactoryBoy.create(:fabric)
puts f0.inspect
f1 = FactoryBoy.build(:fabric, :color => "yellow", :material => "Silk")
puts f1.inspect
f2 = FactoryBoy.build(:fabric, :color => "black")
puts f2.inspect

f3 = FactoryBoy.create(:car)
puts f3.inspect
f4 = FactoryBoy.build(:car, :horsepower => 400, :brand => "Mercedes")
puts f4.inspect
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s