Formal State Machines and Advanced Concepts
In this lesson, we'll explore formal state machine concepts, including guards, conditions, hierarchical states, and state history. We'll also compare the State pattern with the Strategy pattern.
Formal State Machine Concepts
A state machine (or finite state machine - FSM) is a mathematical model of computation that consists of:
- States: A finite set of conditions the system can be in
- Transitions: Rules for moving from one state to another
- Events: Triggers that cause transitions
- Actions: Operations performed during transitions or within states
State Machine Diagram
1 cancel2 ┌───────────────────────────────┐3 │ │4 ▼ │5 ┌─────────┐ pay ┌──────┐ │6 │ Created │──────────►│ Paid │─────┘7 └─────────┘ └───┬──┘8 │ process9 ▼10 ┌────────────┐11 │ Processing │12 └─────┬──────┘13 │ ship14 ▼15 ┌─────────┐16 │ Shipped │17 └────┬────┘18 │ deliver19 ▼20 ┌───────────┐21 │ Delivered │22 └───────────┘
State Machine Table
| Current State | Event | Guard/Condition | Next State | Action |
|---|---|---|---|---|
| Created | pay | amount > 0 | Paid | Process payment |
| Created | cancel | - | Created | Log cancellation |
| Paid | process | items available | Processing | Reserve inventory |
| Paid | cancel | - | Created | Refund payment |
| Processing | ship | items picked | Shipped | Generate label |
| Shipped | deliver | signature received | Delivered | Record delivery |
Guards and Conditions
Guards are boolean conditions that must be true for a transition to occur:
typescript1interface OrderState {2 pay(context: OrderContext): void;3 process(context: OrderContext): void;4 ship(context: OrderContext): void;5 deliver(context: OrderContext): void;6 cancel(context: OrderContext): void;78 getStatusName(): string;9}1011class PaidState extends AbstractOrderState {12 getStatusName(): string {13 return 'Paid';14 }1516 process(context: OrderContext): void {17 // Guard: Check if inventory is available18 if (!this.checkInventoryAvailable(context)) {19 console.log(`Cannot process order ${context.id} - insufficient inventory`);20 return;21 }2223 // Guard: Check if payment is verified24 if (!this.verifyPayment(context)) {25 console.log(`Cannot process order ${context.id} - payment verification failed`);26 return;27 }2829 // All guards passed - perform transition30 console.log(`Starting processing for order ${context.id}`);31 this.transitionTo(context, new ProcessingState(), 'Processing initiated for');32 }3334 private checkInventoryAvailable(context: OrderContext): boolean {35 // Simulate inventory check36 return context.items.length > 0;37 }3839 private verifyPayment(context: OrderContext): boolean {40 // Simulate payment verification41 return context.getTotalAmount() > 0;42 }43}
Complex Guards with Multiple Conditions
typescript1class ProcessingState extends AbstractOrderState {2 getStatusName(): string {3 return 'Processing';4 }56 ship(context: OrderContext): void {7 const checks = {8 itemsPicked: this.areItemsPicked(context),9 qualityChecked: this.isQualityChecked(context),10 labelGenerated: this.isLabelGenerated(context),11 carrierAvailable: this.isCarrierAvailable(context)12 };1314 // Log failed checks15 const failedChecks = Object.entries(checks)16 .filter(([_, passed]) => !passed)17 .map(([check]) => check);1819 if (failedChecks.length > 0) {20 console.log(`Cannot ship order ${context.id}. Failed checks: ${failedChecks.join(', ')}`);21 return;22 }2324 // All checks passed25 console.log(`All pre-shipment checks passed for order ${context.id}`);26 this.transitionTo(context, new ShippedState(), 'Shipped');27 }2829 private areItemsPicked(context: OrderContext): boolean {30 return true; // Simulate check31 }3233 private isQualityChecked(context: OrderContext): boolean {34 return true; // Simulate check35 }3637 private isLabelGenerated(context: OrderContext): boolean {38 return true; // Simulate check39 }4041 private isCarrierAvailable(context: OrderContext): boolean {42 return true; // Simulate check43 }44}
State History and Memento
Track state changes over time using the Memento pattern:
typescript1interface StateSnapshot {2 stateName: string;3 timestamp: Date;4 metadata?: Record<string, any>;5}67class OrderContext {8 private state: OrderState;9 private stateHistory: StateSnapshot[] = [];10 public readonly id: string;11 public readonly items: string[];1213 constructor(id: string, items: string[]) {14 this.id = id;15 this.items = items;16 this.state = new CreatedState();17 this.saveSnapshot();18 }1920 setState(state: OrderState): void {21 this.state = state;22 this.saveSnapshot();23 }2425 private saveSnapshot(): void {26 this.stateHistory.push({27 stateName: this.state.getStatusName(),28 timestamp: new Date(),29 metadata: this.captureMetadata()30 });31 }3233 private captureMetadata(): Record<string, any> {34 const metadata: Record<string, any> = {35 itemCount: this.items.length36 };3738 // Capture state-specific metadata39 if (this.state instanceof ShippedState) {40 metadata.trackingInfo = this.state.getTrackingInfo();41 }4243 return metadata;44 }4546 getHistory(): ReadonlyArray<StateSnapshot> {47 return this.stateHistory;48 }4950 getTimeInState(stateName: string): number {51 const entries = this.stateHistory.filter(s => s.stateName === stateName);52 if (entries.length === 0) return 0;5354 const enter = entries[0].timestamp.getTime();55 const exit = entries.length > 156 ? entries[entries.length - 1].timestamp.getTime()57 : Date.now();5859 return exit - enter;60 }6162 printStateTimeline(): void {63 console.log(`\nState Timeline for Order ${this.id}:`);64 this.stateHistory.forEach((snapshot, index) => {65 const duration = index < this.stateHistory.length - 166 ? this.stateHistory[index + 1].timestamp.getTime() - snapshot.timestamp.getTime()67 : Date.now() - snapshot.timestamp.getTime();6869 console.log(70 ` ${index + 1}. ${snapshot.stateName.padEnd(12)} - ` +71 `${snapshot.timestamp.toISOString()} (${duration}ms)`72 );73 });74 }7576 // Delegate to state77 pay(): void { this.state.pay(this); }78 process(): void { this.state.process(this); }79 ship(): void { this.state.ship(this); }80 deliver(): void { this.state.deliver(this); }81 cancel(): void { this.state.cancel(this); }82 getStatusName(): string { return this.state.getStatusName(); }83}
Hierarchical States (Nested State Machines)
Some states can have sub-states, creating a hierarchy:
1Processing State2├── Picking Items3│ ├── Scanning4│ └── Verifying5├── Quality Check6│ ├── Inspecting7│ └── Photographing8└── Packing9 ├── Boxing10 └── Labeling
typescript1interface SubState {2 getName(): string;3 execute(context: OrderContext): void;4 canTransition(): boolean;5}67class PickingSubState implements SubState {8 private itemsScanned: number = 0;910 getName(): string {11 return 'Picking Items';12 }1314 execute(context: OrderContext): void {15 console.log(`Picking items for order ${context.id}`);16 this.itemsScanned = context.items.length;17 }1819 canTransition(): boolean {20 return this.itemsScanned > 0;21 }22}2324class QualityCheckSubState implements SubState {25 private inspected: boolean = false;2627 getName(): string {28 return 'Quality Check';29 }3031 execute(context: OrderContext): void {32 console.log(`Performing quality check for order ${context.id}`);33 this.inspected = true;34 }3536 canTransition(): boolean {37 return this.inspected;38 }39}4041class PackingSubState implements SubState {42 private packed: boolean = false;4344 getName(): string {45 return 'Packing';46 }4748 execute(context: OrderContext): void {49 console.log(`Packing order ${context.id}`);50 this.packed = true;51 }5253 canTransition(): boolean {54 return this.packed;55 }56}5758class ProcessingState extends AbstractOrderState {59 private subStates: SubState[];60 private currentSubStateIndex: number = 0;6162 constructor() {63 super();64 this.allowedTransitions = ['Shipped'];65 this.subStates = [66 new PickingSubState(),67 new QualityCheckSubState(),68 new PackingSubState()69 ];70 }7172 getStatusName(): string {73 return 'Processing';74 }7576 onEnter(context: OrderContext): void {77 console.log(`Order ${context.id} entering processing pipeline`);78 this.executeSubStates(context);79 }8081 private executeSubStates(context: OrderContext): void {82 for (const subState of this.subStates) {83 console.log(` → ${subState.getName()}`);84 subState.execute(context);8586 if (!subState.canTransition()) {87 console.log(` Failed at ${subState.getName()}`);88 return;89 }90 }9192 console.log(` ✓ All processing steps completed`);93 }9495 ship(context: OrderContext): void {96 // Verify all substates completed97 const allCompleted = this.subStates.every(s => s.canTransition());9899 if (!allCompleted) {100 console.log(`Cannot ship - processing not complete`);101 return;102 }103104 this.transitionTo(context, new ShippedState(), 'Ready to ship');105 }106}
State vs Strategy Pattern
While structurally similar, State and Strategy patterns serve different purposes:
Similarities
Both patterns:
- Use composition over inheritance
- Define a family of algorithms/behaviors
- Encapsulate behavior in separate classes
- Follow the Open/Closed Principle
Key Differences
typescript1// STATE PATTERN: States know about each other and trigger transitions2class PaidState implements OrderState {3 process(context: OrderContext): void {4 // State transitions to another state5 context.setState(new ProcessingState());6 }7}89// STRATEGY PATTERN: Strategies are independent, client changes them10class GroundShipping implements ShippingStrategy {11 calculate(weight: number): number {12 // Just calculates cost, doesn't know about other strategies13 return weight * 5;14 }15}1617class ShippingCalculator {18 private strategy: ShippingStrategy;1920 setStrategy(strategy: ShippingStrategy): void {21 // Client explicitly changes strategy22 this.strategy = strategy;23 }2425 calculate(weight: number): number {26 return this.strategy.calculate(weight);27 }28}
Comparison Table
| Aspect | State Pattern | Strategy Pattern |
|---|---|---|
| Intent | Change behavior when internal state changes | Select algorithm from a family |
| Context Awareness | States are aware of context and other states | Strategies are independent |
| Transitions | States manage their own transitions | Client chooses strategy |
| State/Strategy Count | Usually fixed set of states | Can have unlimited strategies |
| When Changes | Automatically during execution | Explicitly by client |
| Coupling | States coupled to context | Strategies decoupled |
| Example | Order lifecycle, TCP connection | Compression algorithms, sorting |
| Client Knowledge | Client unaware of state changes | Client chooses strategy |
When to Use Each
Use State Pattern when:
- Object behavior changes based on internal state
- Multiple conditional statements based on state
- State transitions follow defined rules
- Example: Order processing, document workflow, game character
Use Strategy Pattern when:
- Multiple algorithms can solve the same problem
- Client needs to choose algorithm at runtime
- Algorithms are independent and interchangeable
- Example: Shipping cost calculation, payment methods, sorting
Combined Example
You can use both patterns together:
typescript1// State pattern for order lifecycle2class ShippedState extends AbstractOrderState {3 // Strategy pattern for delivery method4 private deliveryStrategy: DeliveryStrategy;56 constructor(deliveryStrategy: DeliveryStrategy) {7 super();8 this.deliveryStrategy = deliveryStrategy;9 }1011 deliver(context: OrderContext): void {12 // Use strategy to calculate delivery details13 const deliveryDate = this.deliveryStrategy.calculateDeliveryDate(context);14 console.log(`Estimated delivery: ${deliveryDate}`);1516 // State transition17 this.transitionTo(context, new DeliveredState(), 'Delivered');18 }19}2021interface DeliveryStrategy {22 calculateDeliveryDate(context: OrderContext): Date;23}2425class StandardDelivery implements DeliveryStrategy {26 calculateDeliveryDate(context: OrderContext): Date {27 const date = new Date();28 date.setDate(date.getDate() + 5);29 return date;30 }31}3233class ExpressDelivery implements DeliveryStrategy {34 calculateDeliveryDate(context: OrderContext): Date {35 const date = new Date();36 date.setDate(date.getDate() + 2);37 return date;38 }39}
State Machine Validation
Validate state machine configuration at startup:
typescript1class StateMachineValidator {2 static validate(states: OrderState[]): { valid: boolean; errors: string[] } {3 const errors: string[] = [];45 // Check for duplicate state names6 const stateNames = states.map(s => s.getStatusName());7 const duplicates = stateNames.filter((name, index) => stateNames.indexOf(name) !== index);8 if (duplicates.length > 0) {9 errors.push(`Duplicate states found: ${duplicates.join(', ')}`);10 }1112 // Check for unreachable states13 const reachableStates = new Set<string>();14 states.forEach(state => {15 // Simulate finding reachable states (simplified)16 reachableStates.add(state.getStatusName());17 });1819 // Check for invalid transitions20 // (would need transition table for full validation)2122 return {23 valid: errors.length === 0,24 errors25 };26 }27}
Key Takeaways
- Formal state machines define states, transitions, events, and actions
- Guards are conditions that must be true for transitions to occur
- State history tracks transitions and time spent in each state
- Hierarchical states allow states to have sub-states
- State pattern manages state transitions; Strategy pattern selects algorithms
- States can use strategies internally for specific behaviors
- Validate state machine configuration to catch errors early
In the next workshop, you'll implement a complete order lifecycle state machine with guards, state history, and comprehensive testing.