Skip to main content

State Pattern: Polymorphic Transitions

What This Concept Is​

The State pattern lets an object change its behavior when its internal state changes by delegating state-specific behavior to a separate object. The object "appears as if its class changed" when its state changes.

Three roles:

  • Context -- the object whose behavior varies. Holds a reference to the current state and exposes the operations callers invoke.
  • State interface -- declares the operations whose behavior depends on state.
  • Concrete states -- each implements behavior for one state. The context, state objects or an explicit transition policy may own transition decisions.

Strategy represents interchangeable policy; State represents lifecycle-dependent behavior. Who selects the next helper is an implementation choice, not a universal definition.

Why It Matters Here​

Lots of real objects have a lifecycle: a document has draft/review/published; an order has placed/paid/shipped/delivered/canceled; a network connection has disconnected/connecting/connected/closing. If that lifecycle is implicit in scattered if statements, every new state or new operation forces edits everywhere.

State pattern localizes "what can happen next and how" inside each state class. Adding a state localizes its behavior, but incoming transitions, construction and exhaustive consumers can still require edits.

Concrete Example​

A vending machine with four states; each decides transitions.

interface State {
void insertCoin();
void selectItem();
void dispense();
}

class VendingMachine {
private State current;
int inventory;
State noCoin, hasCoin, sold, soldOut;

VendingMachine(int items) {
inventory = items;
noCoin = new NoCoinState(this);
hasCoin = new HasCoinState(this);
sold = new SoldState(this);
soldOut = new SoldOutState(this);
current = inventory > 0 ? noCoin : soldOut;
}
void setState(State s) { current = s; }
void insertCoin() { current.insertCoin(); }
void selectItem() { current.selectItem(); }
void dispense() { current.dispense(); }
}

class NoCoinState implements State {
private final VendingMachine m;
NoCoinState(VendingMachine m) { this.m = m; }
public void insertCoin() { System.out.println("coin accepted"); m.setState(m.hasCoin); }
public void selectItem() { System.out.println("insert a coin first"); }
public void dispense() { System.out.println("insert a coin first"); }
}

class HasCoinState implements State {
private final VendingMachine m;
HasCoinState(VendingMachine m) { this.m = m; }
public void insertCoin() { System.out.println("coin already inserted"); }
public void selectItem() { m.setState(m.sold); m.dispense(); }
public void dispense() { System.out.println("select an item first"); }
}

class SoldState implements State {
private final VendingMachine m;
SoldState(VendingMachine m) { this.m = m; }
public void insertCoin() { System.out.println("please wait"); }
public void selectItem() { System.out.println("already dispensing"); }
public void dispense() {
m.inventory--;
m.setState(m.inventory > 0 ? m.noCoin : m.soldOut);
}
}

class SoldOutState implements State {
private final VendingMachine m;
SoldOutState(VendingMachine m) { this.m = m; }
public void insertCoin() { System.out.println("sold out"); }
public void selectItem() { System.out.println("sold out"); }
public void dispense() { System.out.println("sold out"); }
}

Each state knows exactly how to respond to every method and which state comes next. Zero switches in VendingMachine.

Common Confusion / Misconception​

  • State and Strategy look identical on paper. Both delegate behavior to a helper. The distinction is lifecycle behavior versus policy variation. State classes need not know each other when transitions are centralized.
  • State classes that hold mutable state of their own. Usually states are stateless (or cached) and push data into the context. A state with its own persistent fields becomes a memory surprise when you switch away and back.
  • Transition logic duplicated across states. If CanceledState and DeliveredState both do the same "refund if needed", you want a small helper or a base class, not five repeats.
  • Creating new state instances on every transition. In a hot loop, this allocates. Measure before caching; a state holding a context reference must not be shared across independent contexts.

How To Use It​

  1. Write down the state names and the transitions between them on paper.
  2. Declare a state interface that lists every operation whose behavior varies.
  3. Create one class per state. Each method decides: act, reject, and/or transition.
  4. On the context, hold currentState and expose each operation as a delegation.
  5. Expose setState to the state classes (package-private, internal, or via a friend class).
  6. Initialize to the correct starting state explicitly.

Test with a scenario table: "in state X, method Y should end up in state Z with side effect S". One row per cell.

Check Yourself​

  1. Which role knows about the concrete state classes: context, states, or both?
  2. What distinguishes State from Strategy?
  3. Why prefer states that do not hold their own mutable data?
  4. How do you add a new state without modifying all existing ones?

Mini Drill or Application​

Model a traffic light with Red, Yellow, Green states and two operations: tick() (time advance) and emergency() (force red).

  1. Implement each state; each decides the next.
  2. Handle emergency from any state.
  3. Write a test scenario that cycles the light three times.

Stop when the light class has no switch on state.

Read This Only If Stuck​

Contract and transfer​

The vending sketch assumes nonnegative initial inventory, a single caller, no externally modified fields and a successful physical dispense. Its direct inventory decrement does not model a motor jam or payment refund. Introducing a device boundary requires states for pending and uncertain outcomes; a method return alone cannot confirm the physical effect.

A state invariant links control state to data: soldOut implies inventory is zero; dispense must not reduce inventory below zero. Model guards and every illegal operation explicitly. A transition table and polymorphic states can implement the same machine; test traces rather than demanding a certain number of classes.

Transfer: two machines cache the same state object containing the first machine's reference. An operation on machine B changes machine A. Explain why per-context ownership fixes this and why globally shared immutable behavior can still be safe when context is an explicit parameter. P14 and L04 exercise state obligations.