Home Design Patterns State
State
Behavioral Gang of Four“Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.”
Gamma, Helm, Johnson & Vlissides, p. 305Purpose
State replaces a mode variable and the conditionals that test it with one object per mode. The object holding the mode delegates its behavior to whichever state object it currently points at, and a transition is nothing more than pointing at a different one.
The reason to do this is not elegance. It is that a mode variable spreads. It
starts as one switch in one method and ends up as a
switch in one method plus fourteen scattered
if (mode == ...) guards, and no tool in the language can tell you
where they all are. Giving each mode a class turns "find every place that cares
about the mode" from a search problem into a compiler problem.
Design
The Context keeps a reference to a State and forwards its public operations to it. Each concrete state implements those operations for one mode, and decides which state comes next by handing the Context a replacement.
Where the transitions live is the one genuine choice the pattern presents. If each state names its successors, the states know about each other and the transition graph is scattered across the classes that implement it. If the Context decides instead, the graph is in one readable place and the Context has grown back the conditional you were trying to remove. The book puts the transitions in the states. That is usually right, but it is right by a narrow margin, and it is why a transition table written out in a comment is a common and sensible addition.
Example
The Autobot Robotics machine controller has modes: idle, homing, running,
holding, faulted and emergency stop. The original implementation was an
enumeration and a large switch inside the control loop, plus guards
in the jog handler, the program loader, the tool-change routine and the network
command dispatcher.
Adding "holding", which pauses a program without losing position, meant finding every one of those places. One was missed. The jog handler still accepted manual jog commands while the machine was holding, so an operator could nudge the arm off its held position during a pause and the resume would then run the rest of the program from coordinates that were no longer true. It took a while to find because the machine behaved perfectly unless somebody jogged during a hold.
Each mode is now a class implementing onEnter,
onExit, handle and tick. When the
force-controlled insert mode was added later, the compiler listed every operation
the new class had not implemented, which is exactly the list that had to be found
by hand before. Two constraints came with it, both from the real-time side. The
six state objects are created once at start-up and reused, because allocating in
the control loop is not acceptable, which means the state classes must hold no
data of their own. And the transition itself has to be atomic with respect to the
loop, so setMode is called only at a defined point in the cycle
rather than wherever a state feels like it.
interface ArmMode
{
void onEnter(ArmContext c);
void onExit(ArmContext c);
void handle(ArmContext c, Command cmd);
void tick(ArmContext c);
}
class ArmContext
{
private ArmMode mode = Modes.Idle;
// The whole transition mechanism. No conditional anywhere.
public void setMode(ArmMode next)
{
mode.onExit(this);
mode = next;
mode.onEnter(this);
}
public void handle(Command cmd) { mode.handle(this, cmd); }
public void tick() { mode.tick(this); }
}
class Running : ArmMode
{
public void onEnter(ArmContext c) { /* release the motion interlock */ }
public void onExit(ArmContext c) { /* decelerate to a stop */ }
public void tick(ArmContext c) { /* step the trajectory */ }
public void handle(ArmContext c, Command cmd)
{
if (cmd == Command.Hold) c.setMode(Modes.Holding);
if (cmd == Command.EStop) c.setMode(Modes.EStopped);
// anything else goes to the motion planner
}
}
class Holding : ArmMode
{
// A jog here is rejected, not queued. That rule now lives in one
// file instead of in a guard somebody can miss.
public void handle(ArmContext c, Command cmd) { }
}
setMode is the pattern. Everything else is a
consequence of it: because behavior is reached through
mode, changing the pointer changes the machine.Seen in the Wild
The Gang of Four's own worked example is a TCP connection, and it is a fair one: the state machine in RFC 793, with LISTEN, SYN_SENT, ESTABLISHED and the rest, is a genuine finite state machine that real stacks implement. Boost.Statechart and Boost.MSM are C++ libraries built explicitly on the idea that a state is a type, and the Meta State Machine library exists because the plain pattern does not handle nested states. In the embedded world, Miro Samek's QP frameworks are an entire real-time architecture organized around hierarchical state machines. On the enterprise side, Spring Statemachine is the same idea with the transition graph declared as configuration rather than written into the state classes.
Comparisons
- Strategy has the same class diagram, and the difference is worth stating precisely because so many descriptions of it are vague. Three things separate them. First, who chooses: a client picks a strategy, whereas a state picks its own successor. Second, how often: a strategy is usually set once and left, whereas a state changes many times during one object's life and the changes are the point. Third, whether the alternatives know each other: strategies are interchangeable and mutually ignorant, states are positions in a graph and must name their neighbors. If your "strategies" are swapping themselves out, you wrote a State.
- Flyweight is how state objects are normally shared. Once a state holds no instance data, one instance per mode serves every context in the process, which is what makes the pattern affordable in a system that cannot allocate.
- Singleton is the lazier version of that same sharing, and it works here better than almost anywhere else, because a stateless state object really is interchangeable.
- Command supplies the events. A state machine driven by command objects rather than by an enumeration gets logging and replay for free, which is how you debug a machine that misbehaves once a shift.
- Memento answers the question State does not: how to save where the machine had got to and put it back after a restart.
Why It Exists
My take
State exists because a mode variable is a dependency the type system cannot see. Every conditional that tests it is a place that has to be updated when a mode is added, and nothing links those places together. The compiler will not find them, a search for the enumeration name will only find the ones that spelled it the same way, and the tests will not find them because the missing case is by definition one nobody thought about. The class-per-state version converts that invisible obligation into an interface, and an unimplemented interface is a build error.
This is one of the few patterns I would say has a genuine home in the world I came from. A machine on a factory floor really is a state machine: it is idle, or it is homing, or it has faulted, and the whole safety argument for the cell depends on those being distinct and on the transitions between them being the only way to move. When the code says that in its structure rather than in a variable, the code and the safety case are talking about the same thing. When it says it with an integer and some conditionals, they are not, and the gap between them is where the incidents live.
The position I will defend is that the pattern is often the wrong size. For three modes and two events, a switch is better: shorter, visible in one screen, and obviously exhaustive. For anything with nested modes, history, or timed transitions, the pattern is not enough and you want a real statechart, which is why libraries like Boost.MSM exist. State is the right answer for the middle, and the middle is narrower than the enthusiasm for the pattern suggests. I would also say that a plain transition table, a two-dimensional array of state and event, beats both for a machine of any size, because it is data: you can print it, diff it, validate that every cell is filled, and hand it to the person writing the safety case.
Criticisms
The transition graph disappears. Before the refactor there was one ugly
switch you could read top to bottom and see the whole machine.
Afterwards the machine exists only as the sum of the setMode calls
scattered through six files, and there is no view in the source that shows it.
Every team that adopts this pattern eventually draws the diagram again, by hand,
in a comment or a wiki page, and that copy then rots. That is a real regression
in comprehensibility traded for a real gain in safety, and it should be treated
as a trade rather than a win.
State objects need to reach into the Context. They have to read sensor values, command motion, and set flags, so the Context ends up exposing a much wider surface to its states than it exposes to anyone else. Languages without a friendship mechanism give you a choice between making those members public and inventing an internal interface that only the states use, and both options are mildly unpleasant. Whatever you pick, the encapsulation the Context had before the refactor is weaker after it.
Finally, the class count is genuinely a cost when the states are trivial. Half the states in a typical machine reject almost everything and do almost nothing, so you get files whose entire content is a name and four empty methods. An abstract base with default rejections helps, but it also means a state that forgets to handle something silently inherits "ignore it", which is precisely the failure mode the pattern was adopted to prevent.