๐พ Memento Pattern: Advanced Undo/Redo System
๐ Overview
The Memento Pattern captures and externalizes an object's internal state so that the object can be restored to this state later. It allows you to implement Undo/Redo functionality without violating encapsulation (i.e., without exposing private fields to the outside world).
Core Concepts
- Originator: The object whose state needs to be saved (e.g., the Text Editor).
- Memento: An immutable object that acts as a snapshot of the Originator's state.
- Caretaker: The object that keeps track of the Mementos (e.g., a History stack) but never modifies them.
๐ญ The Engineering Story & Problem
๐ก The Villain (The Problem)
Imagine a "Complex Text Editor" with text content, cursor position, scroll position, and text selection. You want to implement Ctrl+Z (Undo).
The naive approach is to have a HistoryManager that forcefully reads the Editor's private variables (editor._content, editor._cursor_x) and saves them.
This breaks Encapsulation. If you change the Editor's internal implementation (e.g., renaming _cursor_x to _position), the HistoryManager breaks. The HistoryManager knows too much about the Editor.
๐ฆธ The Hero (The Solution)
The Memento Pattern introduces a "Black Box" snapshot.
The Editor (Originator) has a method save() that returns a Memento object containing a copy of its internal state. The HistoryManager (Caretaker) grabs this object and puts it in a stack. It cannot look inside the Memento; it just holds it.
When the user presses Undo, the HistoryManager pops the Memento and passes it back to the Editor.restore(memento). The Editor opens the box and restores its own state. The secrets remain with the Editor.
๐ Requirements & Constraints
- (Functional): Support unlimited Undo/Redo operations.
- (Technical): The Memento object must be immutable (state cannot be changed once saved).
- (Technical): The Caretaker must not have access to the internal data of the Memento.
๐๏ธ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class Editor {
-content: str
-cursor_x: int
+type(words)
+save() Memento
+restore(memento)
}
class EditorMemento {
-content: str
-cursor_x: int
+get_saved_content()
+get_saved_cursor()
}
class History {
-undo_stack: List
-redo_stack: List
+backup()
+undo()
}
Editor ..> EditorMemento : Creates
History o-- EditorMemento : Stores
EditorMemento -- Editor : Nested/Friend
Runtime Context (Sequence)
sequenceDiagram
participant User
participant History
participant Editor
User->>Editor: type("Hello")
User->>History: backup()
History->>Editor: save()
Editor-->>History: Memento(State A)
User->>Editor: type(" World")
User->>History: undo()
History->>Editor: restore(Memento A)
Editor->>Editor: self.content = "Hello"
๐ป Implementation & Code
๐ง SOLID Principles Applied
- Single Responsibility: The
Historyclass manages the stack; theEditormanages the state. - Open/Closed: You can add new state fields to
EditorandMementowithout changing theHistoryclass.
๐ The Code
The Villain's Code (Without Pattern)
class HistoryManager:
def backup(self, editor):
# ๐ก Breaks encapsulation! accessing private fields
self.snapshots.append({
"content": editor._hidden_content,
"cursor": editor._cursor_pos
})
def undo(self, editor):
state = self.snapshots.pop()
# ๐ก Direct injection of state
editor._hidden_content = state["content"]
The Hero's Code (With Pattern)
from types import MappingProxyType
from typing import Mapping, Optional
from copy import deepcopy
class Memento:
def __init__(self, state: dict) -> None:
self._state: MappingProxyType = MappingProxyType(state)
@property
def state(self) -> Mapping:
return self._state.copy()
class History:
def __init__(self) -> None:
self._undo: list[Memento] = []
self._redo: list[Memento] = []
def save(self, memento: Memento) -> None:
self._undo.append(memento)
self._redo.clear()
def undo(self, current_memento: Memento) -> Optional[Memento]:
memento = self._undo.pop() if self._undo else None
if memento:
self._redo.append(current_memento)
return memento
def redo(self, current_memento: Memento) -> Optional[Memento]:
memento = self._redo.pop() if self._redo else None
if memento:
self._undo.append(current_memento)
return memento
class Editor:
def __init__(self, history: History) -> None:
self.content: str = ""
self.cursor: int = 0
self.selection: tuple = (0, 0)
self.history: History = history
def _restore_from(self, memento: Memento) -> None:
self.content = memento.state["content"]
self.cursor = memento.state["cursor"]
self.selection = memento.state["selection"]
def begin_edit(self) -> None:
self.history.save(self.create_snapshot())
def type(self, text: str) -> None:
start, end = self.selection
if start != end:
self.content = self.content[:start] + text + self.content[end:]
self.cursor = start + len(text)
else:
self.content = self.content[:self.cursor] + text + self.content[self.cursor:]
self.cursor = self.cursor + len(text)
self.selection = (self.cursor, self.cursor)
print(self.content)
def create_snapshot(self) -> Memento:
state = {
"content": self.content,
"cursor": self.cursor,
"selection": self.selection
}
return Memento(state)
def restore(self) -> None:
memento = self.history.undo(current_memento=self.create_snapshot())
if memento:
self._restore_from(memento=memento)
print("Restoring")
print(self.content)
def unrestore(self) -> None:
memento = self.history.redo(current_memento=self.create_snapshot())
if memento:
self._restore_from(memento=memento)
print("Redoing")
print(self.content)
history = History()
editor = Editor(history)
editor.begin_edit()
editor.type("Hello")
editor.type(" World")
editor.cursor = 5
editor.type("X")
โ๏ธ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Encapsulation: Internal state stays private. | Memory Usage: Storing full copies of state can be expensive. |
| Simplified Originator: Doesn't need to manage history logic. | Complexity: Requires creating specific Memento classes. |
| Stability: History class isn't coupled to Originator fields. | Performance: Deep copying state (if required) takes time. |
๐งช Testing Strategy
- Unit Test Memento: Verify that a Memento object correctly holds the data it was given.
- Test Undo Logic: Perform actions, save state, perform more actions, undo, and verify the state is identical to the saved state.
- Test Immutability: Ensure that modifying the Memento after creation doesn't affect the Editor or vice-versa.
๐ค Interview Toolkit
- Interview Signal: mastery of state management, encapsulation, and undo architectures.
- When to Use:
- "Implement Undo/Redo..."
- "Save snapshots of a game state..."
- "Implement database transactions (rollback)..."
- Scalability Probe: "How to handle huge states (e.g., Photoshop)?" (Answer: Use "Deltas" or "Diffs" in the Memento instead of full copies. Or store Mementos on disk.)
- Design Alternatives:
- Command: Command Pattern can also support undo by having an
unexecute()method. Memento is better for "snapshotting" arbitrary state; Command is better for reversing specific operations.
- Command: Command Pattern can also support undo by having an