โช Memento Pattern: Pro-Pixel Undo History
๐ Overview
The Memento Pattern provides the ability to capture and externalize an object's internal state so that it can be restored later, without violating encapsulation. It is the architectural backbone of "Undo/Redo" functionality.
Core Concepts
- Snapshot: Capturing the exact state of an object at a specific point in time.
- Encapsulation: The object's internal details (private fields) are saved but remains hidden from the rest of the system.
- History Management: A separate caretaker manages the timeline of snapshots.
๐ญ The Engineering Story & Problem
๐ก The Villain (The Problem)
You're building a professional text editor. You need an "Undo" feature.
The "Snapshot Leaker" approach is to let the HistoryManager read the Editor's private variables (_text, _cursor, _scroll_y) and copy them into a list.
This creates Tight Coupling. The HistoryManager now depends on the internal implementation of the Editor. If you refactor the Editor to use a GapBuffer instead of a string, the HistoryManager breaks. You've leaked the internal state, violating encapsulation.
๐ฆธ The Hero (The Solution)
The Memento Pattern solves this by creating a "Sealed Envelope" (the Memento).
The Editor (Originator) packs its own state into a Memento object. It hands this sealed envelope to the HistoryManager (Caretaker). The HistoryManager keeps it safe but cannot open it.
When the user hits Undo, the HistoryManager hands the envelope back to the Editor. The Editor opens it and restores its state. The HistoryManager never saw the data, preserving encapsulation.
๐ Requirements & Constraints
- (Functional): implement a text editor with Undo functionality.
- (Technical): The
Historyclass must treat Mementos as opaque objects (no access to fields). - (Technical): Mementos must be immutable deep copies of the state.
๐๏ธ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class Editor {
-content: string
-cursor: int
+type(text)
+save() Memento
+restore(memento)
}
class Memento {
-content: string
-cursor: int
+get_state()
}
class History {
-timeline: List~Memento~
+push(memento)
+undo()
}
Editor ..> Memento : Creates
History o-- Memento : Manages
Runtime Context (Sequence)
sequenceDiagram
participant User
participant History
participant Editor
User->>Editor: type("Hello")
User->>History: push(editor.save())
History->>Editor: save()
Editor-->>History: Memento A
User->>Editor: type(" World")
User->>History: undo()
History->>Editor: restore(Memento A)
Editor->>Editor: content = "Hello"
๐ป Implementation & Code
๐ง SOLID Principles Applied
- Single Responsibility:
Editorhandles editing;Historyhandles storage. - Open/Closed: The
Historymechanism works for any object that implements the Memento pattern.
๐ The Code
The Villain's Code (Without Pattern)
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.stack: list[Memento] = []
self.redo_stack: list[Memento] = []
def save(self, memento: Memento) -> None:
self.stack.append(memento)
self.redo_stack = []
def undo(self, current_memento: Memento) -> Optional[Memento]:
if not self.stack: return None
self.redo_stack.append(current_memento)
return self.stack.pop()
def redo(self, current_memento: Memento) -> Optional[Memento]:
if not self.redo_stack: return None
self.stack.append(current_memento)
return self.redo_stack.pop()
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 type(self, text: str) -> None:
self.content += text
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:
state = self.history.undo(self.create_snapshot())
if state:
self.__dict__.update(state.state)
print("Restoring")
print(self.content)
def unrestore(self) -> None:
state = self.history.redo(self.create_snapshot())
if state:
self.__dict__.update(state.state)
print("Redoing")
print(self.content)
history = History()
editor = Editor(history)
editor.type("Hello")
history.save(editor.create_snapshot())
editor.type(" World")
history.save(editor.create_snapshot())
editor.type("!")
editor.restore()
editor.restore()
editor.unrestore()
editor.unrestore()
# class NHistory:
# def __init__(self) -> None:
# self.history: list[Memento] = []
# self.future: list[Memento] = []
# def save(self, snapshot: dict[str, Any]) -> None:
# memento = Memento(snapshot)
# self.history.append(memento)
# self.future = []
# def undo(self) -> Optional[dict[str, Any]]:
# if self.history != []:
# self.future.append(self.history[-1])
# snapshot = self.history.pop().__dict__.copy()
# snapshot["content"] = snapshot["_Memento__content"]
# del snapshot["_Memento__content"]
# snapshot["cursor"] = snapshot["_Memento__cursor"]
# del snapshot["_Memento__cursor"]
# snapshot["selection"] = snapshot["_Memento__selection"]
# del snapshot["_Memento__selection"]
# return snapshot
# def redo(self) -> Optional[dict[str, Any]]:
# if self.future != []:
# snapshot = self.future.pop().__dict__.copy()
# snapshot["content"] = snapshot["_Memento__content"]
# del snapshot["_Memento__content"]
# snapshot["cursor"] = snapshot["_Memento__cursor"]
# del snapshot["_Memento__cursor"]
# snapshot["selection"] = snapshot["_Memento__selection"]
# del snapshot["_Memento__selection"]
# return snapshot
# class NEditor:
# def __init__(self, history: History):
# self.content: str = ""
# self.cursor: int = 0
# self.selection: tuple = (0, 0)
# self.history: History = history
# def type(self, text: str) -> None:
# self.content += text
# self.history.future = []
# print(self.content)
# def create_snapshot(self) -> dict[str, Any]:
# return self.__dict__.copy()
# def restore(self, history: Optional[dict[str, Any]]) -> None:
# if history:
# self.__dict__.update(history)
# print("Restoring")
# print(self.content)
# def unrestore(self, history: Optional[dict[str, Any]]) -> None:
# if history:
# self.__dict__.update(history)
# print("Redo")
# print(self.content)
# history = History()
# editor = Editor(history)
# editor.type("Hello")
# history.save(editor.create_snapshot())
# editor.type(" World")
# history.save(editor.create_snapshot())
# editor.type("!")
# editor.restore(history.undo())
# editor.restore(history.undo())
# editor.unrestore(history.redo())
# editor.unrestore(history.redo())
โ๏ธ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Encapsulation: State remains private. | RAM Usage: Saving full copies is memory intensive. |
| Separation: Originator and Caretaker are decoupled. | Complexity: Needs extra Memento classes. |
| Safety: Caretaker cannot corrupt the state. | Garbage Collection: Caretakers can keep obsolete states alive. |
๐งช Testing Strategy
- Unit Test Editor: Verify
type()changes state. - Unit Test Undo: Type "A", Save, Type "B", Undo. Verify state is "A".
- Test Immutability: Verify that changing the Memento object externally (if possible) doesn't change the Editor.
๐ค Interview Toolkit
- Interview Signal: Demonstrates understanding of encapsulation boundaries and restoration logic.
- When to Use:
- "Implement 'Ctrl+Z'..."
- "Save game progress..."
- "Transaction rollback..."
- Scalability Probe: "How to implement Undo for a 1GB file?" (Answer: Don't save the whole file. Save the diff or operation (Command pattern) to reverse it.)
- Design Alternatives:
- Command Pattern: Often preferred for "lightweight" undo (reversing the operation) vs Memento's "heavyweight" undo (restoring the state).