20 minlesson

Introduction to the State Pattern

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:

typescript
1class Order {
2 constructor(
3 public id: string,
4 public items: string[],
5 private status: 'created' | 'paid' | 'processing' | 'shipped' | 'delivered' = 'created'
6 ) {}
7
8 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 }
16
17 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 }
25
26 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 }
34
35 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 }
43
44 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:

  1. Complex Conditional Logic: Every method contains state-checking conditionals
  2. Violation of Open/Closed Principle: Adding new states requires modifying existing methods
  3. Poor Maintainability: State transitions are scattered across the class
  4. Limited Extensibility: Adding new behaviors for states is difficult
  5. Code Duplication: Similar state-checking logic repeated across methods
  6. 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:

typescript
1// State interface - defines what actions are available
2interface 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}
10
11// Context - maintains reference to current state
12class OrderContext {
13 private state: OrderState;
14
15 constructor(
16 public readonly id: string,
17 public readonly items: string[]
18 ) {
19 this.state = new CreatedState(); // Initial state
20 }
21
22 setState(state: OrderState): void {
23 this.state = state;
24 console.log(`Order ${this.id} transitioned to ${state.getStatusName()}`);
25 }
26
27 getState(): OrderState {
28 return this.state;
29 }
30
31 // Delegate to current state
32 pay(): void {
33 this.state.pay(this);
34 }
35
36 process(): void {
37 this.state.process(this);
38 }
39
40 ship(): void {
41 this.state.ship(this);
42 }
43
44 deliver(): void {
45 this.state.deliver(this);
46 }
47
48 cancel(): void {
49 this.state.cancel(this);
50 }
51}
52
53// Concrete States
54class CreatedState implements OrderState {
55 getStatusName(): string {
56 return 'Created';
57 }
58
59 pay(order: OrderContext): void {
60 console.log(`Processing payment for order ${order.id}`);
61 order.setState(new PaidState());
62 }
63
64 process(order: OrderContext): void {
65 console.log('Cannot process unpaid order');
66 }
67
68 ship(order: OrderContext): void {
69 console.log('Cannot ship unpaid order');
70 }
71
72 deliver(order: OrderContext): void {
73 console.log('Cannot deliver unpaid order');
74 }
75
76 cancel(order: OrderContext): void {
77 console.log(`Order ${order.id} cancelled`);
78 // Stay in created state
79 }
80}
81
82class PaidState implements OrderState {
83 getStatusName(): string {
84 return 'Paid';
85 }
86
87 pay(order: OrderContext): void {
88 console.log('Order is already paid');
89 }
90
91 process(order: OrderContext): void {
92 console.log(`Starting to process order ${order.id}`);
93 order.setState(new ProcessingState());
94 }
95
96 ship(order: OrderContext): void {
97 console.log('Cannot ship unprocessed order');
98 }
99
100 deliver(order: OrderContext): void {
101 console.log('Cannot deliver unprocessed order');
102 }
103
104 cancel(order: OrderContext): void {
105 console.log(`Refunding order ${order.id}`);
106 order.setState(new CreatedState());
107 }
108}
109
110class ProcessingState implements OrderState {
111 getStatusName(): string {
112 return 'Processing';
113 }
114
115 pay(order: OrderContext): void {
116 console.log('Order is already paid');
117 }
118
119 process(order: OrderContext): void {
120 console.log('Order is already being processed');
121 }
122
123 ship(order: OrderContext): void {
124 console.log(`Shipping order ${order.id}`);
125 order.setState(new ShippedState());
126 }
127
128 deliver(order: OrderContext): void {
129 console.log('Cannot deliver unshipped order');
130 }
131
132 cancel(order: OrderContext): void {
133 console.log('Cannot cancel order that is being processed');
134 }
135}
136
137class ShippedState implements OrderState {
138 getStatusName(): string {
139 return 'Shipped';
140 }
141
142 pay(order: OrderContext): void {
143 console.log('Order is already paid');
144 }
145
146 process(order: OrderContext): void {
147 console.log('Order is already processed');
148 }
149
150 ship(order: OrderContext): void {
151 console.log('Order is already shipped');
152 }
153
154 deliver(order: OrderContext): void {
155 console.log(`Delivering order ${order.id}`);
156 order.setState(new DeliveredState());
157 }
158
159 cancel(order: OrderContext): void {
160 console.log('Cannot cancel shipped order');
161 }
162}
163
164class DeliveredState implements OrderState {
165 getStatusName(): string {
166 return 'Delivered';
167 }
168
169 pay(order: OrderContext): void {
170 console.log('Order is already paid');
171 }
172
173 process(order: OrderContext): void {
174 console.log('Order is already processed');
175 }
176
177 ship(order: OrderContext): void {
178 console.log('Order is already shipped');
179 }
180
181 deliver(order: OrderContext): void {
182 console.log('Order is already delivered');
183 }
184
185 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 to
6│ + 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

typescript
1// Client code
2const order = new OrderContext('ORD-001', ['Laptop', 'Mouse']);
3
4// Valid state transitions
5order.pay(); // Created → Paid
6order.process(); // Paid → Processing
7order.ship(); // Processing → Shipped
8order.deliver(); // Shipped → Delivered
9
10// Invalid transition attempts
11order.ship(); // Already delivered - no effect
12
13// New order with different flow
14const order2 = new OrderContext('ORD-002', ['Keyboard']);
15order2.pay(); // Created → Paid
16order2.cancel(); // Paid → Created (refunded)

Output:

1Order ORD-001 transitioned to Created
2Processing payment for order ORD-001
3Order ORD-001 transitioned to Paid
4Starting to process order ORD-001
5Order ORD-001 transitioned to Processing
6Shipping order ORD-001
7Order ORD-001 transitioned to Shipped
8Delivering order ORD-001
9Order ORD-001 transitioned to Delivered
10Order is already shipped
11
12Order ORD-002 transitioned to Created
13Processing payment for order ORD-002
14Order ORD-002 transitioned to Paid
15Refunding order ORD-002
16Order 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:

typescript
1class ProcessingState implements OrderState {
2 constructor() {
3 // Entry action
4 this.notifyWarehouse();
5 this.reserveInventory();
6 }
7
8 private notifyWarehouse(): void {
9 console.log('Warehouse notified to prepare shipment');
10 }
11
12 private reserveInventory(): void {
13 console.log('Inventory reserved for order');
14 }
15
16 ship(order: OrderContext): void {
17 // Exit action
18 this.releaseInventory();
19 this.generateShippingLabel();
20
21 order.setState(new ShippedState());
22 }
23
24 private releaseInventory(): void {
25 console.log('Inventory released from warehouse');
26 }
27
28 private generateShippingLabel(): void {
29 console.log('Shipping label generated');
30 }
31
32 // ... other methods
33}

Benefits

  1. Eliminates Conditionals: No more if-else chains for state checking
  2. Single Responsibility: Each state class handles one state's behavior
  3. Open/Closed Principle: Add new states without modifying existing code
  4. Clear State Transitions: Transitions are explicit and easy to understand
  5. Testability: Each state can be tested independently
  6. 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:

AspectState PatternStrategy Pattern
PurposeChange behavior based on internal stateSelect algorithm at runtime
State AwarenessStates know about each other and transitionsStrategies are independent
Who ChangesStates change themselvesClient changes strategy
ContextRequired for transitionsOptional, just executes algorithm
ExampleOrder lifecycleSorting 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.