Skip to main content

State

complexitypopularity

Let an object change its behavior when its internal state changes, so completely that it appears to have changed its class.

The problem

State is the object-oriented face of the finite-state machine: at any moment the program sits in one of a limited set of states, behaves differently in each, and moves between them along a fixed set of transitions.

Take a Document class with states Draft, Moderation, and Published. Its publish() method sends a draft to moderation, publishes a moderated document if the current user is an administrator, and does nothing at all once published. The obvious implementation is a switch on a status field, and the obvious implementation of the next method is the same switch again.

Now add a state. You are editing every method in the class, and each edit risks the others. Nobody predicts the full state graph up front, so the lean little conditional you wrote in month one grows into the thing you dread opening in month twelve.

The solution

Give every state its own class and move the state-specific behavior into it. The original object, now called the context, keeps one reference to a state object and delegates all state-dependent work to it. Its own methods shrink to a single forwarding line each.

Switching states means swapping that reference. This works only because every state class implements the same interface, so the context never needs to know which one it is holding. And because states usually carry a backreference to the context, they can perform the swap themselves: ReadyState.clickPlay() starts playback and installs a PlayingState.

That last detail is what separates State from Strategy. Strategies are interchangeable strangers picked by a client. States are a cast of characters who know the plot and hand off to each other.

User interfaceAudioPlayer (context)ReadyStatePlayingStateLockedStateclickPlay()1state.clickPlay()2startPlayback()3changeState(new PlayingState())4clickNext()5state.clickNext()6clickLock()7state.clickPlay()8
  1. 1A button is pressed. The player has no idea what should happen, and that is by design.
  2. 2The context forwards the call to whatever state object it currently holds. No switch, no if, just delegation.
  3. 3ReadyState calls back into the context for the actual service work. States decide what happens; the context still owns the machinery.
  4. 4Then the state performs its own transition. This is the move Strategy never makes.
  5. 5The same button as before, pressed again.
  6. 6Different object answers now, so next means fast-forward five seconds instead of skipping a track. The player did not change one line of code.
  7. 7The user pockets the phone and locks it.
  8. 8Every subsequent button press lands in LockedState, whose methods politely do nothing at all.

Structure

The state interface should declare only the methods that genuinely vary by state. Padding it with methods that most states must implement as no-ops is a sign the seam is in the wrong place. When several states share behavior, put an intermediate abstract class between them rather than copy-pasting.

«abstract»State# player: AudioPlayerclickLock()clickPlay()clickNext()clickPrevious()STATEAudioPlayer- state: StatechangeState(state)clickLock()clickPlay()startPlayback()stopPlayback()CONTEXTReadyStateclickLock()clickPlay()clickNext()clickPrevious()CONCRETEPlayingStateclickLock()clickPlay()clickNext()clickPrevious()CONCRETELockedStateclickLock()clickPlay()clickNext()clickPrevious()CONCRETE
extendsuses

Code

Same example three ways: an audio player whose four buttons mean different things depending on whether it is locked, ready, or playing.

// One field, one switch per method, and a headache that compounds.
class AudioPlayer is
field state: string = "ready"
field playing: boolean = false
 
method clickPlay() is
switch (state)
"locked":
// Do nothing.
break
"ready":
startPlayback()
state = "playing"
break
"playing":
stopPlayback()
state = "ready"
break
 
method clickNext() is
switch (state)
"locked":
// Do nothing.
break
"ready":
nextSong()
break
"playing":
if (event.doubleclick) nextSong() else fastForward(5)
break
 
// ...and clickLock(), and clickPrevious(), and every future method,
// each one repeating the same list of states. Add "buffering" and you
// are editing every single one of them.
// The context forwards and forgets. Not one conditional left.
class AudioPlayer is
field state: State
 
method clickPlay() is
state.clickPlay()
 
method changeState(state: State) is
this.state = state
 
// Each state owns its slice of behavior and its own exits:
class ReadyState extends State is
method clickPlay() is
player.startPlayback()
player.changeState(new PlayingState(player))
 
