Browse TypeScript Design Patterns & Application Architecture

Memento Pattern Use Cases and Examples in TypeScript

Explore practical applications of the Memento Pattern in TypeScript, including undo/redo functionality, game checkpoints, and state management.

6.6.3 Use Cases and Examples

The Memento Pattern is a behavioral design pattern that allows you to capture and externalize an object’s internal state so that the object can be restored to this state later without violating encapsulation. This pattern is particularly useful in scenarios where you need to provide undo/redo functionality, manage state transitions, or save checkpoints in applications like games or text editors.

Understanding the Memento Pattern

Before diving into specific use cases, let’s briefly revisit the core components of the Memento Pattern:

  1. Originator: The object whose state needs to be saved and restored.
  2. Memento: A representation of the Originator’s state at a particular point in time.
  3. Caretaker: Manages the history of Mementos and is responsible for storing and restoring the Originator’s state.

The Memento Pattern is particularly powerful because it allows you to preserve the encapsulation of the Originator. The Memento object is opaque to the Caretaker, which means the Caretaker cannot modify the Originator’s state directly.

Use Case 1: Undo/Redo Functionality in Text Editors

One of the most common applications of the Memento Pattern is implementing undo/redo functionality in text editors or drawing applications. This feature allows users to revert changes and explore different editing paths without losing previous work.

Implementation Example

Let’s implement a simple text editor in TypeScript that supports undo and redo operations using the Memento Pattern.

 1// The Memento class stores the state of the TextEditor.
 2class TextEditorMemento {
 3  constructor(private state: string) {}
 4
 5  getState(): string {
 6    return this.state;
 7  }
 8}
 9
10// The Originator class represents the TextEditor.
11class TextEditor {
12  private content: string = '';
13
14  write(text: string): void {
15    this.content += text;
16  }
17
18  save(): TextEditorMemento {
19    return new TextEditorMemento(this.content);
20  }
21
22  restore(memento: TextEditorMemento): void {
23    this.content = memento.getState();
24  }
25
26  getContent(): string {
27    return this.content;
28  }
29}
30
31// The Caretaker class manages the history of Mementos.
32class Caretaker {
33  private mementos: TextEditorMemento[] = [];
34  private currentIndex: number = -1;
35
36  addMemento(memento: TextEditorMemento): void {
37    this.mementos.splice(this.currentIndex + 1);
38    this.mementos.push(memento);
39    this.currentIndex++;
40  }
41
42  undo(): TextEditorMemento | null {
43    if (this.currentIndex <= 0) {
44      return null;
45    }
46    this.currentIndex--;
47    return this.mementos[this.currentIndex];
48  }
49
50  redo(): TextEditorMemento | null {
51    if (this.currentIndex + 1 >= this.mementos.length) {
52      return null;
53    }
54    this.currentIndex++;
55    return this.mementos[this.currentIndex];
56  }
57}
58
59// Usage
60const editor = new TextEditor();
61const caretaker = new Caretaker();
62
63editor.write('Hello');
64caretaker.addMemento(editor.save());
65
66editor.write(' World');
67caretaker.addMemento(editor.save());
68
69console.log(editor.getContent()); // Output: Hello World
70
71editor.restore(caretaker.undo());
72console.log(editor.getContent()); // Output: Hello
73
74editor.restore(caretaker.redo());
75console.log(editor.getContent()); // Output: Hello World

In this example, the TextEditor class acts as the Originator, the TextEditorMemento class represents the Memento, and the Caretaker class manages the history of Mementos. This setup allows the text editor to revert to previous states seamlessly.

Challenges

  • Memory Usage: Storing a large number of Mementos can consume significant memory, especially if the state is large. Consider implementing strategies to limit the history size or compress the state.
  • Complex State: If the state of the Originator is complex, you might need to carefully design the Memento to capture all necessary information without exposing internal details.

Use Case 2: Saving Checkpoints in Games

