Replace State-Altering Conditionals with State

Destination
Compare
Symptom
Human

The class carries a state field and every operation branches on it. Transitions are scattered across operations; legal state combinations are documented only in the conditional bodies; adding a state requires editing every operation; cross-operation invariants (e.g., 'only draft is editable') live as duplicated comparisons.

Agent

Per-operation state branches the agent must verify cover every state × operation combination consistently. The state machine's transition graph is invisible from any single method body; the agent has to read all operations to reconstruct legal transitions.

Goal
Human

Each state is a class implementing the operation surface; the host delegates and transitions are state-to-state assignments. Adding a state is one new class; per-operation behaviour is locally visible inside each state.

Agent

Per-state class the agent reads as the full operation surface for that state. The transition graph is recoverable by inspecting state-assignment statements; per-state tests pin per-state behaviour.

Before the refactoring

// Every operation branches on the same state string; transitions are scattered.
class Document {
constructor() {
this.state = 'draft';
this.content = '';
}
edit(newContent) {
if (this.state === 'draft') {
this.content = newContent;
} else if (this.state === 'in-review') {
throw new Error('cannot edit while in review');
} else if (this.state === 'published') {
throw new Error('cannot edit a published document');
}
}
submit() {
if (this.state === 'draft') {
this.state = 'in-review';
} else if (this.state === 'in-review') {
throw new Error('already in review');
} else {
throw new Error('cannot submit from ' + this.state);
}
}
publish() {
if (this.state === 'in-review') {
this.state = 'published';
} else {
throw new Error('cannot publish from ' + this.state);
}
}
}

After the refactoring

// Each state is a class; the document delegates and transitions polymorphically.
class DraftState {
edit(doc, newContent) { doc.content = newContent; }
submit(doc) { doc.state = new InReviewState(); }
publish(doc) { throw new Error('cannot publish from draft'); }
}
class InReviewState {
edit() { throw new Error('cannot edit while in review'); }
submit() { throw new Error('already in review'); }
publish(doc) { doc.state = new PublishedState(); }
}
class PublishedState {
edit() { throw new Error('cannot edit a published document'); }
submit() { throw new Error('cannot resubmit'); }
publish() { throw new Error('already published'); }
}
class Document {
constructor() {
this.state = new DraftState();
this.content = '';
}
edit(newContent) { this.state.edit(this, newContent); }
submit() { this.state.submit(this); }
publish() { this.state.publish(this); }
}
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 7. Per ADR-0004, the bookend contrasts the state-string dispatcher with polymorphic state transition — the intermediate Extract Class + Move Function steps live in the linked Fowler refactorings. The book uses a media-player state-machine; this JavaScript version uses a document publishing flow.
Pressure
Human

State-string-driven conditionals are Shotgun Surgery on the state field — a new state edits every operation; a renamed state edits every comparison. Bugs in one branch (forgot to throw, used wrong state name) ship as silent corruption of the state machine.

Agent

N states × M operations = N×M cells the agent must verify match the expected transition graph. Cross-state invariants (every state implements every operation; transitions only go forward) cannot be statically enforced from the conditional form.

Tradeoff
Human

State pattern multiplies class count; the state machine's structure spreads across files. The transition graph is implicit (encoded in which state assigns to which other state); reading it requires traversing classes. Per-state objects allocate on every transition (in stateful instance form) unless flyweight-shared.

Agent

State pattern spreads the machine across N files; the agent traverses them to reconstruct the full transition graph. State assignments inside operations are imperative side effects that complicate static reasoning about which state comes next.

Relief
Human

Each state's allowed operations are locally visible; illegal operations throw from the state itself, not from a per-method comparison. The transition graph is enforced by the state classes' assignments; new states ship without touching existing ones.

Agent

Each state lives at one file the agent reads in isolation; adding a new state is one new class implementing the protocol, and the agent traces transitions by following the assignment sites rather than holding the full conditional in attention.

Trap
Human

Many trivial states with mostly-throwing operations become a class hierarchy where the actual state machine is hard to scan. The pattern earns its keep when each state has meaningful behaviour; for state machines that are mostly 'this operation is illegal here', a state-driven dispatch table may stay clearer.

Agent

A state machine with many states that mostly throw makes the agent load N files to discover that operation X is only legal in state Y. A state-transition table (data, not code) may be more economical for the agent to scan than N state classes.