Files
Design-Patterns/docs/state.md
2026-08-13 22:47:17 -04:00

12 KiB

← Design patterns index

State

Category: Behavioral | Source: patterns/state/BadCarState/ + patterns/state/CarState/

Problem

Behavior depends on an internal state field, so every state-dependent method switches on it. As states and behaviors multiply, each method grows a branch per state, and changing a transition rule means finding and editing that conditional in every method that has one.

Solution

Make one class per state, all sharing a common State base class. The context holds a reference to its current state object and delegates state-dependent calls to it instead of branching. Each state class implements only what makes sense in that state, and states drive transitions by handing the context a new state object. The object "appears to change its class" because you're swapping what it delegates to.

Participants (GoF names → this repo)

  • Context → Car — holds the current State, delegates, exposes ChangeState.
  • State → State base class — declares the state-specific operations.
  • ConcreteState → Off, Stopped, Driving.
stateDiagram-v2
    [*] --> Off
    Off --> Stopped : turn-clockwise / engine has started
    Stopped --> Off : turn-counter / engine has stopped
    Stopped --> Driving : gas / car begins to drive
    Driving --> Stopped : brake / car slows to a stop
    Driving --> Off : turn-counter / engine off, car stops suddenly
    Stopped --> Stopped : turn-clockwise / grinding noise
    Driving --> Driving : turn-clockwise / grinding noise
classDiagram
    class Car {
        -State _state
        +HandleInput(string) bool
        +ChangeState(State)
    }
    class State {
        #Car _car
        +TurnKeyClockwise() virtual
        +TurnKeyCounterClockwise() virtual
        +PressGas() virtual
        +PressBrake() virtual
    }
    Car o--> State : delegates
    State <|-- Off
    State <|-- Stopped
    State <|-- Driving
    Off ..> Car : calls ChangeState
    Stopped ..> Car : calls ChangeState
    Driving ..> Car : calls ChangeState

Before — BadCarState: a switch inside a switch

public class Car
{
    private enum State { Off, Stopped, Driving }
    private State _state = State.Off;

    public bool HandleInput(string input)
    {
        switch (input)
        {
            case "turn-clockwise":
                switch (_state)                        // <-- nested state switch #1
                {
                    case State.Off:
                        Console.WriteLine("The engine has started");
                        _state = State.Stopped;
                        break;
                    case State.Stopped:
                    case State.Driving:
                        Console.WriteLine("You hear a grinding noise...");
                        break;
                }
                break;
            case "turn-counter":
                switch (_state)                        // <-- nested state switch #2
                {
                    case State.Stopped:
                        Console.WriteLine("The engine has stopped");
                        _state = State.Off;
                        break;
                    case State.Driving:
                        Console.WriteLine("The engine turns off, and the car stops suddenly");
                        _state = State.Off;
                        break;
                }
                break;
            case "gas":
                switch (_state)                        // <-- nested state switch #3
                {
                    case State.Stopped:
                        Console.WriteLine("The car begins to drive");
                        _state = State.Driving;
                        break;
                }
                break;
            case "brake":
                switch (_state)                        // <-- nested state switch #4
                {
                    case State.Driving:
                        Console.WriteLine("The car slows to a stop");
                        _state = State.Stopped;
                        break;
                }
                break;
            case "exit":
                return false;
        }
        return true;
    }
}

Adding a Reversing state means auditing all four nested switches. And notice there is no case State.Off under "gas" — pressing the gas while off silently does nothing, and nothing in the code says whether that was intentional or an oversight.

After — CarState: one class per state

// States/State.cs — base State. Virtual empty bodies = "this input is a no-op here."
public class State
{
    protected Car _car;                    // backreference, so states can transition

    public State(Car car) { _car = car; }

    public virtual void TurnKeyClockwise() { }
    public virtual void TurnKeyCounterClockwise() { }
    public virtual void PressGas() { }
    public virtual void PressBrake() { }
}

// States/Off.cs — overrides exactly one operation; the rest are inherited no-ops
public class Off : State
{
    public Off(Car car) : base(car) { }

    public override void TurnKeyClockwise()
    {
        Console.WriteLine("The engine has started");
        _car.ChangeState(new Stopped(_car));    // the state drives its own transition
    }
}

// States/Stopped.cs
public class Stopped : State
{
    public Stopped(Car car) : base(car) { }

    public override void PressGas()
    {
        Console.WriteLine("The car begins to drive");
        _car.ChangeState(new Driving(_car));
    }

    public override void TurnKeyClockwise()
    {
        Console.WriteLine("You hear a grinding noise...");   // valid input, no transition
    }

    public override void TurnKeyCounterClockwise()
    {
        Console.WriteLine("The engine has stopped");
        _car.ChangeState(new Off(_car));
    }
}

