Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Builder | LevelBuilder | Collects settings one step at a time and assembles the level at the end. |
| Director | TutorialLevel | Knows a recipe, the steps to call, without knowing how the level is put together. |
| Product | Level | The finished object. Immutable once built. |
Ruby implementation
# The finished product: a level, immutable once built.
Level = Data.define(:gold, :lives, :waves)
Wave = Data.define(:enemy, :count)
# The Builder collects choices one step at a time, then assembles the level.
class LevelBuilder
def self.build
builder = new
yield builder
builder.level
end
def initialize
@gold = 120
@lives = 20
@waves = []
end
def gold(amount)
@gold = amount
self
end
def lives(count)
@lives = count
self
end
def wave(enemy, count)
@waves << Wave.new(enemy:, count:)
self
end
def level
raise ArgumentError, "a level needs at least one wave" if @waves.empty?
Level.new(gold: @gold, lives: @lives, waves: @waves.dup.freeze)
end
end
# A Director knows a recipe, and can drive any builder through it.
class TutorialLevel
def construct(builder)
builder.gold(200).wave(:goblin, 3).wave(:goblin, 6).wave(:orc, 2)
end
endIn use
# Chained: each step returns the builder.
LevelBuilder.new.gold(300).lives(5).wave(:goblin, 8).wave(:troll, 1).level
# => #<data Level gold=300, lives=5, waves=[...]>
# Block form: reads like a level description.
LevelBuilder.build do |level|
level.lives(1)
level.wave(:orc, 4)
end
# With a director supplying the recipe.
TutorialLevel.new.construct(LevelBuilder.new).level.waves.size
# => 3
Notes
Ruby idioms
- Return
selffrom each step to allow chaining. - Yield the builder from a class method (
LevelBuilder.build { |level| ... }) for a block-based DSL. This is how many Ruby libraries, like gem specifications and Rails routes, build configuration. - Check at the end.
#levelis the one place that enforces rules, such as “at least one wave”, before handing back a valid, frozen product.
Reach for it when
- A constructor would need many optional arguments, or arguments that only make sense together.
- Tests need to set up an interesting state in a few readable lines instead of many separate calls.
Watch for
- If keyword arguments with defaults cover it,
Level.new(gold: 300, waves: [...])is simpler. Use a builder when the process matters.
The director knows the steps. The builder knows how to put the parts together.