360 lines
12 KiB
Markdown
360 lines
12 KiB
Markdown
[← Design patterns index](README.md)
|
|
|
|
# State
|
|
|
|
**Category:** Behavioral | **Source:** `patterns/state/BadCarState/` +
|
|
`patterns/state/CarState/`
|
|
|
|
## Problem
|
|
|
|
Behavior depends on an internal state field, so every state-dependent method
|
|
`switch`es 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`.
|
|
|
|
```mermaid
|
|
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
|
|
```
|
|
|
|
```mermaid
|
|
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`
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// 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));
|
|
}
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
## 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](observer.md) · [All patterns](README.md)
|