Browse TypeScript Design Patterns & Application Architecture

State Saving and Restoration with Memento Pattern

Explore how the Memento Pattern in TypeScript enables efficient state saving and restoration, facilitating undo capabilities and state management.

6.6.2 State Saving and Restoration with Memento Pattern

In the realm of software design, managing the state of an object is a critical task, especially when it comes to implementing features like undo and redo. The Memento Pattern offers a robust solution for capturing and restoring an object’s state without exposing its internal structure. This section delves into the intricacies of the Memento Pattern, exploring how it facilitates state management and undo capabilities in TypeScript applications.

Understanding the Memento Pattern

The Memento Pattern is a behavioral design pattern that allows you to save and restore the previous state of an object without revealing the details of its implementation. It is particularly useful in scenarios where you need to implement undo/redo functionality, as it enables you to revert an object to a previous state.

Key Components of the Memento Pattern

  1. Originator: The object whose state needs to be saved and restored. It creates a memento containing a snapshot of its current state and can use the memento to restore its state.

  2. Memento: A representation of the state of the Originator. It is an immutable object that stores the state of the Originator at a particular point in time.

  3. Caretaker: Manages the mementos. It is responsible for storing and restoring the mementos but does not operate on or examine the contents of the mementos.

Capturing State Without Exposing Internal Structure

One of the primary advantages of the Memento Pattern is its ability to capture an object’s state without exposing its internal structure. This encapsulation is achieved by having the Originator create a Memento object that contains the state information. The Memento is typically a simple object with no methods other than those necessary to store and retrieve the state.

 1// Originator class
 2class Editor {
 3  private content: string;
 4
 5  constructor() {
 6    this.content = '';
 7  }
 8
 9  public type(words: string): void {
10    this.content += words;
11  }
12
13  public getContent(): string {
14    return this.content;
15  }
16
17  public save(): EditorMemento {
18    return new EditorMemento(this.content);
19  }
20
21  public restore(memento: EditorMemento): void {
22    this.content = memento.getContent();
23  }
24}
25
26// Memento class
27class EditorMemento {
28  private readonly content: string;
29
30  constructor(content: string) {
31    this.content = content;
32  }
33
34  public getContent(): string {
35    return this.content;
36  }
37}
38
39// Caretaker class
40class History {
41  private mementos: EditorMemento[] = [];
42
43  public push(memento: EditorMemento): void {
44    this.mementos.push(memento);
45  }
46
47  public pop(): EditorMemento | undefined {
48    return this.mementos.pop();
49  }
50}

In this example, the Editor class acts as the Originator, the EditorMemento class acts as the Memento, and the History class acts as the Caretaker. The Editor can save its state to a EditorMemento and restore it later.

Managing Multiple States and History of Mementos

When implementing undo/redo functionality, it’s crucial to manage a history of mementos. The Caretaker is responsible for maintaining this history. It stores a stack of mementos, allowing you to push new states and pop old ones as needed.

Implementing Undo and Redo Functionality

To implement undo and redo functionality, you need to maintain two stacks: one for undo operations and another for redo operations. When the user performs an action, you save the current state to the undo stack. If the user undoes an action, you pop the state from the undo stack and push it to the redo stack. If the user redoes an action, you pop the state from the redo stack and push it back to the undo stack.

 1class EditorWithUndoRedo {
 2  private content: string;
 3  private undoStack: EditorMemento[] = [];
 4  private redoStack: EditorMemento[] = [];
 5
 6  constructor() {
 7    this.content = '';
 8  }
 9
10  public type(words: string): void {
11    this.undoStack.push(this.save());
12    this.content += words;
13    this.redoStack = []; // Clear redo stack on new action
14  }
15
16  public getContent(): string {
17    return this.content;
18  }
19
20  public save(): EditorMemento {
21    return new EditorMemento(this.content);
22  }
23
24  public restore(memento: EditorMemento): void {
25    this.content = memento.getContent();
26  }
27
28  public undo(): void {
29    if (this.undoStack.length > 0) {
30      this.redoStack.push(this.save());
31      const memento = this.undoStack.pop();
32      if (memento) this.restore(memento);
33    }
34  }
35
36  public redo(): void {
37    if (this.redoStack.length > 0) {
38      this.undoStack.push(this.save());
39      const memento = this.redoStack.pop();
40      if (memento) this.restore(memento);
41    }
42  }
43}

