20 minlesson

Introduction to the Memento Pattern

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:

typescript
1class OrderDraft {
2 private items: string[] = [];
3 private customer: string = '';
4 private shippingAddress: string = '';
5 private notes: string = '';
6
7 addItem(item: string): void {
8 this.items.push(item);
9 }
10
11 setCustomer(customer: string): void {
12 this.customer = customer;
13 }
14
15 setShippingAddress(address: string): void {
16 this.shippingAddress = address;
17 }
18
19 setNotes(notes: string): void {
20 this.notes = notes;
21 }
22
23 // 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:

  1. Broken Encapsulation: Exposing internal state violates encapsulation
  2. No State History: Can't maintain multiple saved versions
  3. No Undo Support: Can't easily revert to previous states
  4. Tight Coupling: Client code needs to know about internal structure
  5. 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.

typescript
1// Memento - stores snapshot of OrderDraft state
2class 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: Date
9 ) {}
10
11 // Memento is opaque - no getters for external access
12 // Only the originator can access the state
13
14 getTimestamp(): Date {
15 return this.timestamp;
16 }
17
18 // 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.notes
25 };
26 }
27}
28
29// Originator - creates and restores from mementos
30class OrderDraft {
31 private items: string[] = [];
32 private customer: string = '';
33 private shippingAddress: string = '';
34 private notes: string = '';
35
36 addItem(item: string): void {
37 this.items.push(item);
38 }
39
40 setCustomer(customer: string): void {
41 this.customer = customer;
42 }
43
44 setShippingAddress(address: string): void {
45 this.shippingAddress = address;
46 }
47
48 setNotes(notes: string): void {
49 this.notes = notes;
50 }
51
52 // Create a memento capturing current state
53 save(): OrderMemento {
54 return new OrderMemento(
55 [...this.items],
56 this.customer,
57 this.shippingAddress,
58 this.notes,
59 new Date()
60 );
61 }
62
63 // Restore state from memento
64 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 }
71
72 getPreview(): string {
73 return `Order for ${this.customer || '(no customer)'}: ${this.items.length} items`;
74 }
75}
76
77// Caretaker - manages mementos without examining their contents
78class DraftHistory {
79 private history: OrderMemento[] = [];
80 private currentIndex: number = -1;
81
82 save(memento: OrderMemento): void {
83 // Remove any "future" history when saving a new version
84 this.history = this.history.slice(0, this.currentIndex + 1);
85 this.history.push(memento);
86 this.currentIndex++;
87 }
88
89 undo(): OrderMemento | null {
90 if (this.currentIndex > 0) {
91 this.currentIndex--;
92 return this.history[this.currentIndex];
93 }
94 return null;
95 }
96
97 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 }
104
105 getHistory(): Array<{ timestamp: Date; index: number }> {
106 return this.history.map((memento, index) => ({
107 timestamp: memento.getTimestamp(),
108 index
109 }));
110 }
111
112 canUndo(): boolean {
113 return this.currentIndex > 0;
114 }
115
116 canRedo(): boolean {
117 return this.currentIndex < this.history.length - 1;
118 }
119}

Pattern Structure

1┌─────────────┐
2│ Caretaker │
3│ (DraftHist) │
4└──────┬──────┘
5 │ manages
6
7
8┌─────────────┐ creates/restores ┌─────────────┐
9│ Originator │─────────────────────────────────►│ Memento │
10│(OrderDraft) │ │ (snapshot) │
11└─────────────┘ └─────────────┘
12 │ │
13 │ has state │ stores state
14 ▼ ▼
15 items, customer items, customer
16 address, notes address, notes
17 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

typescript
1// Create order draft
2const draft = new OrderDraft();
3const history = new DraftHistory();
4
5// Make some changes and save
6draft.setCustomer('ACME Corp');
7draft.addItem('Widget A');
8history.save(draft.save());
9console.log('Saved v1:', draft.getPreview());
10
11// Make more changes and save
12draft.addItem('Widget B');
13draft.setShippingAddress('123 Main St, New York, NY 10001');
14history.save(draft.save());
15console.log('Saved v2:', draft.getPreview());
16
17// Make more changes and save
18draft.addItem('Gadget X');
19draft.setNotes('Rush delivery');
20history.save(draft.save());
21console.log('Saved v3:', draft.getPreview());
22
23// Undo to v2
24const v2 = history.undo();
25if (v2) {
26 draft.restore(v2);
27 console.log('After undo:', draft.getPreview());
28}
29
30// Undo to v1
31const v1 = history.undo();
32if (v1) {
33 draft.restore(v1);
34 console.log('After undo:', draft.getPreview());
35}
36
37// Redo back to v2
38const redoV2 = history.redo();
39if (redoV2) {
40 draft.restore(redoV2);
41 console.log('After redo:', draft.getPreview());
42}
43
44// View history
45console.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 items
2Saved v2: Order for ACME Corp: 2 items
3Saved v3: Order for ACME Corp: 3 items
4After undo: Order for ACME Corp: 2 items
5After undo: Order for ACME Corp: 1 items
6After redo: Order for ACME Corp: 2 items
7
8Version history:
9 1. 10:23:45 AM
10 2. 10:23:46 AM
11 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

  1. Preserves Encapsulation: Internal state stays private
  2. Simplifies Originator: State management logic is externalized
  3. Undo/Redo Support: Natural support for state history
  4. State Snapshots: Capture state at specific points in time
  5. Audit Trail: Complete history of all state changes
  6. 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:

AspectMementoCommand
What's StoredComplete state snapshotIndividual operations
Undo MethodRestore saved stateReverse operation
MemoryHigher (full snapshots)Lower (just operation data)
GranularityCoarse (whole object)Fine (individual changes)
Best ForComplex state, many fieldsDiscrete 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.