Abstract Factory

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Also known as: Kit

Participants

RoleIn this exampleResponsibility
Abstract Factory#tower, #enemyThe creation messages every tile set answers. In Ruby this is a duck type, not a class.
Concrete FactoryCompactTiles, LargeTilesMakes one family of art.
ProductCompactTower, CompactEnemy, LargeTower, LargeEnemyThe art itself. Pieces from the same family are the same height.
ClientLegendDraws using whichever tile set it was given.

Ruby implementation

abstract_factory.rb
# The game draws the map at two tile sizes. Each size is a family of art
# that has to fit together: every piece in a family is the same height.

# Compact family: one character per tile.
CompactTower = Data.define(:glyph) do
  def lines = [glyph]
end

CompactEnemy = Data.define(:glyph) do
  def lines = [glyph]
end

# Large family: three characters wide, two lines tall.
LargeTower = Data.define(:glyph) do
  def lines = ["/#{glyph}\\", "|_|"]
end

LargeEnemy = Data.define(:glyph) do
  def lines = ["(#{glyph})", "/ \\"]
end

# Concrete factories, one per family. Ruby needs no abstract base class:
# any object that answers #tower and #enemy is a tile set.
class CompactTiles
  def tower(glyph) = CompactTower.new(glyph:)
  def enemy(glyph) = CompactEnemy.new(glyph:)
end

class LargeTiles
  def tower(glyph) = LargeTower.new(glyph:)
  def enemy(glyph) = LargeEnemy.new(glyph:)
end

# The client draws a tower next to an enemy. It only talks to its tile set,
# so it can never mix a compact tower with a large enemy.
class Legend
  def initialize(tiles)
    @tiles = tiles
  end

  def lines(tower_glyph, enemy_glyph)
    tower = @tiles.tower(tower_glyph).lines
    enemy = @tiles.enemy(enemy_glyph).lines
    tower.zip(enemy).map { |left, right| "#{left} #{right}" }
  end
end

In use

Legend.new(CompactTiles.new).lines("A", "g")
# => ["A g"]

Legend.new(LargeTiles.new).lines("A", "g")
# => ["/A\\ (g)",
#     "|_| / \\"]

Legend never names a concrete class. Swap the tile set and every piece of art changes together, so a compact tower can’t end up beside a large goblin.

Notes

Ruby idioms

  • No abstract base class needed. The “abstract factory” is just the messages #tower and #enemy. Any object that answers them will do, including a test double.
  • Classes are objects. If a family is only a set of classes, a hash such as { tower: LargeTower, enemy: LargeEnemy } can serve as a lightweight factory.
  • Data.define gives small, immutable products with value equality for very little code.

Reach for it when

  • A program must work with one of several families of related objects, chosen once: tile sizes, colour themes, database adapters, cloud providers.
  • Objects from different families must never be mixed.

Watch for

  • Adding a new kind of product (say, #scenery) means changing every factory. The pattern makes new families cheap and new products expensive.

One factory per family. Change the factory and every product changes with it.