In this implementation, the EditorWithUndoRedo class maintains two stacks: undoStack and redoStack. The type method saves the current state to the undoStack before making changes, and the undo and redo methods manage the state transitions between the stacks.

Considerations for Memory Usage

When implementing the Memento Pattern, it’s essential to consider the memory usage associated with storing multiple mementos. Each memento represents a snapshot of the object’s state, and storing many mementos can consume significant memory.

Strategies for Optimizing State Storage

  1. Incremental Changes: Instead of storing the entire state, store only the changes between states. This approach can significantly reduce memory usage, especially for large objects.

  2. Compression: Apply compression techniques to reduce the size of the stored state. This can be particularly useful when dealing with large data sets.

  3. Pruning: Implement a strategy to prune old or unnecessary mementos. For example, you might keep only the last N states or periodically remove older states.

  4. Lazy Loading: Load mementos only when needed. This approach can help reduce memory usage by keeping only the necessary states in memory.

Example: Incremental Changes

To implement incremental changes, you can modify the Memento class to store only the differences between states. This approach requires a mechanism to apply these differences when restoring the state.

 1// Incremental Memento class
 2class IncrementalEditorMemento {
 3  private readonly changes: string;
 4
 5  constructor(changes: string) {
 6    this.changes = changes;
 7  }
 8
 9  public getChanges(): string {
10    return this.changes;
11  }
12}
13
14// Modified Editor class
15class EditorWithIncrementalChanges {
16  private content: string;
17  private history: IncrementalEditorMemento[] = [];
18
19  constructor() {
20    this.content = '';
21  }
22
23  public type(words: string): void {
24    const changes = words;
25    this.history.push(new IncrementalEditorMemento(changes));
26    this.content += changes;
27  }
28
29  public getContent(): string {
30    return this.content;
31  }
32
33  public undo(): void {
34    if (this.history.length > 0) {
35      const memento = this.history.pop();
36      if (memento) {
37        const changes = memento.getChanges();
38        this.content = this.content.slice(0, -changes.length);
39      }
40    }
41  }
42}

In this example, the IncrementalEditorMemento class stores only the changes made to the content. The EditorWithIncrementalChanges class uses these incremental mementos to manage the state efficiently.

Visualizing the Memento Pattern

To better understand the flow of the Memento Pattern, let’s visualize the interactions between the Originator, Memento, and Caretaker using a sequence diagram.

    sequenceDiagram
	    participant Originator
	    participant Memento
	    participant Caretaker
	
	    Originator->>Memento: Create Memento
	    Memento-->>Caretaker: Store Memento
	    Caretaker-->>Originator: Restore Memento
	    Originator->>Memento: Restore State

Diagram Description: This sequence diagram illustrates the process of creating, storing, and restoring a memento. The Originator creates a Memento, which is stored by the Caretaker. When needed, the Caretaker provides the Memento back to the Originator for state restoration.

Try It Yourself

To deepen your understanding of the Memento Pattern, try modifying the code examples provided. Here are some suggestions:

  • Add a Redo Stack: Extend the EditorWithIncrementalChanges class to support redo functionality by maintaining a redo stack.
  • Implement Pruning: Introduce a mechanism to prune old mementos in the History class to manage memory usage.
  • Experiment with Compression: Apply a simple compression algorithm to the memento data to reduce memory usage.

Knowledge Check

Before we wrap up, let’s reinforce the key concepts covered in this section:

  • The Memento Pattern enables saving and restoring an object’s state without exposing its internal structure.
  • The pattern involves three main components: Originator, Memento, and Caretaker.
  • Managing multiple states requires maintaining a history of mementos, typically using stacks for undo and redo operations.
  • Memory usage can be optimized through strategies like incremental changes, compression, pruning, and lazy loading.

Embrace the Journey

Remember, mastering design patterns is a journey. The Memento Pattern is just one of many tools in your software design toolkit. As you continue to explore and implement these patterns, you’ll develop a deeper understanding of how to create robust, maintainable, and efficient software systems. Keep experimenting, stay curious, and enjoy the journey!

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026