Design Patterns · Behavioral Patterns
State
Put lifecycle-specific behavior in state objects. Let the context delegate and make valid transitions part of the model.
This lesson follows Observer. Compare its delegation with Strategy after reading the lifecycle example.
State makes an object's behavior depend on its current state object. The context delegates operations to that object and changes the reference when a transition occurs. The cost is more classes and a transition model that must stay consistent.
Name the states and transitions first
A document starts as a draft. Submit sends it to review. Publish is allowed from review. Repeated publish is a no-op after publication. A switch on status inside every method repeats these lifecycle rules across the class.
interface DocumentState {
submit(context: DocumentContext): void;
publish(context: DocumentContext): void;
}
class DocumentContext {
private state: DocumentState = new Draft();
transitionTo(state: DocumentState) { this.state = state; }
submit() { this.state.submit(this); }
publish() { this.state.publish(this); }
}
class Draft implements DocumentState {
submit(context: DocumentContext) { context.transitionTo(new Review()); }
publish() { throw new Error("Review required"); }
}
class Review implements DocumentState {
submit() { /* already submitted */ }
publish(context: DocumentContext) {
context.transitionTo(new Published());
}
}
class Published implements DocumentState {
submit() { throw new Error("Already published"); }
publish() { /* idempotent no-op */ }
}
const document = new DocumentContext();
document.submit(); // Draft → Review
document.publish(); // Review → Published
DocumentContext is the context. DocumentState is the contract. Draft, Review, and Published are concrete states. Here the states trigger transitions through the context. The context can also own transition decisions. State does not require every state to know every other state.
Run the same action in different states
Try it
Choose a starting state and an action
Draft.submit transitions to Review
resulting state: Review
Publish rejects: review required
resulting state: Draft
Already submitted: no transition
resulting state: Review
Review.publish transitions to Published
resulting state: Published
Submit rejects: already published
resulting state: Published
Publish is a no-op in this example
resulting state: Published
Each choice evaluates one action from the selected starting state. It does not retain a separate running document.
Step through
Follow a document through its lifecycle
Draft rejects early publication
The context delegates publish(). Draft rejects it because review is required.
Submission changes the helper
Draft.submit() replaces the current state with Review. The next publish() reaches a different implementation.
Publication changes later behavior
Review.publish() transitions to Published. Later submit() rejects, while later publish() is a no-op by the chosen contract.
Protect the transition boundary
A state object does not enforce every lifecycle rule
The sample exposes transitionTo for state cooperation. Real code must control who can call it so outside callers cannot skip review. Permission checks, persistence, and concurrent updates also need their own boundary. The pattern organizes behavior. It does not make a transition atomic.
Check yourself
Two requests both try to publish a document. Does using state classes serialize the writes?
Not quite. Different requests can act on the same old state unless persistence enforces the transition.
Correct. State organizes behavior. Atomic transition enforcement belongs at the shared write boundary.
Map the roles
State objects may keep a back reference to the context, which lets them read context data and trigger transitions. Both the context and the concrete states can set the context's next state.
| Role | In this example | Job |
|---|---|---|
| Context | DocumentContext | Holds a reference to one concrete state object and delegates all state-specific work to it. Talks to states through the state interface and exposes a way to set a new state. |
| State | DocumentState | Declares the state-specific methods. They should make sense for every concrete state, so no state carries methods it never uses. |
| Concrete states | Draft, Review, Published | Implement the state-specific behavior. An intermediate abstract class can hold behavior that several states share. |
Check yourself
Who may switch DocumentContext to a new state in this pattern?
Not quite. If only an outside client chose, the design would look like Strategy.
Correct. Here the states trigger transitions through transitionTo. The context could own those decisions instead.
Reach for it when behavior depends on a growing lifecycle
- an object behaves differently depending on its current state, there are many states, and the state-specific code changes often. Moving each state's code into its own class lets you add states or change one without touching the others.
- a class is polluted with large conditionals that switch behavior on the current values of its fields. State turns each branch into a method on a state class, and removes temporary fields and helper methods tied to one state.
- condition-based state machines repeat a lot of code across similar states and transitions. A hierarchy of state classes lets you pull shared code into an abstract base class.
Check yourself
A Subscription class has if (status === ...) checks in eleven methods, and product keeps adding statuses. Which State use applies?
Correct. Each status becomes a class, and each branch becomes a method on it.
Not quite. Small, stable machines can stay as a switch. Eleven methods and growing is the pressure State relieves.
Implement it in seven steps
Refactor existing code toward the pattern in this order:
- Decide which class acts as the context. It can be an existing class with state-dependent code, or a new class if state-specific code is spread across several classes.
- Declare the state interface. It could mirror every method of the context, but aim for only the methods with state-specific behavior.
- Create a class for every actual state, implementing the state interface. Move the state-specific code from the context into these classes.
- If the moved code depends on private members of the context, choose a workaround: make those members public, turn the needed context behavior into public methods that states call, or nest the state classes inside the context if your language supports it.
- Add a field of the state interface type to the context, plus a public setter to change it.
- Go through the context's methods again and replace the now-empty state conditionals with calls to the state object's methods.
- To switch states, create an instance of a state class and pass it to the context. This can happen in the context, in the states, or in the client. Whichever class does it becomes dependent on the concrete state class it creates.
Check yourself
After step 7, Review contains new Published(). What dependency did that add?
Not quite. Constructing a class always depends on that class, whatever interface it implements.
Correct. Every place that creates a state depends on that class. That is the price of states triggering their own transitions.
Name what it costs
| You gain | You pay |
|---|---|
| Code for each state lives in its own class (single responsibility) | Overkill when the machine has only a few states or rarely changes. An enum and a switch may be enough. |
| New states without changing existing state classes or the context (open/closed) | Transitions still couple states to each other or to a coordinator |
| The context loses its bulky state-machine conditionals | Adding a reachable state can still change existing transition rules |
Check yourself
A traffic light has three states and has not changed in five years. Should you convert its switch into state classes?
Not quite. Applying it everywhere adds classes without solving a real problem.
Correct. The pattern pays off when states are many or the code changes often.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Strategy | State can be seen as an extension of Strategy. Both use composition to change the context's behavior by delegating to helper objects. Strategy makes those objects completely independent and unaware of each other. State lets concrete states know about each other and trigger transitions. |
| Bridge, Adapter | Bridge, State, Strategy, and to some degree Adapter share a delegation-based structure. Each solves a different problem. |
Check yourself
The context's helper objects never reference each other, and only the client ever swaps them. State or Strategy?
Correct. Independent helpers chosen from outside point to Strategy. States usually know their successors.
Not quite. Without transitions between the helpers, there is no lifecycle to model.
Retrieve and apply
Check yourself
Adding Archived requires Published to allow archive(). Does State guarantee that no existing state ever changes?
Correct. State separates responsibilities but does not eliminate relationships between states. Adding a new reachable state can change the lifecycle graph.
Not quite. The context and transition owners still need to know how the new state is entered.
Draw an order lifecycle and define cancel() in each state. Continue to Strategy to compare lifecycle transitions with independent algorithm selection.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. State, printed pages 329–344. Explanations, examples, and exercises are adapted for this course.