Factory Pattern in Ruby


A discussion came up today discussing how the factory pattern looks in certain languages, and I found myself thinking about how it might look in Ruby. A quick Google resulted in an example that seemed to violate part of the reason for the Factory Pattern in the first place (there was a distinct factory for every subclass?), so I decided to ponder my own contrived example to refresh my knowledge.

Factory Pattern Contrived Example: Shapes

One example that I quickly thought of (but quickly became useless beyond three sides) was a factory that got an instance of a Shape subclass when given a variable number of sides. Ultimately, a Triangle can have its perimeter and area determined if only the lengths of the sides are known, but any shape beyond that needs either diagonals or angles specified. So I settled on the factory producing either a Triangle that could report back an area or a perimeter or a generic Polygon (arbitrarily excluding Triangle from this) that would only report back perimeter and raise an InterdeterminateArea if the area was requested.

No news is Good news

A hallmark of the factory pattern is blocking instantiation directly on the target classes (in this case, making new protected so that only classes within the hierarchy can access it. If our .new method is properly protected, then we should receive a NoMethodError calling .new. (It is enough to assert that NoMethodError is raised because otherwise, all classes should respond to .new:

The MiniTest cases

require 'minitest/autorun'
require_relative 'shape_factory'

class TestShapeFactory < MiniTest::Unit::TestCase
  def test_triangle
    triangle=Shape.get_shape(3,3,3)
    assert_in_epsilon(3.89, triangle.area, 0.01)
    assert_equal(9, triangle.perimeter)
    assert_instance_of(Triangle, triangle)
  end

  def test_polygon
    polygon=Shape.get_shape(4,4,4,4)
    assert_equal(16, polygon.perimeter)
    assert_raises(IndeterminateArea) {polygon.area}
    assert_instance_of(Polygon, polygon)
  end

  def test_new_shape
    assert_raises(NoMethodError) {Triangle.new}
    assert_raises(NoMethodError) {Polygon.new}
    assert_raises(NoMethodError) {Shape.new}
  end
end

Above we have are testing that get_shape with 3 arguments returns a Triangle that can return an area (using assert_in_epsilon because the result is irrational) and a perimeter. test_polygon tests that a shape with 4 sides returns a Polygon that can correctly return a perimeter but raises an InterdeterminateArea when #area is called. Finally, test_new_shape validates that we have correctly blocked .new calls on Shape, Polygon, and Triangle.

shape_factory.rb

class Shape
  # be sure to protect {class}.new, not instance.new
  class << self
    protected :new
  end

  def self.get_shape(*sides)
    case sides.length
    when 3
      Triangle.new(*sides)
    else
      Polygon.new(*sides)
    end
  end

  def perimeter
    @perimeter ||= @sides.inject(:+)
  end

  def area
    raise 'not implemented'
  end

  def initialize(*sides)
    @sides = sides
  end
end

class Triangle < Shape
  def area
    # Heron's Formula
    Math.sqrt(perimeter/2.0*(@sides.map {|s| perimeter/2.0 - s}.inject(:*)))
  end
end

class IndeterminateArea < StandardError; end

class Polygon < Shape
  def area
    raise IndeterminateArea
  end
end

Ultimately, Polygon could probably be collapsed into Shape and only return Triangle. Of course, implementations of shapes with more sides could be made to provide area, but they would require providing additional information to get_shape that might expose the need for the sender to have an awareness of implementation details. Another option might be to pass the successive vertices of the polygon which might allow deeper inferring of how to calculate meaningful information from the shape, but this example is a start with a factory pattern implementation that can be expanded as necessary.


Leave a Reply

%d bloggers like this: