9.0 KiB
Memento
Category: Behavioral | Also known as: Snapshot | Source:
patterns/memento/MementoPattern/
Problem
You want undo. The obvious approach — some other object reads the target's fields and stores copies — only works if the target exposes its state publicly. That breaks encapsulation, and every later refactor of the target ripples out into whatever was copying its fields.
Solution
Let the state's owner (the Originator) produce its own opaque snapshot (the Memento) and restore itself from one later. A Caretaker holds mementos — in an undo stack — without being able to read or alter them. GoF calls this the narrow vs. wide interface: the caretaker sees a narrow interface (it can only hold and pass the memento along); the originator sees a wide one (full access to restore itself).
In languages with nested classes — "C++, C#, and Java", as Dive Into Design Patterns notes — the classic implementation nests the memento inside the originator, which is exactly what this project does.
Participants (GoF names → this repo)
- Originator →
Originator(implementsIOriginator) - Memento →
Originator.Memento, a private nested class implementingIMemento - Caretaker →
Caretaker, holding aStack<IMemento>
classDiagram
class IMemento {
<<interface>>
declaresNothing_theNarrowInterface
}
class IOriginator {
<<interface>>
+GetMemento() IMemento
+SetMemento(IMemento)
}
class Originator {
-Guid _id
+GetMemento() IMemento
+SetMemento(IMemento)
+UpdateId()
}
class Memento {
<<private nested>>
+Guid Id
}
class Caretaker {
-Stack~IMemento~ _mementos
-IOriginator _originator
+Save()
+Undo()
}
IOriginator <|.. Originator
Originator *-- Memento : nested, private
IMemento <|.. Memento
Caretaker --> IOriginator
Caretaker --> IMemento : holds, cannot read
Code
// IMemento.cs — the narrow interface. It declares NOTHING, on purpose:
// that is precisely what stops the Caretaker from reading the snapshot.
public interface IMemento
{
}
// IOriginator.cs
public interface IOriginator
{
IMemento GetMemento();
void SetMemento(IMemento memento);
}
// Originator.cs — Console.WriteLine calls kept, since the verified output depends on them
public class Originator : IOriginator
{
// Private + nested: only Originator can even name this type, let alone read .Id
private class Memento : IMemento
{
public Guid Id;
}
private Guid _id;
public Originator()
{
_id = Guid.NewGuid();
Console.WriteLine($"Starting ID: {_id}");
}
public IMemento GetMemento()
{
Memento memento = new Memento();
memento.Id = _id;
Console.WriteLine("Creating Memento...");
return memento; // handed out as IMemento — opaque to everyone else
}
public void SetMemento(IMemento memento)
{
if (!(memento is Memento)) // guard: reject foreign IMemento implementations
{
return;
}
Console.WriteLine("Restting ID..."); // [sic] typo is in the original
_id = (memento as Memento).Id; // downcast gives the WIDE interface
Console.WriteLine($"ID is reset to {_id}");
}
public void UpdateId()
{
Console.WriteLine("Updating Id...");
_id = Guid.NewGuid();
Console.WriteLine($"Updated ID is now {_id}");
}
}
// Caretaker.cs — holds history, never inspects it
public class Caretaker
{
// container that could hold memento of Originator object
private readonly Stack<IMemento> _mementos = new();
private readonly IOriginator _originator;
public Caretaker(IOriginator originator)
{
_originator = originator;
}
public void Save()
{
_mementos.Push(_originator.GetMemento());
}
public void Undo()
{
_originator.SetMemento(_mementos.Pop());
}
}
Caretaker cannot reach the Guid. It has no member that returns one, and
IMemento declares nothing — the compiler enforces the encapsulation, not a
convention.
// Program.cs — the walkthrough (prompts/ReadLine calls elided)
Originator originator = new Originator();
Caretaker caretaker = new Caretaker(originator);
caretaker.Save(); // stack: [id0]
originator.UpdateId(); // → id1 (never saved!)
originator.UpdateId(); // → id2
caretaker.Save(); // stack: [id0, id2]
originator.UpdateId(); // → id3
caretaker.Undo(); // pop id2 → current = id2 (id3 and id1 are gone)
originator.UpdateId(); // → id4
caretaker.Save(); // stack: [id0, id4]
originator.UpdateId(); // → id5
caretaker.Undo(); // pop id4 → current = id4
caretaker.Undo(); // pop id0 → current = id0 (stack now EMPTY)
Output (verified — GUIDs differ every run)
Starting ID: cdd24bbf-eac8-40b4-ae9a-9b09db9b2d7c
Creating Memento...
Updating Id...
Updated ID is now e9119540-d546-4c38-bd51-462f90df3e47
Updating Id...
Updated ID is now fcc5d975-3811-4f13-84ab-2768136a83c7
Creating Memento...
Updating Id...
Updated ID is now 02bafbee-f3e7-46d9-a6be-c757f4378595
Restting ID...
ID is reset to fcc5d975-3811-4f13-84ab-2768136a83c7 <- undo skipped 02bafbee
Updating Id...
Updated ID is now ca9f441d-cdf9-4ffa-9269-362291f33235
Creating Memento...
Updating Id...
Updated ID is now b8cfe4d8-de93-4fc9-839d-f63a0a12eb7b
Restting ID...
ID is reset to ca9f441d-cdf9-4ffa-9269-362291f33235
Restting ID...
ID is reset to cdd24bbf-eac8-40b4-ae9a-9b09db9b2d7c <- back to the very first ID
The two lessons the run makes concrete: undo returns to the last state that
was explicitly Save()d, not the previous value (e9119540 was never saved,
so nothing can ever get back to it); and a stack of mementos gives multi-level
undo for free — the last two Undo() calls walk all the way back to the
starting ID.
GoF pseudocode (text editor, with Command as the caretaker)
class Editor is // Originator
private field text, curX, curY, selectionWidth
method createSnapshot(): Snapshot is
// Memento is immutable: state passed once, via the constructor
return new Snapshot(this, text, curX, curY, selectionWidth)
class Snapshot is // Memento
private field editor: Editor
private field text, curX, curY, selectionWidth
method restore() is // memento restores its own originator
editor.setText(text)
editor.setCursor(curX, curY)
editor.setSelectionWidth(selectionWidth)
class Command is // Caretaker
private field backup: Snapshot
method makeBackup() is backup = editor.createSnapshot()
method undo() is if (backup != null) backup.restore()
This is the "even stricter encapsulation" variant: the memento links back to its
originator and owns restore(), so the caretaker never calls a setter on the
originator at all. The repo's version instead keeps restoration on the
originator (SetMemento) — the "intermediate interface" variant.
Applicability
- You must snapshot (part of) an object's state so you can restore it later — undo, but equally transaction rollback on error.
- Direct access to that state via getters/setters would expose implementation details and break encapsulation.
Pros / Cons
| Pros | Cons |
|---|---|
| Snapshots without violating encapsulation | Can consume lots of RAM if snapshots are frequent or large |
| Simplifies the Originator by moving history-keeping to the Caretaker | The caretaker must track the originator's lifecycle to destroy stale mementos |
| Multi-level undo falls out of using a stack | Dynamic languages (PHP/Python/JS) can't truly guarantee the memento stays untouched — C# can |
Seen in the wild (.NET)
TransactionScoperollback semantics.- EF Core's change tracker, which holds original values so
Reload()/rollback can restore them. - The undo stack in any editor control.
Related patterns
- Command + Memento is the classic undo pairing: the command captures a memento immediately before mutating the target, and restores it on undo.
- Iterator — a memento can capture iteration state, letting it roll back.
- Prototype is a simpler substitute when the object is straightforward and holds no external resource links.
← Prev: Mediator · All patterns · Next → Observer