State
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Also known as: Objects for States
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Context | Game | Has the public interface and delegates every request to its current state. |
| State | #start_wave, #clear_wave, #break_through, #can_build? | The interface every state shares. |
| Concrete State | Building, WaveInProgress, Won, Lost | Decides what each request means in that state, and answers with the next state. |
Ruby implementation
# The Context: a game that delegates every request to its current state.
class Game
FINAL_WAVE = 10
def initialize
@state = Building.new(0)
end
def start_wave
@state = @state.start_wave
self
end
def clear_wave
@state = @state.clear_wave
self
end
def break_through
@state = @state.break_through
self
end
def can_build? = @state.can_build?
def status = @state.to_s
end
# Concrete States: each one decides what the requests mean right now,
# and answers with the next state (or itself, if nothing changes).
class Building
def initialize(waves_cleared)
@waves_cleared = waves_cleared
end
def start_wave = WaveInProgress.new(@waves_cleared + 1)
def clear_wave = self
def break_through = self
def can_build? = true
def to_s = "Building: wave #{@waves_cleared + 1} is next"
end
class WaveInProgress
def initialize(wave)
@wave = wave
end
def start_wave = self
def clear_wave = @wave == Game::FINAL_WAVE ? Won.new : Building.new(@wave)
def break_through = Lost.new(@wave)
def can_build? = true
def to_s = "Wave #{@wave} in progress"
end
class Won
def start_wave = self
def clear_wave = self
def break_through = self
def can_build? = false
def to_s = "Victory!"
end
class Lost
def initialize(wave)
@wave = wave
end
def start_wave = self
def clear_wave = self
def break_through = self
def can_build? = false
def to_s = "Defeat on wave #{@wave}"
endIn use
game = Game.new
game.status # => "Building: wave 1 is next"
game.start_wave.status # => "Wave 1 in progress"
game.start_wave.status # => "Wave 1 in progress" (already running)
game.clear_wave.status # => "Building: wave 2 is next"
game.start_wave.break_through.status # => "Defeat on wave 2"
game.can_build? # => false
Notes
Ruby idioms
- States return the next state. Each transition is
@state = @state.start_wave, with nocase @statein sight. Adding aPausedstate means adding one class, not editing every branch. - States can carry data.
WaveInProgressremembers its wave number, soGamedoesn’t need a field that only matters in some states. - Endless methods (
def can_build? = true) keep each state’s answers to one line each, so a whole state reads like a row in a table. - For simple lifecycles, a gem such as AASM or
state_machinesgives a declarative table instead.
Reach for it when
- Methods start with the same guard clauses, such as
return if over?, andcase statestatements repeat across the code. - The transitions themselves are rules worth naming and testing.
Watch for
- Very simple two-state toggles don’t need it. A boolean is fine until the conditionals start multiplying.
Each state handles the requests that make sense in it.