Memento Pattern
Overview
Returning to an earlier edit requires saved state. If history code reads and restores every editor field itself, an editor representation change also breaks history management.
A Memento holds a state snapshot. The originator saves and restores its own state, while a caretaker retains the snapshot without interpreting its contents.
UML
TextEditor creates a private Snapshot and returns ISnapshot. A caretaker retains that handle and returns it for restoration. TextEditor owns the state representation.
History
GoF cataloged Memento as a behavioral pattern in Design Patterns in 1994. It captures restorable state while preserving the object encapsulation.1
Why use it?
History management need not understand editor fields. The originator updates its save and restore operations when its state changes.
Retained snapshots consume storage, so define their lifetime and scope. Restoring an in-memory object does not undo already-sent mail or mutations in external storage.
Example
Save a draft, change the text and restore the snapshot.
using System;
using System.Collections.Generic;
using System.Linq;
var editor = new TextEditor();
editor.Write("draft");
ISnapshot saved = editor.Save();
editor.Write("edited");
Console.WriteLine(editor.Restore(saved));
Console.WriteLine(editor.Text);
interface ISnapshot { }
// Keep interpretation and restoration of saved state inside the original editor.
sealed class TextEditor
{
private readonly Guid id = Guid.NewGuid();
private sealed record Snapshot(Guid Owner, string Text) : ISnapshot
{
// A record's default ToString prints its property values, so hide them.
public override string ToString() => nameof(ISnapshot);
}
public string Text { get; private set; } = "";
public void Write(string text) => Text = text;
// Expose the snapshot through its common interface; the original editor interprets its private representation.
public ISnapshot Save() => new Snapshot(id, Text);
public bool Restore(ISnapshot snapshot)
{
if (snapshot is not Snapshot saved || saved.Owner != id) return false;
Text = saved.Text;
return true;
}
}
True
draftThe caller cannot read text through ISnapshot. TextEditor interprets its own snapshot.
An extended example
Reject a snapshot supplied to the wrong editor by checking the originator identity.
using System;
using System.Collections.Generic;
using System.Linq;
var editor = new TextEditor();
editor.Write("draft");
ISnapshot saved = editor.Save();
var other = new TextEditor();
other.Write("other");
Console.WriteLine(other.Restore(saved));
Console.WriteLine(other.Text);
editor.Write("edited");
Console.WriteLine(editor.Restore(saved));
Console.WriteLine(editor.Text);
interface ISnapshot { }
// Keep interpretation and restoration of saved state inside the original editor.
sealed class TextEditor
{
private readonly Guid id = Guid.NewGuid();
private sealed record Snapshot(Guid Owner, string Text) : ISnapshot
{
// A record's default ToString prints its property values, so hide them.
public override string ToString() => nameof(ISnapshot);
}
public string Text { get; private set; } = "";
public void Write(string text) => Text = text;
// Expose the snapshot through its common interface; the original editor interprets its private representation.
public ISnapshot Save() => new Snapshot(id, Text);
public bool Restore(ISnapshot snapshot)
{
if (snapshot is not Snapshot saved || saved.Owner != id) return false;
Text = saved.Text;
return true;
}
}
False
other
True
draftA snapshot from another originator returns false and leaves the receiver unchanged. This example restores one in-memory string; persisted snapshot formats and version compatibility require their own design.
TypeScript example
History management retains snapshots, while TextEditor owns their contents and restoration. This preserves past state without exposing the editor’s internal text to the caretaker.
type ISnapshot = Readonly<{ kind: "snapshot" }>;
// Keep interpretation and restoration of saved state inside the original editor.
class TextEditor {
#text = "";
readonly #snapshots = new WeakMap<ISnapshot, string>();
get text(): string { return this.#text; }
write(text: string): void { this.#text = text; }
// Return only a storage token and retain the actual text privately.
save(): ISnapshot {
const token = Object.freeze({ kind: "snapshot" as const });
this.#snapshots.set(token, this.#text);
return token;
}
restore(snapshot: ISnapshot): boolean {
const text = this.#snapshots.get(snapshot);
if (text === undefined) return false;
this.#text = text;
return true;
}
}
const editor = new TextEditor();
editor.write("draft");
const saved = editor.save();
editor.write("new");
console.log(editor.restore(saved));
console.log(editor.text);
console.log(new TextEditor().restore(saved));true
draft
falseAn immutable token and an editor-private WeakMap hide snapshot contents in this version. A look-alike token or a token from another editor cannot restore its state.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Memento. ↩