Low-level designCore8 min
State
Let an object change its behavior when its state changes, by giving each state its own class. The object hands every request to its current state, and the states decide what happens and which state comes next.
A scooter with three states
Scooter stores a Status and switches on it in every event: unlock, pause, end and its display name.
1 · The situation
A scooter that is locked, riding or paused
An e-scooter rental app controls each scooter through three events: unlock(), pause() and end(). What an event does depends on the scooter's state: unlocking a locked scooter starts a ride, unlocking a paused one resumes it, and pausing only works while riding.
The first version keeps the state in an enum, Status, and each event method switches on it. A fourth method, state(), gives the state's display name. Python writes the enum the same way and uses match.
enum Status { LOCKED, RIDING, PAUSED }
class Scooter {
private Status status = Status.LOCKED;
String unlock() {
switch (status) {
case LOCKED:
status = Status.RIDING;
return "ride started";
case PAUSED:
status = Status.RIDING;
return "ride resumed";
default:
return "already riding";
}
}
String pause() {
switch (status) {
case RIDING:
status = Status.PAUSED;
return "paused at half rate";
default:
return "refused: not riding";
}
}
String end() {
switch (status) {
case LOCKED:
return "refused: no ride";
default:
status = Status.LOCKED;
return "ride ended, locked";
}
}
String state() {
switch (status) {
case LOCKED:
return "Locked";
case RIDING:
return "Riding";
default:
return "Paused";
}
}
}
public class Main {
static void report(String event, String result, Scooter s) {
System.out.println(event + ": " + result + " [" + s.state() + "]");
}
public static void main(String[] args) {
Scooter s = new Scooter();
report("pause", s.pause(), s);
report("unlock", s.unlock(), s);
report("pause", s.pause(), s);
report("unlock", s.unlock(), s);
report("end", s.end(), s);
}
}The class diagram of this version, drawn from its code: the scooter holds a status and interprets it in every method.
A rider tries to pause before unlocking, then rides, pauses, resumes and ends:
pause: refused: not riding [Locked] unlock: ride started [Riding] pause: paused at half rate [Paused] unlock: ride resumed [Riding] end: ride ended, locked [Locked]
2 · The change
Reservations add a state to every switch
The company adds reservations: a rider can hold a scooter for 10 minutes before unlocking it, or cancel the hold. That's a new state, Reserved, and a new event, reserve(). Look at where state-dependent behavior lives:
class Scooter {
private Status status = Status.LOCKED;
String unlock() {
switch (status) {
case LOCKED:
status = Status.RIDING;
return "ride started";
case PAUSED:
status = Status.RIDING;
return "ride resumed";
default:
return "already riding";
}
}
String pause() {
switch (status) {
case RIDING:
status = Status.PAUSED;
return "paused at half rate";
default:
return "refused: not riding";
}
}
String end() {
switch (status) {
case LOCKED:
return "refused: no ride";
default:
status = Status.LOCKED;
return "ride ended, locked";
}
}
String state() {
switch (status) {
case LOCKED:
return "Locked";
case RIDING:
return "Riding";
default:
return "Paused";
}
}
}Each switch is a column of a table, state by event, spread across four methods. A new state adds a case to every column; a new event adds a whole new column:
State |
|
|
|
|---|---|---|---|
Locked | start a ride | refuse | refuse |
Riding | already riding | pause | end, lock |
Paused | resume | refuse | end, lock |
Reserved (new) | start a ride | refuse | cancel the hold |
And reserve() is a new column: Locked holds the scooter, every other state refuses.
Everything about being Reserved, what each event does there and where it leads, would be scattered across five switches, mixed with every other state's rules. The practice checks flag the design: Status is switched on in four places.
3 · The refactor
Give each state its own class
Start from the first version. Turn each state into a class that answers every event for itself, and let the scooter hand events to its current state:
ScooterState declares the events, with a default refusal for the ones most states refuse. Locked, Riding and Paused each override what they accept, and move the scooter on by calling setState() with the next state.
abstract class ScooterState {
abstract String name();
abstract String unlock(Scooter s);
String pause(Scooter s) {
return "refused: not riding";
}
String end(Scooter s) {
return "refused: no ride";
}
}
class Riding extends ScooterState {
String name() {
return "Riding";
}
String unlock(Scooter s) {
return "already riding";
}
@Override
String pause(Scooter s) {
s.setState(new Paused());
return "paused at half rate";
}
@Override
String end(Scooter s) {
s.setState(new Locked());
return "ride ended, locked";
}
}After the second step the program prints what it printed before. After the third, reservations work, and are refused while riding:
reserve: reserved for 10 minutes [Reserved] end: reservation canceled [Locked] reserve: reserved for 10 minutes [Reserved] unlock: ride started [Riding] reserve: refused: not available [Riding] end: ride ended, locked [Locked]
What reservations cost. Before: a case in each of the four switches, and a new switch with four cases. After: the new Reserved class, plus a default in ScooterState and an override in Locked.
4 · The pattern
State: behavior that changes with the state
State lets an object, the context, change its behavior when its internal state changes, as if it had changed class. Each state is a class implementing one interface. The context forwards each request to its current state object, and the states perform the transitions, replacing the context's state with the next one.
The roles in the scooter's code: Scooter is the context, ScooterState the state interface, and each state a concrete state.
Role | In the scooter app | Its job |
|---|---|---|
Context |
| Holds the current state and forwards requests to it |
State |
| Declares what every state must answer, with defaults |
Concrete state |
| Behaves as that state, and chooses the next one |
The scooter's states and the events that move between them. Each arrow is one transition in one state class.
The first unlock goes to Locked, which moves the scooter to Riding; the next event goes to the new state.
5 · What it costs
The machine is spread over many classes
- A class per state. Four states are four classes and a base class, where there was one enum.
- The whole machine is hard to see at once. Each class shows its own transitions; to see every path through the states you read every class, or draw the diagram above.
- States know each other.
LockednamesRidingandReserved. A renamed or removed state reaches every state that leads to it. - A new event reaches the base class. States make new states cheap; a new event needs a method in the interface, with a default, and an override wherever it does something.
Situation | State, or something else? |
|---|---|
A few states, each with its own behavior for several events | State |
Many states and events, with simple actions | A transition table, as in State machines |
Two states and one event | A boolean and an |
The behavior is chosen from outside and never changes itself | Strategy |
6 · Getting it right
Defaults in the base, transitions in the states
- Put the common answer in the base class. Most states refuse most events. A default refusal in
ScooterStatekeeps each state class to what it actually accepts. - Keep data in the context. Ride time, the rider and the battery level belong to
Scooter; states read them through it. States without data can be shared, one object per state. - Change state through one method. All transitions call
setState(), which is the one place to log them, or check that they're allowed. - Don't switch on the state's type. A context that asks "is my state
Riding?" has brought the switch back.
Language | The usual shape |
|---|---|
Java | An abstract class or interface, one class per state; an |
C++ | An abstract class; stateless states can be shared constant objects, so a transition never deletes the state that is running |
JavaScript | A base class with default methods, one subclass per state |
Python | An abstract base class with default methods; the context imports nothing but the starting state |
7 · Look-alikes
Same structure, different driver
State has the same structure as Strategy: a context holding an object behind an interface. What differs is who changes that object, and why:
Pattern | Who changes the object | Do the implementations know each other? |
|---|---|---|
State | The states themselves, as events arrive | Yes: each state names the states it leads to |
Strategy | The client, from outside | No: strategies are interchangeable and independent |
Transition table | The context, by looking up the next state in a table | No: states are data, not classes |
Enum with behavior | The context, assigning a constant | Constants can name each other; all live in one type |
If you can't tell State from Strategy, look at what triggers the swap. A rider never chooses "Paused" as a way of doing something; pausing moves the scooter there. That self-driven change is what makes it State.
8 · In practice
The snack machine on platform 2
The vending-machine practice problem is a state machine. Kai's draft keeps an enum Status and switches on it in insertCoin(), select() and cancel(), which the checks report as "Status is switched on in 3 places". The reference design gives the machine a VendingState interface, with an idle state and a has-money state that decide what each coin, pick and cancel does, and which state comes next.
The checks recognize a State when a class holds an object behind an interface, forwards requests to it, and the implementations, or the class itself, replace that object as things happen. Fix the vending machine and let its states drive it.
Check yourself
4 questions
Takeaways
Remember this
- State gives each state of an object its own class, and the object delegates to its current state.
- States perform the transitions, replacing the context's state with the next one.
- A new state is a new class; switches over a status enum are gone.
- Defaults in the base class keep each state to what it accepts; data stays in the context.
- State and Strategy share a shape; in State the object changes itself as events arrive.