Introduction to the Memento Pattern
The Memento pattern is a behavioral design pattern that captures and externalizes an object's internal state so that the object can be restored to this state later, without violating encapsulation.
The Problem
Imagine you're building an order management system where users can create draft orders. They should be able to save their work-in-progress and restore it later. Traditional approaches create several challenges:
typescript1class OrderDraft {2 private items: string[] = [];3 private customer: string = '';4 private shippingAddress: string = '';5 private notes: string = '';67 addItem(item: string): void {8 this.items.push(item);9 }1011 setCustomer(customer: string): void {12 this.customer = customer;13 }1415 setShippingAddress(address: string): void {16 this.shippingAddress = address;17 }1819 setNotes(notes: string): void {20 this.notes = notes;21 }2223 // How do we save this state?24 // Option 1: Expose all fields publicly? (breaks encapsulation)25 // Option 2: Return a complex state object? (tight coupling)26 // Option 3: Serialize to JSON? (loses type safety, functions, private data)27}
Problems with this approach:
- Broken Encapsulation: Exposing internal state violates encapsulation
- No State History: Can't maintain multiple saved versions
- No Undo Support: Can't easily revert to previous states
- Tight Coupling: Client code needs to know about internal structure
- Validation Issues: Restored state might bypass validation logic
The Solution: Memento Pattern
The Memento pattern solves these problems by introducing a memento object that stores a snapshot of the originator's state. The memento is opaque to everyone except the originator.
typescript1// Memento - stores snapshot of OrderDraft state2class OrderMemento {3 constructor(4 private readonly items: string[],5 private readonly customer: string,6 private readonly shippingAddress: string,7 private readonly notes: string,8 private readonly timestamp: Date9 ) {}1011 // Memento is opaque - no getters for external access12 // Only the originator can access the state1314 getTimestamp(): Date {15 return this.timestamp;16 }1718 // Package-private getters (in practice, use friend classes or symbols)19 getState() {20 return {21 items: [...this.items],22 customer: this.customer,23 shippingAddress: this.shippingAddress,24 notes: this.notes25 };26 }27}2829// Originator - creates and restores from mementos30class OrderDraft {31 private items: string[] = [];32 private customer: string = '';33 private shippingAddress: string = '';34 private notes: string = '';3536 addItem(item: string): void {37 this.items.push(item);38 }3940 setCustomer(customer: string): void {41 this.customer = customer;42 }4344 setShippingAddress(address: string): void {45 this.shippingAddress = address;46 }4748 setNotes(notes: string): void {49 this.notes = notes;50 }5152 // Create a memento capturing current state53 save(): OrderMemento {54 return new OrderMemento(55 [...this.items],56 this.customer,57 this.shippingAddress,58 this.notes,59 new Date()60 );61 }6263 // Restore state from memento64 restore(memento: OrderMemento): void {65 const state = memento.getState();66 this.items = [...state.items];67 this.customer = state.customer;68 this.shippingAddress = state.shippingAddress;69 this.notes = state.notes;70 }7172 getPreview(): string {73 return `Order for ${this.customer || '(no customer)'}: ${this.items.length} items`;74 }75}7677// Caretaker - manages mementos without examining their contents78class DraftHistory {79 private history: OrderMemento[] = [];80 private currentIndex: number = -1;8182 save(memento: OrderMemento): void {83 // Remove any "future" history when saving a new version84 this.history = this.history.slice(0, this.currentIndex + 1);85 this.history.push(memento);86 this.currentIndex++;87 }8889 undo(): OrderMemento | null {90 if (this.currentIndex > 0) {91 this.currentIndex--;92 return this.history[this.currentIndex];93 }94 return null;95 }9697 redo(): OrderMemento | null {98 if (this.currentIndex < this.history.length - 1) {99 this.currentIndex++;100 return this.history[this.currentIndex];101 }102 return null;103 }104105 getHistory(): Array<{ timestamp: Date; index: number }> {106 return this.history.map((memento, index) => ({107 timestamp: memento.getTimestamp(),108 index109 }));110 }111112 canUndo(): boolean {113 return this.currentIndex > 0;114 }115116 canRedo(): boolean {117 return this.currentIndex < this.history.length - 1;118 }119}
Pattern Structure
1┌─────────────┐2│ Caretaker │3│ (DraftHist) │4└──────┬──────┘5 │ manages6 │7 ▼8┌─────────────┐ creates/restores ┌─────────────┐9│ Originator │─────────────────────────────────►│ Memento │10│(OrderDraft) │ │ (snapshot) │11└─────────────┘ └─────────────┘12 │ │13 │ has state │ stores state14 ▼ ▼15 items, customer items, customer16 address, notes address, notes17 timestamp
Key Components:
- Memento: Stores internal state of the originator (OrderMemento)
- Originator: Creates mementos and restores its state from them (OrderDraft)
- Caretaker: Manages memento history without examining contents (DraftHistory)
Using the Memento Pattern
typescript1// Create order draft2const draft = new OrderDraft();3const history = new DraftHistory();45// Make some changes and save6draft.setCustomer('ACME Corp');7draft.addItem('Widget A');8history.save(draft.save());9console.log('Saved v1:', draft.getPreview());1011// Make more changes and save12draft.addItem('Widget B');13draft.setShippingAddress('123 Main St, New York, NY 10001');14history.save(draft.save());15console.log('Saved v2:', draft.getPreview());1617// Make more changes and save18draft.addItem('Gadget X');19draft.setNotes('Rush delivery');20history.save(draft.save());21console.log('Saved v3:', draft.getPreview());2223// Undo to v224const v2 = history.undo();25if (v2) {26 draft.restore(v2);27 console.log('After undo:', draft.getPreview());28}2930// Undo to v131const v1 = history.undo();32if (v1) {33 draft.restore(v1);34 console.log('After undo:', draft.getPreview());35}3637// Redo back to v238const redoV2 = history.redo();39if (redoV2) {40 draft.restore(redoV2);41 console.log('After redo:', draft.getPreview());42}4344// View history45console.log('\nVersion history:');46history.getHistory().forEach((entry, idx) => {47 console.log(` ${idx + 1}. ${entry.timestamp.toLocaleTimeString()}`);48});
Output:
1Saved v1: Order for ACME Corp: 1 items2Saved v2: Order for ACME Corp: 2 items3Saved v3: Order for ACME Corp: 3 items4After undo: Order for ACME Corp: 2 items5After undo: Order for ACME Corp: 1 items6After redo: Order for ACME Corp: 2 items78Version history:9 1. 10:23:45 AM10 2. 10:23:46 AM11 3. 10:23:47 AM
Logistics Domain Context
In logistics and supply chain management, the Memento pattern is particularly valuable:
Order Draft Management
- Save Progress: Users can save partial orders and return later
- Version History: Track all changes to an order over time
- Undo Changes: Revert accidental modifications
- Compare Versions: See what changed between saved versions
Route Planning
- Save Routes: Store optimized delivery routes
- Route Variations: Compare different route options
- Rollback: Revert to previous route if new one doesn't work
- Snapshot Before Optimization: Save current state before trying optimizations
Inventory Snapshots
- Daily Snapshots: Store inventory state at end of day
- Audit Trail: Reconstruct inventory state at any point in time
- Rollback Transactions: Undo inventory adjustments if errors detected
- Trend Analysis: Compare inventory snapshots over time
Configuration Management
- Settings Backup: Save system configuration before changes
- Quick Restore: Revert to working configuration if problems occur
- Configuration History: Maintain history of all setting changes
- Environment Switching: Switch between dev/staging/prod configs
Benefits
- Preserves Encapsulation: Internal state stays private
- Simplifies Originator: State management logic is externalized
- Undo/Redo Support: Natural support for state history
- State Snapshots: Capture state at specific points in time
- Audit Trail: Complete history of all state changes
- Time Travel: Navigate backward and forward through state history
When to Use
Use the Memento pattern when you need:
- Undo/Redo: Text editors, graphics apps, order drafters
- State Snapshots: Save points in games, draft systems
- Audit History: Track all changes to important objects
- Transaction Rollback: Revert to previous valid state
- State Comparison: Compare current vs saved states
- Checkpointing: Save state before risky operations
Trade-offs
Pros:
- Maintains encapsulation boundaries
- Simplifies originator implementation
- Makes undo/redo straightforward
- Natural audit trail
Cons:
- Memory overhead for storing mementos
- Copying state can be expensive
- Mementos can become stale if not managed
- May need custom serialization for complex objects
Memento vs Command
Both patterns support undo, but differently:
| Aspect | Memento | Command |
|---|---|---|
| What's Stored | Complete state snapshot | Individual operations |
| Undo Method | Restore saved state | Reverse operation |
| Memory | Higher (full snapshots) | Lower (just operation data) |
| Granularity | Coarse (whole object) | Fine (individual changes) |
| Best For | Complex state, many fields | Discrete operations |
When to use Memento:
- Complex objects with many fields
- State changes are complex or interconnected
- Need to restore entire object state at once
When to use Command:
- Individual operations are distinct
- Can easily reverse each operation
- Want fine-grained undo/redo
Key Takeaways
- Memento pattern captures object state without breaking encapsulation
- Three roles: Originator (creates/restores), Memento (stores state), Caretaker (manages history)
- Mementos are opaque to everyone except the originator
- Particularly useful in logistics for draft management and state snapshots
- Supports undo/redo through state restoration
- Trade memory for the ability to time travel through object history
In the next lesson, we'll dive deeper into implementing the Memento pattern with a focus on efficient storage and serialization strategies.