Add code examples, and documentation
This commit is contained in:
359
docs/state.md
Normal file
359
docs/state.md
Normal file
@@ -0,0 +1,359 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user