books / hfdp / ch-09SHEET 9 / 12 · REV ASIGN IN
MODULE 9 · HEAD FIRST DESIGN PATTERNS CH 10

The Gumball State Machine (State)

Turn a four-way if/else pyramid into state objects that own their own transitions — then add a fifth state without reopening a single old one.

The idea

A gumball machine has four situations — no quarter, has quarter, dispensing, sold out — and four things that can happen in each: insert coin, eject coin, turn crank, dispense. The obvious code is an int state field and a conditional inside every method. Sixteen combinations, hand-checked. It fits in your head, right up until the boss wants a 1-in-10 free gumball promotion and you are editing every method at once.

Look at what actually varies: behavior by state. turnCrank does a different thing depending on the situation the machine is in. So pull each situation into its own class — NoQuarterState, HasQuarterState, SoldState, SoldOutState — each implementing the same four actions and answering only for itself. The grid did not disappear; it got sliced into four small classes of four honest methods instead of four methods hiding four branches apiece.

The machine keeps one field, currentState, and every action becomes one line of delegation. Structurally that is Strategy redrawn, and the difference is intent: here the state objects change which state is current. HasQuarterState.turnCrank flips the machine to SoldState itself. Each state owns exactly its outgoing arrows, so a new state only touches the states with arrows into it.

That is why the winner state is nearly free: one new file plus one edited decision, where the conditional version meant surgery everywhere. The other payoff is honesty — when transitions are explicit assignments, your code has the same shape as the diagram on the whiteboard, so you can fuzz thousands of random event sequences against a transition table and prove they agree. Conditional pyramids fail that interrogation constantly.

The bench — 4 exercises

EX 01

Feel the Pyramid

Build the gumball machine the way everyone builds it first, then measure exactly what it costs you when a fifth state shows up.

  1. Write GumballMachine with an int state field and a four-way conditional inside insertQuarter, ejectQuarter, turnCrank and dispense.
  2. Write a small random-event driver that fires thousands of seeded coin/eject/crank sequences and checks gumballs out never exceeds quarters in.
  3. Let it find the interleaving that steals a gumball — an eject during dispense, or a double crank — and record the minimized sequence in NOTES.md.
  4. On a throwaway branch, add the 1-in-10 winner state the naive way and count how many methods and branches you had to edit; write that number down as your blast radius.
hint

Do not hunt the theft by reading the code; at four states and four events your eyes have already lost. Let the driver find it.

hint

Keep the branch throwaway — you want the broken version preserved for comparison, not fixed.

DONE WHEN

· NOTES.md records a concrete event sequence that yields two gumballs for one quarter

· NOTES.md records a blast-radius number of at least four touched methods

· The naive winner branch is discarded, not merged

EX 02

One Class per Situation

Refactor to a State interface with one class per situation, each performing its own transitions, and keep the observable transcript identical.

  1. Define a State interface with insertQuarter, ejectQuarter, turnCrank and dispense, and implement NoQuarterState, HasQuarterState, SoldState and SoldOutState.
  2. Give each state honest wrong-button answers too: ejecting with no quarter, cranking twice, inserting a coin when sold out.
  3. Reduce GumballMachine to the state field, four one-line delegating methods, setState, releaseBall, the inventory count and a getter per state.
  4. Print the full 4x4 state-by-event response matrix and compare it to the transcripts you captured before the refactor.
hint

SoldState.dispense releases the ball first, then checks the count and picks its own successor — NoQuarter or SoldOut. Transitions are decisions and belong to whoever has the information.

hint

If setState is being called from GumballMachine, a transition has escaped its state.

DONE WHEN

· grep for state ==, switch(state) or int state constants in GumballMachine returns nothing

· Pre-refactor transcripts replay byte-identical, and exercise 01's theft sequence now yields exactly one gumball

· Every setState call lives inside a state class

· No method in the machine or any state class has more than a couple of branches

EX 03

The Ten-Percent Jackpot

Add a winner state that pays two gumballs, changing exactly one existing file — and prove it by freezing the others.

  1. Record a checksum of every file you certified in exercise 02 so you can prove afterwards what changed.
  2. Make the machine take an injected random source in its constructor so the odds are deterministic under a fixed seed.
  3. Add WinnerState as a new file: dispense two gumballs, handle the cruel case of winning with one left, then transition on the real remaining count.
  4. Amend only HasQuarterState.turnCrank to roll 1-in-10 and route to WinnerState or SoldState.
  5. Recount your blast radius and put it in NOTES.md next to exercise 01's number.
hint

If the machine file changed, you taught it about WinnerState beyond the plain state getter the others already have — copy how SoldState is exposed, nothing more.

hint

Randomness is a dependency you inject, not a thing you sprinkle; a seeded source makes 1-in-10 a testable schedule.

DONE WHEN

· Under a fixed seed, winners land on the expected cranks and pay exactly two gumballs with correct inventory

· Winning with one gumball left pays one and lands in SoldOut without going negative

· Checksums show only WinnerState (new) and HasQuarterState differ

· The new blast-radius number is at most two files

EX 04

Fuzzed Against the Blueprint

Write the machine's transition table as a data file, add refill, then fuzz your implementation against the table until code and diagram agree.

  1. Write blueprint.tsv: one row per (state, event) giving next state and effect, covering all five states plus refill and the winner odds.
  2. Add refill(n) to every state — legal everywhere, and from SoldOut it returns to NoQuarter — then re-snapshot your checksums.
  3. Write a fuzzer that parses the blueprint and drives thousands of seeded random event sequences, checking reported state and inventory after every single event.
  4. Minimize and print any diverging sequence, then fix your code — never the blueprint — until a full run is clean.
hint

Caught on refill mid-dispense? A state machine has no off-duty states; Sold and Winner feel momentary but the table still has a cell for them.

hint

Print the seed on every run so a failure is reproducible tomorrow.

hint

Check jackpot frequency across the whole corpus against the odds in the table, with a tolerance — not per run.

DONE WHEN

· A full fuzz run reports zero illegal transitions and zero inventory drift

· Refilling a sold-out machine resumes vending

· Jackpot frequency over the corpus is within tolerance of the table's declared odds

· No state conditionals crept back into GumballMachine while fixing fuzz failures

Go deeper (after the bench)

Read Head First Design Patterns Ch. 10 (pp. 385-424) after exercise 01 — the "now Mighty Gumball wants a promotion" reveal is much funnier once you have personally paid the blast radius on the conditional version. Then read Game Programming Patterns' free State chapter (gameprogrammingpatterns.com/state.html) for what the book leaves out: hierarchical states, pushdown automata, and when a plain enum plus switch is honestly the right call.