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 new
s is Good new
s
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.