One-line Definition
Extracts state-specific behaviors into separate classes, allowing an object to change its behavior at runtime as its internal state changes.
Problem
Suppose you are programming a Vending Machine. It behaves wildly differently depending on whether it's in the Idle, HasCoin, or Dispensing state. If you try to manage this using a single VendingMachine class, every single method (insertCoin(), pressButton(), dispense()) requires a massive switch statement checking the current state. Adding a new state (e.g., OutOfStock) breaks every method. The State Pattern solves this by turning states into objects, delegating all state-specific logic to the currently active state object.
Real-world Analogy
Think of a smartphone. If the phone is locked (State A), pressing the home button wakes up the screen. If the phone is unlocked (State B), pressing the home button goes to the home screen. If an app is open (State C), pressing the home button minimizes the app. The physical button is exactly the same, but its behavior completely changes based on the phone's current state.
Structure
- Client
- Interacts with the Context, largely unaware of the internal state transitions.
- Context
- Maintains a reference to one of the ConcreteState subclasses that represents the current state. Delegates all work to this state object.
- State
- Interface declaring state-specific methods.
- ConcreteState
- Implements the behavior associated with a specific state of the Context. Can optionally trigger transitions to other states.
Diagram
classDiagram
class Client {
}
class Context {
-State state
+request()
+setState(State)
}
class State {
<<interface>>
+handle(Context)
}
class ConcreteStateA {
+handle(Context)
}
class ConcreteStateB {
+handle(Context)
}
Client --> Context
Context o-- State : delegates
ConcreteStateA ..|> State
ConcreteStateB ..|> State
ConcreteStateA --> Context : triggers transition
Code Walkthrough
Notice how the VendingMachine (Context) delegates insertCoin() to whatever State object it is currently holding. The IdleState actually triggers the transition into the HasCoinState.
interface State {
void insertCoin(VendingMachine machine);
void dispense(VendingMachine machine);
}
class VendingMachine {
private State state;
public VendingMachine() {
this.state = new IdleState(); // Initial state
}
public void setState(State state) {
this.state = state;
}
// Delegation!
public void insertCoin() { state.insertCoin(this); }
public void dispense() { state.dispense(this); }
}
class IdleState implements State {
@Override
public void insertCoin(VendingMachine machine) {
System.out.println("Coin inserted. Ready to dispense.");
machine.setState(new HasCoinState()); // Transition!
}
@Override
public void dispense(VendingMachine machine) {
System.out.println("Insert coin first!");
}
}
class HasCoinState implements State {
@Override
public void insertCoin(VendingMachine machine) {
System.out.println("Already has a coin. Please wait.");
}
@Override
public void dispense(VendingMachine machine) {
System.out.println("Dispensing item...");
machine.setState(new IdleState()); // Transition!
}
}
class Main {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
machine.dispense(); // Fails: in IdleState
machine.insertCoin(); // Success: moves to HasCoinState
machine.insertCoin(); // Fails: already in HasCoinState
machine.dispense(); // Success: dispenses and goes back to IdleState
}
}
Bad vs Good
Bad Approach
Problems
- Massive, duplicated
if-elseblocks in every single method. - Adding a new state (e.g.,
MaintenanceMode) requires touching every method, violating the Open/Closed Principle.
class VendingMachine {
private String state = "IDLE";
public void insertCoin() {
if (state.equals("IDLE")) {
state = "HAS_COIN";
} else if (state.equals("HAS_COIN")) {
System.out.println("Already has coin.");
}
}
public void dispense() {
if (state.equals("IDLE")) {
System.out.println("Insert coin first.");
} else if (state.equals("HAS_COIN")) {
state = "IDLE";
}
}
}
Better Approach
Improvements
- No
if-elseblocks. The behavior is inherently determined by polymorphism. - Adding a new state means creating one new class, completely isolated from existing logic.
// Context delegates entirely to the current State object
public void insertCoin() {
state.insertCoin(this);
}
Pros vs Cons
| Pros | Cons |
|---|---|
Eliminates massive, monolithic state machines full of if-else | Can be overkill for a state machine with only two or three states |
| Strongly adheres to the Single Responsibility Principle | Scatters state transition logic across many classes |
| Enforces the Open/Closed Principle when adding new states | Can create tight coupling between Concrete States if they manage transitions directly |
| Makes state transitions explicit rather than relying on raw strings/enums |
When to Use
- An object's behavior changes drastically depending on its state.
- You have massive conditional statements (
switch/if) that check the same state variable across numerous methods. - You are implementing a formal Finite State Machine (FSM).
When Not to Use
- The state machine is extremely simple (e.g., just
ON/OFF) and will never change. - The behavior doesn't change significantly based on the state.
Real-world Examples
- TCP Connection states (
LISTEN,ESTABLISHED,CLOSED). - Document workflows (
DRAFT,REVIEW,PUBLISHED). - JSF (JavaServer Faces) lifecycle phases.
Key Takeaway
The State Pattern replaces giant switch statements with polymorphism, turning each state into its own class. Use it when an object behaves like a Finite State Machine, altering its entire personality based on internal conditions. Be aware, however, that it scatters your transition logic across multiple files, which can make the overall "flow" harder to read at a glance.