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.

Watch it evolve1 / 10
The problem

A scooter with three states

Scooter stores a Status and switches on it in every event: unlock, pause, end and its display name.

80
Start
Stage 1
Stage 2
Final

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.

The first versionJava
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);
    }
}
«enum»StatusLOCKEDRIDINGPAUSEDScooter− status: Status~ unlock(): String~ pause(): String~ end(): String~ state(): String

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:

Every event switches on the statusJava
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

unlock()

pause()

end()

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.

Java
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";
    }
}
«create»«create»«create»«create»«create»1«abstract»ScooterState~ name(): String~ unlock(Scooter): String~ pause(Scooter): String~ end(Scooter): StringLocked~ name(): String~ unlock(Scooter): StringRiding~ name(): String~ unlock(Scooter): String~ pause(Scooter): String~ end(Scooter): StringPaused~ name(): String~ unlock(Scooter): String~ end(Scooter): StringScooter− state: ScooterState~ setState(ScooterState)~ unlock(): String~ pause(): String~ end(): String~ state(): String

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]
Switch cases to edit
8
Classes to edit
2

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.

«create»«create»«create»«create»«create»«create»«create»«create»1«abstract»ScooterState~ name(): String~ unlock(Scooter): String~ pause(Scooter): String~ end(Scooter): String~ reserve(Scooter): StringLocked~ name(): String~ unlock(Scooter): String~ reserve(Scooter): StringReserved~ name(): String~ unlock(Scooter): String~ end(Scooter): StringRiding~ name(): String~ unlock(Scooter): String~ pause(Scooter): String~ end(Scooter): StringPaused~ name(): String~ unlock(Scooter): String~ end(Scooter): StringScooter− state: ScooterState~ setState(ScooterState)~ unlock(): String~ pause(): String~ end(): String~ reserve(): String~ state(): String

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

Scooter

Holds the current state and forwards requests to it

State

ScooterState

Declares what every state must answer, with defaults

Concrete state

Locked, Reserved, Riding, Paused

Behaves as that state, and chooses the next one

LockedReservedRidingPausedreserveendunlockunlockpauseunlockendendLockedReservedRidingPausedreserveendunlockunlockpauseunlockendend

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. Locked names Riding and Reserved. 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 if

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 ScooterState keeps 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 enum whose constants override methods is a compact alternative for small machines

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

1. The company adds a "Low battery" state where only end() works. What changes?
2. Why does each state method take the scooter as a parameter?
3. Which is State rather than Strategy?
4. Where should the time a ride has lasted be stored?

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.