The Memento pattern claims to save an object's state 'without violating encapsulation'. Concretely, how is a memento kept opaque to the caretaker while still being fully readable by the originator?
answer
- narrow interface = caretaker, wide = originator
- nested class / internal / friend / closure
- immutable snapshot, no shared mutable refs
- restore validates provenance + version
- opacity is a contract, reflection still sees it
basics
~20 sThe memento offers two views: a narrow public one (basically just a handle, maybe a label or timestamp) used by the caretaker, and a wide one exposing the actual state that only the originator can reach — via a nested class, package/internal visibility, a private interface, or a language-specific friend mechanism.
solid answer
~50 sMemento defines two interfaces to the same object. The narrow interface is what the caretaker sees: often an empty marker type or one carrying only metadata (id, timestamp, label). The wide interface exposes the captured state and is reachable only by the originator. Enforcement mechanisms differ by language: a private/protected nested class inside the originator (Java, C#) so the outer class can read private members while outsiders see only the public supertype; package-private or Kotlin internal visibility so only the originator's package/module can read fields; C++ friend declarations; in dynamic languages, closures capturing state, or convention plus an opaque token. Two extra rules matter: the memento should be immutable so a stored snapshot cannot drift or be tampered with, and restore() should validate that the memento actually came from this originator (type or identity check), otherwise a caretaker could inject a foreign snapshot and break invariants. Absolute enforcement is rarely possible against reflection or serialization; the goal is a clear, checkable contract.
code
pseudocode · 14 linesinterface Snapshot { timestamp(): Instant } // narrow: all the caretaker sees
class Editor {
private class EditorMemento(text, cursor, ts) : Snapshot { // wide, nested & private
fun timestamp() = ts
}
fun save(): Snapshot = EditorMemento(text, cursor, now())
fun restore(s: Snapshot) {
val m = s as? EditorMemento ?: throw IllegalArgumentException("foreign snapshot")
text = m.text; cursor = m.cursor
}
}go deeper
Say the caretaker sees only an opaque handle while the originator can read the state, and give one mechanism (nested class or internal visibility).
Name narrow vs wide interface explicitly, give two or three enforcement mechanisms across languages, and mention immutability.
Add provenance/version checking in restore(), defensive copying depth, thread-safety implications of immutable snapshots, and the ceremony-vs-strictness trade-off.
Discuss opacity as a reviewable contract rather than a wall, schema evolution for persisted mementos, and the data-governance angle when snapshots contain sensitive state (retention, logging, encryption).
## Encapsulation, briefly Encapsulation means an object keeps its internal representation private and exposes only operations that preserve its invariants (rules that must always hold — e.g. `selectionStart <= selectionEnd`). If external code can read and write the fields directly, the object can no longer guarantee those rules, and you can never change the representation without breaking callers. Memento's whole selling point is doing snapshot/restore *without* giving that up. The technique is **two interfaces to the same object**. ## Narrow vs wide interface - **Narrow interface** — what the caretaker sees. Ideally nothing at all: an empty marker type, or at most non-sensitive metadata such as a creation timestamp, a human-readable label ("before paste"), or an identifier used for a redo list. The caretaker can store, order, count, and evict mementos, and hand one back — nothing else. - **Wide interface** — what the originator sees: the actual captured fields. The caretaker is compiled against the narrow view, so it cannot become coupled to the originator's internals even by accident. ## How languages enforce it | Mechanism | How it works | Notes | |---|---|---| | Private/protected **nested class** | `Editor.Memento` is declared inside `Editor`; the outer class may read its private fields, outsiders only see a public marker interface it implements | Classic Java/C# formulation | | **Package-private / `internal`** visibility | Fields or accessors are visible only within the originator's package or module | Simple; leaks to anything else in the same package/module | | **`friend`** declarations | The originator is declared a friend of the memento class | C++ | | **Closure capture** | `save()` returns a closure/thunk that, when invoked by the originator, replays state | Works in dynamic and functional languages; state is unreachable except through the closure | | **Opaque handle / token** | Memento is stored in a private table inside the originator; the caretaker receives only an id | Adds lifetime management (you must evict entries) | | **Sealed/opaque type + module boundary** | Only the declaring module can construct or destructure it | Kotlin `internal`, Rust module privacy, TypeScript branded types (compile-time only) | ## Two rules people forget **Immutability.** If the memento holds mutable references to the same objects the originator still mutates, the "snapshot" changes underneath you and undo restores the *current* state. Copy defensively (deep enough for the mutable parts), or capture persistent/immutable data structures. Immutability also makes the memento safe to share across threads and safe to keep in a redo stack. **Provenance checking.** `restore(m)` receives a memento from the outside. If a caretaker hands back a memento from a *different* originator, or a hand-crafted one, restoring it could install a state that violates invariants. Defensive originators check the runtime type and, where relevant, an owner identity or version stamp, and reject mismatches. A version/schema stamp also matters if mementos are persisted across releases: old snapshots may no longer describe the current state shape. ## Where it is only a contract, not a wall Reflection, serialization frameworks, debuggers, and memory inspection can all read a memento's private fields. So can anything in the same package when you rely on package-private visibility. Treat opacity as a *design contract* enforced as strongly as the language cheaply allows — the value is that no ordinary code path can couple to the internals, and reviewers can spot violations. If a memento holds secrets (credentials, personal data), opacity is not a security control: you still need the usual protections (avoid logging, zero/clear buffers, encrypt if persisted, bound retention). ## Trade-off worth naming The stricter the enforcement, the more awkward the code (nested classes, extra interfaces, generics that thread the memento type through the caretaker). Some teams deliberately settle for an immutable data class in the same module plus a review rule. That is a legitimate engineering choice — say so explicitly in an interview rather than pretending the pattern demands maximal ceremony.
- What can go wrong if the memento stores references to mutable objects that the originator keeps using?The snapshot aliases live state, so later mutations silently change what you saved and undo restores the current state instead of the old one. Fix it with a deep-enough copy at capture time, or by holding immutable/persistent data structures so sharing is safe.
- Should restore() accept any memento handed to it?No. It should verify the memento is one this originator produced — runtime type check plus, where snapshots are persisted or shared, an owner id and a schema/version stamp — and reject anything else, otherwise a caller can install a state that breaks the originator's invariants.
saying these in an interview costs you the question
- Believing private nested classes make a memento truly unreadable — reflection and serialization still get at it.
- Exposing full public getters on the memento 'for convenience', which recreates the coupling the pattern removes.
- Storing shared mutable references in the memento and calling it a snapshot.
- Skipping the type/ownership check in restore(), letting a foreign or forged memento violate invariants.
- Treating memento opacity as a security boundary for secrets held in the snapshot.