Add code examples, and documentation
This commit is contained in:
275
docs/memento.md
Normal file
275
docs/memento.md
Normal file
@@ -0,0 +1,275 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user