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

RoleIn this exampleResponsibility
ContextGameHas 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 StateBuilding, WaveInProgress, Won, LostDecides what each request means in that state, and answers with the next state.

Ruby implementation

state.rb
# 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}"
end

In 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 no case @state in sight. Adding a Paused state means adding one class, not editing every branch.
  • States can carry data. WaveInProgress remembers its wave number, so Game doesn’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_machines gives a declarative table instead.

Reach for it when

  • Methods start with the same guard clauses, such as return if over?, and case state statements 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.