Add code examples, and documentation

This commit is contained in:
Aaron Po
2026-08-12 00:03:32 -04:00
commit 77c16e69f6
79 changed files with 4458 additions and 0 deletions

359
docs/state.md Normal file
View 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)