Abstract Factory
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Also known as: Kit
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Abstract Factory | #tower, #enemy | The creation messages every tile set answers. In Ruby this is a duck type, not a class. |
| Concrete Factory | CompactTiles, LargeTiles | Makes one family of art. |
| Product | CompactTower, CompactEnemy, LargeTower, LargeEnemy | The art itself. Pieces from the same family are the same height. |
| Client | Legend | Draws using whichever tile set it was given. |
Ruby implementation
# 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
endIn 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
#towerand#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.definegives 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.