Add code examples, and documentation

This commit is contained in:
Aaron Po
2026-08-12 00:03:32 -04:00
commit 77c16e69f6
79 changed files with 4458 additions and 0 deletions

275
docs/memento.md Normal file
View 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)