In gaming applications, the Memento Pattern can be used to save checkpoints, allowing players to revert to previous states. This is particularly useful in complex games where players might want to retry a level or recover from a mistake.

Implementation Example

Let’s consider a simple game scenario where a player’s position and score are saved as checkpoints.

 1// The Memento class stores the state of the Game.
 2class GameMemento {
 3  constructor(private position: string, private score: number) {}
 4
 5  getPosition(): string {
 6    return this.position;
 7  }
 8
 9  getScore(): number {
10    return this.score;
11  }
12}
13
14// The Originator class represents the Game.
15class Game {
16  private position: string = 'Start';
17  private score: number = 0;
18
19  play(position: string, score: number): void {
20    this.position = position;
21    this.score = score;
22  }
23
24  save(): GameMemento {
25    return new GameMemento(this.position, this.score);
26  }
27
28  restore(memento: GameMemento): void {
29    this.position = memento.getPosition();
30    this.score = memento.getScore();
31  }
32
33  getStatus(): string {
34    return `Position: ${this.position}, Score: ${this.score}`;
35  }
36}
37
38// The Caretaker class manages the history of Mementos.
39class GameCaretaker {
40  private mementos: GameMemento[] = [];
41  private currentIndex: number = -1;
42
43  addMemento(memento: GameMemento): void {
44    this.mementos.splice(this.currentIndex + 1);
45    this.mementos.push(memento);
46    this.currentIndex++;
47  }
48
49  undo(): GameMemento | null {
50    if (this.currentIndex <= 0) {
51      return null;
52    }
53    this.currentIndex--;
54    return this.mementos[this.currentIndex];
55  }
56
57  redo(): GameMemento | null {
58    if (this.currentIndex + 1 >= this.mementos.length) {
59      return null;
60    }
61    this.currentIndex++;
62    return this.mementos[this.currentIndex];
63  }
64}
65
66// Usage
67const game = new Game();
68const gameCaretaker = new GameCaretaker();
69
70game.play('Level 1', 100);
71gameCaretaker.addMemento(game.save());
72
73game.play('Level 2', 200);
74gameCaretaker.addMemento(game.save());
75
76console.log(game.getStatus()); // Output: Position: Level 2, Score: 200
77
78game.restore(gameCaretaker.undo());
79console.log(game.getStatus()); // Output: Position: Level 1, Score: 100
80
81game.restore(gameCaretaker.redo());
82console.log(game.getStatus()); // Output: Position: Level 2, Score: 200

In this example, the Game class acts as the Originator, the GameMemento class represents the Memento, and the GameCaretaker class manages the history of Mementos. This allows the game to revert to previous checkpoints.

Challenges

  • Complex Game State: Games often have complex states involving multiple objects. Consider using multiple Mementos or a composite Memento to capture the entire game state.
  • Performance: Frequent state saving and restoring can impact performance. Optimize the Memento creation process to minimize overhead.

Use Case 3: Managing State Transitions in Workflow Applications

Workflow applications often involve complex state transitions, where the Memento Pattern can be used to manage and revert states as needed. This is particularly useful in applications where users need to backtrack or explore different workflow paths.

Implementation Example

Consider a workflow application where tasks can be moved between different states, such as “To Do,” “In Progress,” and “Completed.”

 1// The Memento class stores the state of the Workflow.
 2class WorkflowMemento {
 3  constructor(private state: string) {}
 4
 5  getState(): string {
 6    return this.state;
 7  }
 8}
 9