// States/Driving.cs
public class Driving : State
{
    public Driving(Car car) : base(car) { }

    public override void TurnKeyClockwise()
    {
        Console.WriteLine("You hear a grinding noise...");
    }

    public override void TurnKeyCounterClockwise()
    {
        Console.WriteLine("The engine turns off, and the car stops suddenly");
        _car.ChangeState(new Off(_car));
    }

    public override void PressBrake()
    {
        Console.WriteLine("The car slows to a stop");
        _car.ChangeState(new Stopped(_car));
    }
}
// Car.cs — the Context. All state branching is gone.
public class Car
{
    private State _state;

    public Car()
    {
        ChangeState(new Off(this));        // initial state
    }

    public bool HandleInput(string input)
    {
        switch (input)                     // switches on INPUT only, never on state
        {
            case "turn-clockwise": _state.TurnKeyClockwise();        break;
            case "turn-counter":   _state.TurnKeyCounterClockwise(); break;
            case "gas":            _state.PressGas();                break;
            case "brake":          _state.PressBrake();              break;
            case "exit":           return false;
        }
        return true;
    }

    public void ChangeState(State state) { _state = state; }
}

What actually improved:

  • Car went from four nested state-switches to four one-line delegations. It no longer knows the state names at all beyond the initial Off.
  • Adding Reversing = adding one file. No existing method changes.
  • Each state's complete behavior is readable in one place, instead of scattered across four switch arms.
  • "Gas while off does nothing" is now an explicit, deliberate design statement: Off doesn't override PressGas, so the base class's empty body runs.
  • Trade-off: the transition graph is now distributed across the state classes rather than visible in one switch. That's why the state diagram shown earlier is worth keeping next to the code.

Output (verified — both projects, same scripted input)

Input: gas, turn-clockwise, gas, turn-clockwise, brake, turn-counter, exit

Welcome to the Driving Simulater

Usage:
    turn-clockwise  -Turns the car's key clockwise
    turn-counter    -Turns the car's key counter clockwise
    gas             -Presses the gas pedal
    brake           -Presses the brake pedal
    exit            -Exits the simulator

>                                    <- "gas" while Off: silently ignored
>
The engine has started
>
The car begins to drive
>
You hear a grinding noise...
>
The car slows to a stop
>
The engine has stopped
>

goodbye

BadCarState and CarState produce byte-identical output. That's the headline: the refactor changed structure and nothing else. The first blank prompt is the silent no-op discussed earlier, present in both versions.

GoF pseudocode (audio player)

class AudioPlayer is                       // Context
  field state: State
  constructor AudioPlayer() is this.state = new ReadyState(this)
  method changeState(state: State) is this.state = state
  method clickPlay() is state.clickPlay()  // pure delegation
  method startPlayback() is // service method a state can call back into

abstract class State is
  protected field player: AudioPlayer
  constructor State(player) is this.player = player
  abstract method clickLock()
  abstract method clickPlay()

class ReadyState extends State is
  method clickPlay() is
     player.startPlayback()
     player.changeState(new PlayingState(player))

class PlayingState extends State is
  method clickPlay() is
     player.stopPlayback()
     player.changeState(new ReadyState(player))

class LockedState extends State is
  method clickPlay() is // Locked, so do nothing.

GoF makes State abstract with abstract methods, forcing every state to answer every input explicitly. The repo instead uses virtual empty bodies, so "do nothing" is the default. Abstract = the compiler catches states you forgot to handle; virtual = less boilerplate. Pick deliberately.

Applicability

  • An object's behavior depends on its state and must change at run-time accordingly, the number of states is large, and state-specific code changes often.
  • Operations have large multipart conditionals branching on the object's state (usually enum values), and several operations repeat that same conditional structure.
  • Lots of duplicate code across similar states — factor it into intermediate abstract state classes.

Pros / Cons

Pros Cons
Single Responsibility — each state's code in its own class Overkill for a machine with few states that rarely change
Open/Closed — add states without touching existing ones More classes; the transition graph is no longer visible in one place
Kills the bulky conditionals in the context Allocates a new state object on every transition (usually irrelevant; states can be cached/shared if stateless)
Makes transitions explicit instead of stray field assignments

Seen in the wild (.NET)

  • TcpConnection-style protocol handling — GoF's own example.
  • Workflow/order-status engines.
  • Game character controllers.
  • HttpClient's connection lifecycle.
  • Bridge / State / Strategy share the composition-delegation structure and differ in intent.
  • State vs. Strategy — State is arguably Strategy where the delegates know about each other and actively swap themselves in (_car.ChangeState(new Stopped(_car))). Strategy objects are independent and mutually unaware, and are chosen by the client rather than by each other.

← Prev: Observer · All patterns