Introduction to the State Pattern
The State pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class, as the behavior changes dramatically depending on the current state.
The Problem
In traditional programming, objects often have different behaviors based on their current state. This is typically implemented using conditional logic with if-else or switch statements:
typescript1class Order {2 constructor(3 public id: string,4 public items: string[],5 private status: 'created' | 'paid' | 'processing' | 'shipped' | 'delivered' = 'created'6 ) {}78 pay(): void {9 if (this.status === 'created') {10 this.status = 'paid';11 console.log(`Order ${this.id} has been paid`);12 } else {13 console.log(`Cannot pay order in ${this.status} status`);14 }15 }1617 process(): void {18 if (this.status === 'paid') {19 this.status = 'processing';20 console.log(`Order ${this.id} is being processed`);21 } else {22 console.log(`Cannot process order in ${this.status} status`);23 }24 }2526 ship(): void {27 if (this.status === 'processing') {28 this.status = 'shipped';29 console.log(`Order ${this.id} has been shipped`);30 } else {31 console.log(`Cannot ship order in ${this.status} status`);32 }33 }3435 deliver(): void {36 if (this.status === 'shipped') {37 this.status = 'delivered';38 console.log(`Order ${this.id} has been delivered`);39 } else {40 console.log(`Cannot deliver order in ${this.status} status`);41 }42 }4344 cancel(): void {45 if (this.status === 'created' || this.status === 'paid') {46 this.status = 'created';47 console.log(`Order ${this.id} has been cancelled`);48 } else {49 console.log(`Cannot cancel order in ${this.status} status`);50 }51 }52}
Problems with this approach:
- Complex Conditional Logic: Every method contains state-checking conditionals
- Violation of Open/Closed Principle: Adding new states requires modifying existing methods
- Poor Maintainability: State transitions are scattered across the class
- Limited Extensibility: Adding new behaviors for states is difficult
- Code Duplication: Similar state-checking logic repeated across methods
- Difficult Testing: Each method needs tests for all possible states
The Solution: State Pattern
The State pattern solves these problems by encapsulating state-specific behavior into separate state classes. Each state class implements the same interface, and the context object delegates to the current state:
typescript1// State interface - defines what actions are available2interface OrderState {3 pay(order: OrderContext): void;4 process(order: OrderContext): void;5 ship(order: OrderContext): void;6 deliver(order: OrderContext): void;7 cancel(order: OrderContext): void;8 getStatusName(): string;9}1011// Context - maintains reference to current state12class OrderContext {13 private state: OrderState;1415 constructor(16 public readonly id: string,17 public readonly items: string[]18 ) {19 this.state = new CreatedState(); // Initial state20 }2122 setState(state: OrderState): void {23 this.state = state;24 console.log(`Order ${this.id} transitioned to ${state.getStatusName()}`);25 }2627 getState(): OrderState {28 return this.state;29 }3031 // Delegate to current state32 pay(): void {33 this.state.pay(this);34 }3536 process(): void {37 this.state.process(this);38 }3940 ship(): void {41 this.state.ship(this);42 }4344 deliver(): void {45 this.state.deliver(this);46 }4748 cancel(): void {49 this.state.cancel(this);50 }51}5253// Concrete States54class CreatedState implements OrderState {55 getStatusName(): string {56 return 'Created';57 }5859 pay(order: OrderContext): void {60 console.log(`Processing payment for order ${order.id}`);61 order.setState(new PaidState());62 }6364 process(order: OrderContext): void {65 console.log('Cannot process unpaid order');66 }6768 ship(order: OrderContext): void {69 console.log('Cannot ship unpaid order');70 }7172 deliver(order: OrderContext): void {73 console.log('Cannot deliver unpaid order');74 }7576 cancel(order: OrderContext): void {77 console.log(`Order ${order.id} cancelled`);78 // Stay in created state79 }80}8182class PaidState implements OrderState {83 getStatusName(): string {84 return 'Paid';85 }8687 pay(order: OrderContext): void {88 console.log('Order is already paid');89 }9091 process(order: OrderContext): void {92 console.log(`Starting to process order ${order.id}`);93 order.setState(new ProcessingState());94 }9596 ship(order: OrderContext): void {97 console.log('Cannot ship unprocessed order');98 }99100 deliver(order: OrderContext): void {101 console.log('Cannot deliver unprocessed order');102 }103104 cancel(order: OrderContext): void {105 console.log(`Refunding order ${order.id}`);106 order.setState(new CreatedState());107 }108}109110class ProcessingState implements OrderState {111 getStatusName(): string {112 return 'Processing';113 }114115 pay(order: OrderContext): void {116 console.log('Order is already paid');117 }118119 process(order: OrderContext): void {120 console.log('Order is already being processed');121 }122123 ship(order: OrderContext): void {124 console.log(`Shipping order ${order.id}`);125 order.setState(new ShippedState());126 }127128 deliver(order: OrderContext): void {129 console.log('Cannot deliver unshipped order');130 }131132 cancel(order: OrderContext): void {133 console.log('Cannot cancel order that is being processed');134 }135}136137class ShippedState implements OrderState {138 getStatusName(): string {139 return 'Shipped';140 }141142 pay(order: OrderContext): void {143 console.log('Order is already paid');144 }145146 process(order: OrderContext): void {147 console.log('Order is already processed');148 }149150 ship(order: OrderContext): void {151 console.log('Order is already shipped');152 }153154 deliver(order: OrderContext): void {155 console.log(`Delivering order ${order.id}`);156 order.setState(new DeliveredState());157 }158159 cancel(order: OrderContext): void {160 console.log('Cannot cancel shipped order');161 }162}163164class DeliveredState implements OrderState {165 getStatusName(): string {166 return 'Delivered';167 }168169 pay(order: OrderContext): void {170 console.log('Order is already paid');171 }172173 process(order: OrderContext): void {174 console.log('Order is already processed');175 }176177 ship(order: OrderContext): void {178 console.log('Order is already shipped');179 }180181 deliver(order: OrderContext): void {182 console.log('Order is already delivered');183 }184185 cancel(order: OrderContext): void {186 console.log('Cannot cancel delivered order');187 }188}
Pattern Structure
1┌──────────────────┐2│ OrderContext │3│ ─────────────── │4│ - state: State │───────┐5│ + pay() │ │ delegates to6│ + process() │ │7│ + ship() │ ▼8│ + deliver() │ ┌────────────┐9└──────────────────┘ │ OrderState │◄─────────┐10 │ interface │ │11 └────────────┘ │12 △ │13 │ implements │14 ┌────────┴────────┐ │15 │ │ │16 ┌──────────────┐ ┌──────────────┐│17 │ CreatedState │ │ PaidState ││18 └──────────────┘ └──────────────┘│19 │20 ┌──────────────┐ ┌──────────────┐│21 │ProcessingState │ ShippedState ││22 └──────────────┘ └──────────────┘│23 │24 ┌──────────────┐ │25 │DeliveredState│───────────────────┘26 └──────────────┘27 contains state transition logic
Key Components:
- State: Interface declaring state-specific methods
- ConcreteState: Implements behavior for a specific state and handles transitions
- Context: Maintains a reference to current state and delegates operations to it
Using the State Pattern
typescript1// Client code2const order = new OrderContext('ORD-001', ['Laptop', 'Mouse']);34// Valid state transitions5order.pay(); // Created → Paid6order.process(); // Paid → Processing7order.ship(); // Processing → Shipped8order.deliver(); // Shipped → Delivered910// Invalid transition attempts11order.ship(); // Already delivered - no effect1213// New order with different flow14const order2 = new OrderContext('ORD-002', ['Keyboard']);15order2.pay(); // Created → Paid16order2.cancel(); // Paid → Created (refunded)
Output:
1Order ORD-001 transitioned to Created2Processing payment for order ORD-0013Order ORD-001 transitioned to Paid4Starting to process order ORD-0015Order ORD-001 transitioned to Processing6Shipping order ORD-0017Order ORD-001 transitioned to Shipped8Delivering order ORD-0019Order ORD-001 transitioned to Delivered10Order is already shipped1112Order ORD-002 transitioned to Created13Processing payment for order ORD-00214Order ORD-002 transitioned to Paid15Refunding order ORD-00216Order ORD-002 transitioned to Created
Logistics Domain Context: Order Lifecycle
In logistics and supply chain management, orders go through a well-defined lifecycle:
Standard Order Flow
Created → Paid → Processing → Shipped → Delivered
State Transitions and Rules
Created State:
- Actions: Can be paid or cancelled
- Cannot: Process, ship, or deliver
- Transitions: To Paid (on payment) or stays Created (on cancel)
Paid State:
- Actions: Can be processed or cancelled (with refund)
- Cannot: Ship or deliver directly
- Transitions: To Processing (on process) or back to Created (on cancel)
Processing State:
- Actions: Can be shipped
- Cannot: Be paid again or cancelled
- Transitions: To Shipped (when ready)
Shipped State:
- Actions: Can be delivered
- Cannot: Be cancelled or modified
- Transitions: To Delivered (on delivery confirmation)
Delivered State:
- Actions: None (terminal state)
- Cannot: Be modified or cancelled
- Transitions: None (final state)
Entry/Exit Actions
States can have special actions when entering or exiting:
typescript1class ProcessingState implements OrderState {2 constructor() {3 // Entry action4 this.notifyWarehouse();5 this.reserveInventory();6 }78 private notifyWarehouse(): void {9 console.log('Warehouse notified to prepare shipment');10 }1112 private reserveInventory(): void {13 console.log('Inventory reserved for order');14 }1516 ship(order: OrderContext): void {17 // Exit action18 this.releaseInventory();19 this.generateShippingLabel();2021 order.setState(new ShippedState());22 }2324 private releaseInventory(): void {25 console.log('Inventory released from warehouse');26 }2728 private generateShippingLabel(): void {29 console.log('Shipping label generated');30 }3132 // ... other methods33}
Benefits
- Eliminates Conditionals: No more if-else chains for state checking
- Single Responsibility: Each state class handles one state's behavior
- Open/Closed Principle: Add new states without modifying existing code
- Clear State Transitions: Transitions are explicit and easy to understand
- Testability: Each state can be tested independently
- Maintainability: State-specific logic is localized
When to Use
Use the State pattern when:
- State-dependent behavior: Object behavior changes significantly based on state
- Complex conditionals: Multiple if-else or switch statements based on state
- State transitions: Well-defined rules for moving between states
- Multiple states: Object has 3 or more distinct states
- Workflow systems: Order processing, document approval, manufacturing
Trade-offs
Pros:
- Cleaner code without complex conditionals
- Easy to add new states
- State-specific behavior is encapsulated
- State transitions are explicit
Cons:
- Increases number of classes (one per state)
- Can be overkill for simple state machines (2-3 states)
- State classes are tightly coupled to context
- Requires careful design of state interface
State vs Strategy Pattern
While State and Strategy patterns have similar structures, they serve different purposes:
| Aspect | State Pattern | Strategy Pattern |
|---|---|---|
| Purpose | Change behavior based on internal state | Select algorithm at runtime |
| State Awareness | States know about each other and transitions | Strategies are independent |
| Who Changes | States change themselves | Client changes strategy |
| Context | Required for transitions | Optional, just executes algorithm |
| Example | Order lifecycle | Sorting algorithms |
Key Takeaways
- State pattern encapsulates state-specific behavior in separate classes
- Eliminates complex conditionals based on state
- Each state knows its valid transitions to other states
- Particularly useful in logistics for order lifecycle management
- Context delegates to current state, which handles transitions
- Makes adding new states easy without modifying existing code
In the next lesson, we'll explore implementing the State pattern with a focus on state-specific logic and transition validation.