12 KiB
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 currentState, delegates, exposesChangeState. - State →
Statebase 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:
Carwent from four nested state-switches to four one-line delegations. It no longer knows the state names at all beyond the initialOff.- 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:
Offdoesn't overridePressGas, 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.
Related patterns
- 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