Memento — Find the Bug¶
Each section presents a Memento that looks fine but is broken. Find the bug yourself, then check.
Table of Contents¶
- Bug 1: Mutable reference in Memento
- Bug 2: Caretaker reads Memento internals
- Bug 3: Mutable Memento
- Bug 4: Redo stack not cleared on new action
- Bug 5: Unbounded history leaks memory
- Bug 6: Memento captures resource handle
- Bug 7: Concurrent capture races
- Bug 8: Schema breakage on deserialization
- Bug 9: Memento exposes sensitive data in logs
- Bug 10: Half-restore corrupts state
- Bug 11: Diff-based Memento applied out of order
- Bug 12: Memento captures wrong scope
- Practice Tips
Bug 1: Mutable reference in Memento¶
public final class Editor {
private final List<String> lines = new ArrayList<>();
public Memento save() { return new Memento(lines); } // BUG: shares the same list
public static class Memento {
private final List<String> lines;
Memento(List<String> l) { this.lines = l; }
}
}
After save, modifying the editor changes the Memento too.
Reveal
**Bug:** The Memento stores a reference to the same `lines` list. When the editor mutates `lines`, the Memento sees the change. **Fix:** deep-copy on save. Or use immutable lists: **Lesson:** Mementos must capture *values*, not references to mutable objects. Either copy or use immutable data.Bug 2: Caretaker reads Memento internals¶
public final class History {
private final List<Memento> stack = new ArrayList<>();
public void log(Memento m) {
// BUG: reaches into Memento
System.out.println("snapshot: " + m.content + " @ " + m.cursor);
}
}
Reveal
**Bug:** The Caretaker reads Memento's internal fields. Encapsulation broken; refactoring the Originator's state shape breaks the Caretaker. **Fix:** Caretaker only stores; doesn't read. If logging is needed, the Originator can produce a `String` representation: Or expose a public `summary()` on Memento that the Originator controls. **Lesson:** Caretakers are storage only. Reading defeats the encapsulation contract.Bug 3: Mutable Memento¶
class Memento:
def __init__(self, content):
self.content = content # mutable
m = editor.save()
m.content = "tampered"
editor.restore(m) # restores tampered state
Reveal
**Bug:** Memento is mutable. After capture, anyone can modify it. The "snapshot" is no longer a snapshot. **Fix:** make Mementos immutable. Or in JS, freeze: **Lesson:** Mementos are values. Once created, they don't change. Use immutability primitives.Bug 4: Redo stack not cleared on new action¶
public final class History {
public void execute(Command c) {
c.execute();
undo.push(c);
// BUG: redo not cleared
}
}
Steps: do A, undo, do B, redo. Redo replays A — but A was undone and B is current.
Reveal
**Bug:** New `execute` doesn't clear the redo stack. After "undo + new execute," redo holds stale commands. **Fix:** **Lesson:** Redo is valid only after an undo, before any new execute. Branching the history breaks redo.Bug 5: Unbounded history leaks memory¶
After hours of editing, OOM.
Reveal
**Bug:** No size limit. Mementos accumulate forever. **Fix:** cap with a deque. `deque(maxlen=N)` automatically evicts the oldest when full. **Lesson:** Histories must be bounded. Always.Bug 6: Memento captures resource handle¶
class FileEditor:
def __init__(self, path):
self.fd = open(path, 'r+') # open file
self.cursor = 0
def save(self) -> 'Memento':
return Memento(fd=self.fd, cursor=self.cursor)
After process restart, restoring a serialized Memento has a stale fd.
Reveal
**Bug:** Memento captures a file descriptor. On restart, the `fd` is invalid (or could refer to a different file). **Fix:** capture identifiers, reacquire resources on restore. **Lesson:** Mementos capture state, not resources. Strip transient resources; reacquire on restore.Bug 7: Concurrent capture races¶
public final class Counter {
private int count;
private String label;
public Memento save() {
return new Memento(count, label); // not atomic
}
}
Thread A calls save(). Thread B updates count. Snapshot has new count + old label (or vice versa).
Reveal
**Bug:** Save reads multiple fields without synchronization. The Memento may capture inconsistent state. **Fix:** synchronize, or use immutable atomic state. Or immutable holder: **Lesson:** Multi-field captures need atomicity. Single AtomicReference + immutable record is the cleanest pattern.Bug 8: Schema breakage on deserialization¶
class Memento {
constructor(public count: number, public label: string) {}
serialize(): string { return JSON.stringify(this); }
static deserialize(s: string): Memento {
const data = JSON.parse(s);
return new Memento(data.count, data.label); // BUG: breaks if v1 has no label
}
}
Loading v1 data: data.label is undefined; Memento ends up with label = undefined.
Reveal
**Bug:** Deserializer assumes all fields present. V1 Mementos (before `label` was added) break. **Fix:** schema version + defaults. Always include version field. Always handle missing fields. **Lesson:** Persistent Mementos need schema versioning. Migrations live in the deserializer.Bug 9: Memento exposes sensitive data in logs¶
@dataclass
class UserMemento:
username: str
password: str
email: str
m = user.save()
logger.info(f"snapshot: {m}") # logs the password
Reveal
**Bug:** Default `__repr__` includes all fields. Password leaks into logs. **Fix:** custom `__repr__` that redacts sensitive fields. Or override: **Lesson:** Mementos can contain sensitive data. Mark explicitly; redact in logs.Bug 10: Half-restore corrupts state¶
public void restore(Memento m) {
this.title = m.title;
if (m.body == null) return; // BUG: early return; cursor not restored
this.body = m.body;
this.cursor = m.cursor;
}
Restoring a Memento with null body leaves the editor with new title but old body and cursor.
Reveal
**Bug:** Partial restore. Some fields set, others left at old values. State is now incoherent — neither the Memento's nor the original's. **Fix:** restore atomically. Either all fields set, or none. If null is valid, the Memento should NOT have null fields — they should be `Optional` or "absent" markers. **Lesson:** Restore is all-or-nothing. Validate Memento; restore all fields.Bug 11: Diff-based Memento applied out of order¶
class Document:
def revert(self, patches):
for p in patches: # BUG: forward order
setattr(self, p.field, p.old)
If patches are dependent, applying in forward order corrupts state.
Reveal
**Bug:** Diff patches must be reverted in **reverse** order. Applying them forward means later patches' "old" values are based on a state that doesn't exist anymore. **Fix:** **Lesson:** Diff-based undo applies patches in reverse order. Like nested function calls — last in, first out.Bug 12: Memento captures wrong scope¶
public final class Editor {
private String content;
private final List<Listener> listeners = new ArrayList<>(); // not part of "state"
public Memento save() {
return new Memento(this.content, this.listeners); // BUG: includes listeners
}
}
Restoring resets the listener list — observers are silently disconnected.
Reveal
**Bug:** Memento captures *all* fields, including transient infrastructure (listeners, locks, executors). Restoring corrupts observers. **Fix:** capture only "state," not "infrastructure." **Lesson:** Decide what's "state" (saved) vs "infrastructure" (kept). Mementos shouldn't disrupt the Originator's wiring.Practice Tips¶
- Mutable references in Mementos cause silent drift. Deep-copy or use immutable values.
- Caretaker reads = encapsulation broken. Caretaker stores opaque tokens only.
- Mementos must be immutable. Frozen / final / readonly.
- Redo invariant: cleared on new action. Always.
- Bound history. Always.
- Strip resources before saving. Capture identifiers; reacquire on restore.
- Concurrent captures need atomicity. Synchronize or use immutable atomic state.
- Persistent Mementos need versioning. Migrate in deserializer.
- Sensitive data → explicit redaction.
- Restore is all-or-nothing. Validate; set all fields.
- Diff-based undo: reverse order.
- Capture state, not infrastructure. Listeners, locks, executors stay.