The Memento Design Pattern is a powerful tool for implementing undo and redo functionality or otherwise managing the state of objects in a way that adheres to software design principles.
What is the Memento Pattern?
The Memento Pattern allows an object to save its internal state so that it can be restored to that state later. Often referred to as the “Token Pattern,” you can think of a memento as a save point in a video game or a particular commit in source code management.
Real-World Applications of the Memento Pattern
Here are some common scenarios where the Memento Pattern is applied:
- Save states in games: Allowing players to return to a previous point in the game.
- Undo operations: For example, in a drawing application where you need to revert changes.
- Rolling back distributed transactions: Similar to the undo operation but involving multiple systems with their own states.
The Problem the Memento Pattern Solves
Any situation where you need the ability to roll back an object to its previous state can benefit from the Memento Pattern. If your application requires undo functionality, the Memento Pattern is likely a good fit, as it adds this capability without overburdening existing objects. This helps maintain adherence to the Single Responsibility Principle.
For primitive types like strings, where undoing changes might be straightforward, the Memento Pattern might be overkill. However, for complex objects, attempting to roll back a state without this pattern would require exposing the internal state to external components—violating encapsulation. The Memento Pattern allows you to capture and restore the internal state of an object without breaking encapsulation or compromising the Single Responsibility Principle.
Structure of the Memento Pattern
The Memento Pattern is composed of three main parts:
1. The Originator
The object whose state is being tracked (e.g., a document, game, etc.).
2. The Caretaker
The external object that interacts with the Originator (e.g., a UI, console app, etc.). The Caretaker manages the state of the Originator by performing operations and storing its states.
3. The Memento
The Memento is the key component of this design pattern. It captures and stores the internal state of the Originator. This state is then used to restore the Originator to its previous condition.
The Memento must hold the complete state of the Originator so that the Originator can be fully restored to a previous state using it.
UML Class Diagram
In this diagram:
- The
state
is private, maintaining encapsulation and separation from the rest of the application. - The
SetMemento()
method restores the Originator’s state. - `CreateMemento() allows you to create a Memento from the current state of the Originator.
The Originator directly interacts with the Memento, creating it, setting its internal state, and reading that state back as needed. Access to the Memento’s state should ideally be restricted to the Originator to maintain encapsulation.
The Caretaker is responsible for saving the Memento, either in memory or a persistent store if serialization is supported. Any user interaction for selecting or organizing Mementos is handled by the Caretaker.
How the Memento Pattern Works
When a user requests to save the state of the system:
- The Caretaker calls the Originator.
- The Originator creates a new Memento and sets its state.
Later, when the user requests to restore the system’s state:
- The Caretaker calls the Originator’s SetMemento() method, passing in the desired Memento.
- The Originator retrieves the state from the Memento and updates its current state accordingly.
Key Points to Remember
- The Memento should be simple: just an internal state with methods to get and set that state.
- The Originator must support methods to create and restore Mementos.
- The Caretaker manages the previous states.
- To preserve encapsulation, avoid giving the Caretaker direct access to the internal state of the Memento or the Originator.
Managing Mementos in the Caretaker
A straightforward way to manage Mementos in the Caretaker is by using a stack.
For example, say:
- State 1 is created and added to the stack.
- State 2 is created and added to the stack.
- State 3 is created and added to the stack.
- State 4 is the current state of the system.
To restore the system to a previous state, simply pop the last state from the stack (State 3) and make it the current state of the system. In applications that support only undo, there is no way to save State 4; it is replaced by State 3.
Supporting Redo Operations
Supporting redo is easy once you have undo set up. Instead of deleting the current state when undoing, save that state to a redo stack.
For example:
- With the system in State 4, undo moves State 4 to the redo stack and makes State 3 the current state.
- If undo is performed again, State 3 is pushed to the redo stack, and State 2 becomes the current state.
To redo:
- State 2 is moved from the current state to the undo stack.
- State 3 is popped from the redo stack and becomes the current state.
Implementing Undo and Redo Operations
When implementing these operations, ensure the Caretaker does the following:
- Store states (Mementos) on an undo stack.
- After each action, add a new Memento to the undo stack.
- On undo, pop the previous Memento from the undo stack and add it to the redo stack.
- On redo, pop the previous Memento from the redo stack and add it to the undo stack.
Remember, Mementos should be immutable objects that encapsulate state without any behavior.
Steps to Apply the Memento Pattern
- Follow refactoring fundamentals to ensure you don’t introduce bugs.
- Regardless of whether you have a state management system built in, define a Memento type. Keep it simple and ensure its accessors are only available to the Originator to preserve encapsulation.
- Add methods to the Originator to save and restore state.
- Create the Caretaker, which will manage the Mementos. Identify the class responsible for this and create any necessary structures to store one or more Mementos.
Alternative Approaches
Reverse Operations
Instead of storing the entire state, you can store the operations and their corresponding reverse operations. This is often used in conjunction with the Command Pattern.
For example, in a calculator:
- Start with 30.
- Add 5 to get 35.
- Multiply by 2 to get 70.
To undo:
- Retrieve the last operation (*2) and apply its reverse (÷2) to get 35.
- Retrieve the next operation (+5) and apply its reverse (-5) to get 30.
This approach works well when operations have clear, consistent reversals. However, it can break down when reversals are ambiguous, as in squaring operations.
Storing Diffs
Another approach is to store only the differences (diffs) between states rather than entire states, similar to how version control systems like Git work.
This approach works well for small changes and short history spans. However, it can be resource-intensive if generating diffs or applying multiple diffs in succession requires more computational resources than simply copying the entire state.
Related Patterns
-
Command Pattern: This can be used as an alternative to storing full states. Instead of the Caretaker managing states, it manages a series of commands. Reverse commands are applied to revert to a previous state, as shown in the calculator example.
-
Iterator Pattern: In cases where iterators need to store state for individual iterations, they can leverage the Memento Pattern, keeping this logic separate from the iterator itself.
Key Takeaways
- The Memento Design Pattern stores the state of an Originator object and removes state management responsibility from the Originator to the Caretaker.
- It is commonly used for game save points, undo support, and undo/redo support.
- The pattern enforces the Single Responsibility Principle by offloading state management and preserves encapsulation by protecting the Originator’s internal state.