10// The Originator class represents the Workflow.
11class Workflow {
12  private state: string = 'To Do';
13
14  transitionTo(newState: string): void {
15    this.state = newState;
16  }
17
18  save(): WorkflowMemento {
19    return new WorkflowMemento(this.state);
20  }
21
22  restore(memento: WorkflowMemento): void {
23    this.state = memento.getState();
24  }
25
26  getState(): string {
27    return this.state;
28  }
29}
30
31// The Caretaker class manages the history of Mementos.
32class WorkflowCaretaker {
33  private mementos: WorkflowMemento[] = [];
34  private currentIndex: number = -1;
35
36  addMemento(memento: WorkflowMemento): void {
37    this.mementos.splice(this.currentIndex + 1);
38    this.mementos.push(memento);
39    this.currentIndex++;
40  }
41
42  undo(): WorkflowMemento | null {
43    if (this.currentIndex <= 0) {
44      return null;
45    }
46    this.currentIndex--;
47    return this.mementos[this.currentIndex];
48  }
49
50  redo(): WorkflowMemento | null {
51    if (this.currentIndex + 1 >= this.mementos.length) {
52      return null;
53    }
54    this.currentIndex++;
55    return this.mementos[this.currentIndex];
56  }
57}
58
59// Usage
60const workflow = new Workflow();
61const workflowCaretaker = new WorkflowCaretaker();
62
63workflow.transitionTo('In Progress');
64workflowCaretaker.addMemento(workflow.save());
65
66workflow.transitionTo('Completed');
67workflowCaretaker.addMemento(workflow.save());
68
69console.log(workflow.getState()); // Output: Completed
70
71workflow.restore(workflowCaretaker.undo());
72console.log(workflow.getState()); // Output: In Progress
73
74workflow.restore(workflowCaretaker.redo());
75console.log(workflow.getState()); // Output: Completed

In this example, the Workflow class acts as the Originator, the WorkflowMemento class represents the Memento, and the WorkflowCaretaker class manages the history of Mementos. This setup allows the workflow to revert to previous states seamlessly.

Challenges

  • State Complexity: Workflows can have complex states with multiple dependencies. Ensure that all necessary state information is captured in the Memento.
  • Concurrency: In multi-user environments, concurrent state changes can complicate Memento management. Consider implementing locking mechanisms or conflict resolution strategies.

Visualizing the Memento Pattern

To better understand how the Memento Pattern works, let’s visualize the interaction between the Originator, Memento, and Caretaker.

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

Diagram Description: This sequence diagram illustrates the interaction between the Originator, Memento, and Caretaker. The Originator creates a Memento to capture its state, which is then stored by the Caretaker. When needed, the Caretaker retrieves the Memento, allowing the Originator to restore its state.

Try It Yourself

Experiment with the Memento Pattern by modifying the code examples provided:

  • Extend the Text Editor: Add functionality to track cursor position or selected text, and update the Memento accordingly.
  • Enhance the Game: Include additional game attributes, such as player health or inventory, and ensure they are captured in the Memento.
  • Complex Workflow: Implement a more complex workflow with multiple tasks and dependencies, and manage their states using the Memento Pattern.

Considerations for Using the Memento Pattern

When considering the Memento Pattern for your application, keep the following points in mind:

  • Encapsulation: The Memento Pattern preserves the encapsulation of the Originator, making it a suitable choice for managing state without exposing internal details.
  • State Size: Be mindful of the size of the state being captured. Large states can lead to increased memory usage and performance overhead.
  • Complexity: The Memento Pattern can add complexity to your application, especially when dealing with complex states or concurrent modifications.

Knowledge Check

  • What is the primary benefit of using the Memento Pattern?
    • Preserving encapsulation while allowing state rollback.
  • How does the Caretaker interact with the Memento?
    • The Caretaker stores and retrieves Mementos but does not modify them.
  • What are some challenges associated with the Memento Pattern?
    • Memory usage, complex state management, and concurrency issues.

Conclusion

The Memento Pattern is a powerful tool for managing state transitions and providing undo/redo functionality in applications. By preserving encapsulation and allowing state rollback, it enables developers to create flexible and user-friendly applications. As you explore the Memento Pattern, consider its benefits and challenges, and experiment with different implementations to find the best fit for your needs.

Quiz Time!

Loading quiz…

Remember, this is just the beginning. As you progress, you’ll build more complex and interactive applications using the Memento Pattern. Keep experimenting, stay curious, and enjoy the journey!

Revised on Thursday, April 23, 2026