// Adding a "buffering" state is one new class and zero edits
// to AudioPlayer. That is the Open/Closed Principle, cashed in.
// The context holds a reference to the current state and delegates every
// state-dependent action to it.
class AudioPlayer is
field state: State
field UI, volume, playlist, currentSong
 
constructor AudioPlayer() is
this.state = new ReadyState(this)
 
UI = new UserInterface()
UI.lockButton.onClick(this.clickLock)
UI.playButton.onClick(this.clickPlay)
UI.nextButton.onClick(this.clickNext)
UI.prevButton.onClick(this.clickPrevious)
 
// States and clients alike use this to move the player forward.
method changeState(state: State) is
this.state = state
 
// Every UI handler is now a one-liner. No conditionals survive here.
method clickLock() is
state.clickLock()
method clickPlay() is
state.clickPlay()
method clickNext() is
state.clickNext()
method clickPrevious() is
state.clickPrevious()
 
// The context keeps the plumbing; states just decide when to pull it.
method startPlayback() is
// ...
method stopPlayback() is
// ...
method nextSong() is
// ...
method previousSong() is
// ...
method fastForward(time) is
// ...
method rewind(time) is
// ...
 
// The base state declares every state-dependent action and carries the
// backreference states use to read context data and trigger transitions.
abstract class State is
protected field player: AudioPlayer
 
constructor State(player) is
this.player = player
 
abstract method clickLock()
abstract method clickPlay()
abstract method clickNext()
abstract method clickPrevious()
 
// Doing nothing is a behavior, and it gets its own class.
class LockedState extends State is
// Unlocking lands you in one of two states, and this state decides which.
method clickLock() is
if (player.playing)
player.changeState(new PlayingState(player))
else
player.changeState(new ReadyState(player))
 
method clickPlay() is
// Locked, so nothing happens.
 
method clickNext() is
// Locked, so nothing happens.
 
method clickPrevious() is
// Locked, so nothing happens.
 
class ReadyState extends State is
method clickLock() is
player.changeState(new LockedState(player))
 
method clickPlay() is
player.startPlayback()
player.changeState(new PlayingState(player))
 
method clickNext() is
player.nextSong()
 
method clickPrevious() is
player.previousSong()
 
// Same four buttons, wholly different meanings.
class PlayingState extends State is
method clickLock() is
player.changeState(new LockedState(player))
 
method clickPlay() is
player.stopPlayback()
player.changeState(new ReadyState(player))
 
method clickNext() is
if (event.doubleclick)
player.nextSong()
else
player.fastForward(5)
 
method clickPrevious() is
if (event.doubleclick)
player.previousSong()
else
player.rewind(5)

When to use it

  • An object behaves differently depending on its current state, there are many states, and the state-specific code changes often.
  • A class is drowning in conditionals that switch on the values of its own fields. Extracting each branch into a state class also lets you evict the temporary fields and helpers that only mattered to one branch.
  • A conditional state machine has grown duplicate code across similar states, which an abstract base state can absorb.

Pitfalls

  • Overkill on small machines. Three states that never change do not justify an interface and three classes. Wait for the churn.
  • Transition logic scattered everywhere. If states, the context, and the client all install new states, nobody can read the state graph. Pick one owner per transition and be consistent.
  • Coupling to concrete states. Whoever writes new PlayingState(...) depends on that class. That is often fine, but if it spreads, funnel creation through the context or a small factory.
  • Fat state interfaces. Every state paying the tax of a method only one state uses means the interface is describing the union of behaviors rather than the shared contract.

Don't confuse it with

  • Strategy. Structurally near-identical: a context delegating to a swappable helper. The intent differs. Strategy offers interchangeable algorithms for the same task, chosen by the client, mutually ignorant. State models a lifecycle whose members know each other and drive the transitions. State is fairly described as Strategy with the restriction lifted.
  • Template Method. Template Method varies steps of a fixed algorithm through subclassing, decided at compile time and frozen per class. State varies whole behavior sets through composition, decided and re-decided at runtime.
  • Bridge. Same composition skeleton again. Bridge exists so an abstraction and its implementation can evolve on separate axes; State exists so an object can act like a different class from one minute to the next.

Check yourself

Question 1 of 5

What is the single sharpest difference between State and Strategy?