Memento
The Memento Pattern is a behavioral design pattern that allows an object to save its state so that it can be restored to this state later. This pattern is particularly useful for implementing undo mechanisms or for saving and restoring the state of an object.
Here's a basic outline of the Memento Pattern:
Memento Pattern in the Wikipedia
-
Originator: The object whose state needs to be saved. It creates a memento containing a snapshot of its current internal state and uses the memento to restore its internal state.
-
Memento: A lightweight object that stores the internal state of the Originator. It should have two interfaces: a wide one for the Originator, allowing it to access all the necessary data to restore its state, and a narrow one for other objects, preventing them from accessing the Originator’s internal state stored in the memento.
-
Caretaker: The object that keeps track of multiple mementos. The Caretaker requests a memento from the Originator when it needs to save the state and provides the memento back to the Originator when the state needs to be restored.
In TypeScript, you would typically implement the Memento Pattern by creating classes for each of these components. The Originator class would have methods for saving and restoring its state, the Memento class would be used to encapsulate the state, and the Caretaker would manage the mementos.
Implementation Example​
Let's create a simple TypeScript example to illustrate the Memento Pattern. In this example, we'll have an Editor
class as the Originator, which can write and erase text. We'll create a Snapshot
class as the Memento, which stores the state of the Editor
. Lastly, the History
class will act as the Caretaker, managing the snapshots.
-
Editor (Originator): This class has a method to write text and a method to erase the last character. It also has methods to save its state and restore a previous state.
-
Snapshot (Memento): This class holds the state of the
Editor
. It's a simple class with just a constructor and a getter method. -
History (Caretaker): This class maintains a history of
Snapshots
. It can add new snapshots and retrieve the latest one.
Here's the TypeScript implementation:
class Editor {
private content: string = '';
public write(words: string): void {
this.content += words;
}
public erase(): void {
this.content = this.content.slice(0, -1);
}
public save(): Snapshot {
return new Snapshot(this.content);
}
public restore(snapshot: Snapshot): void {
this.content = snapshot.getContent();
}
public getContent(): string {
return this.content;
}
}
class Snapshot {
private readonly state: string;
constructor(state: string) {
this.state = state;
}
public getContent(): string {
return this.state;
}
}
class History {
private snapshots: Snapshot[] = [];
public saveSnapshot(snapshot: Snapshot): void {
this.snapshots.push(snapshot);
}
public getLastSnapshot(): Snapshot | undefined {
return this.snapshots.pop();
}
}
// Usage
const editor = new Editor();
const history = new History();
editor.write('Hello, ');
history.saveSnapshot(editor.save());
editor.write('World!');
history.saveSnapshot(editor.save());
console.log(editor.getContent()); // Output: Hello, World!
// Undo last action
editor.restore(history.getLastSnapshot()!);
console.log(editor.getContent()); // Output: Hello,
In this example, every time the state of the Editor
changes, we save a snapshot of its state in History
. When we need to undo a change, we restore the Editor
's state from the last snapshot saved in History
.
This pattern is useful in scenarios where you need to track changes and possibly revert to a previous state, such as text editors, game state management, or in transactional systems.
Combining Memento with...​
The Memento Pattern can be effectively combined with several other design patterns, depending on the specific requirements of your application. Here are some patterns that often work well with Memento:
-
Command Pattern: The Command Pattern encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations. It's commonly used for implementing undo/redo functionalities in applications. By pairing it with the Memento Pattern, each command can be responsible for saving the state before executing an operation and restoring it if undo is required.
-
Iterator Pattern: The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. When combined with Memento, it can be used to traverse a complex structure (like a tree or graph) and restore to previous states at specific points in the iteration.
-
State Pattern: The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. When used with Memento, it's possible to save and restore states of the context object, especially in scenarios where state transitions are complex or involve many different variables.
-
Prototype Pattern: The Prototype Pattern is used to create duplicate objects while keeping performance in mind. This can be useful in conjunction with Memento when the state to be saved is complex and you want to avoid deep copying every time you create a memento. Instead, you could prototype the memento.
-
Strategy Pattern: The Strategy Pattern is used to create an interchangeable family of algorithms from which the required process is chosen at runtime. When used with Memento, different strategies can leverage state snapshots for various purposes, like different undo/redo strategies in a text editor.
-
Observer Pattern: The Observer Pattern is used when there is one-to-many relationship between objects such as if one object is modified, its dependent objects are to be notified automatically. Memento can be used along with Observer to keep track of the state changes and notify observers whenever there's a state rollback or forward.
Combining patterns depends greatly on the specific problems you're trying to solve. It's important to ensure that the combination of patterns doesn't lead to overly complex or hard-to-maintain code. Remember, design patterns are tools to solve common problems in software design, but they're not one-size-fits-all solutions. The context of your specific project should guide how and which patterns to use.
FP approach​
The Memento Pattern can be implemented in a functional programming (FP) style, though it's inherently different from the object-oriented approach due to the stateless nature of FP. In functional programming, instead of modifying an object's state, you work with immutable data structures and pure functions.
In a functional style, the Memento Pattern might not be recognized as such, because the pattern relies heavily on object state and object references, which are not common in FP. However, the core concept of saving and restoring state can still be applied. Here's how you might approach it:
-
Immutable State: Use immutable data structures to represent the state of your system. Instead of changing the state, you always produce a new state based on the previous one.
-
Functions for State Transformation: Create functions that take the current state (and other inputs) and return a new state. These functions are pure and have no side effects.
-
History Management: Maintain a list (stack) of states. This list can be used to implement undo functionality, where you can move back to the previous state by popping the latest state off the stack.
Here is a simplistic TypeScript example demonstrating this approach:
type EditorState = string;
function write(currentState: EditorState, newText: string): EditorState {
return currentState + newText;
}
function erase(currentState: EditorState): EditorState {
return currentState.slice(0, -1);
}
function save(state: EditorState, history: EditorState[]): EditorState[] {
return [state, ...history];
}
function undo(history: EditorState[]): [EditorState | undefined, EditorState[]] {
if (history.length === 0) return [undefined, history];
const [lastState, ...newHistory] = history;
return [lastState, newHistory];
}
// Usage
let currentState = "Hello, ";
let history: EditorState[] = [];
history = save(currentState, history); // Save current state
currentState = write(currentState, "World!"); // Modify state
console.log(currentState); // "Hello, World!"
// Undo
let result = undo(history);
currentState = result[0] ?? currentState;
history = result[1];
console.log(currentState); // "Hello, "
In this example, write
and erase
are pure functions that take a state and return a new state. The save
function is used to keep a history of states. The undo
function allows you to revert to the previous state.
This FP approach aligns well with the principles of immutability and pure functions, and it can be particularly useful in contexts like Redux in React development, where you manage the application state in a functional and predictable way.