276 lines
9.0 KiB
Markdown
276 lines
9.0 KiB
Markdown
[← Design patterns index](README.md)
|
|
|
|
# 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` (implements `IOriginator`)
|
|
- **Memento** → `Originator.Memento`, a **private nested class** implementing
|
|
`IMemento`
|
|
- **Caretaker** → `Caretaker`, holding a `Stack<IMemento>`
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```csharp
|
|
// 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);
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// 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}");
|
|
}
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// 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.
|
|
|
|
```csharp
|
|
// 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)
|
|
|
|
- `TransactionScope` rollback 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](mediator.md) · [All patterns](README.md) · **Next →**
|
|
[Observer](observer.md)
|