COMMAND / QUICK REFERENCE
Package a request.
Give it a common lifecycle.
The invoker works with Command. The concrete command knows the receiver and arguments. Undo is an explicit capability in this example, not a guarantee of the pattern.
- What becomes an object?
- A request: the operation, its receiver and its arguments travel together.
- What does the invoker know?
- Only the Command contract. It can execute and retain requests without knowing how the receiver works.
- Where does undo come from?
- An explicit undo operation and enough saved state. Command does not automatically make every action reversible.
Who owns what?
- Client · editor controls
- Creates a fresh command with the receiver and arguments, then passes it to history.
- Invoker · CommandHistory
- Executes Command objects and manages undo/redo stacks without branching on edit types.
- Command · InsertText / ReplaceText
- Stores the request, delegates to the document and remembers the previous text for undo.
- Receiver · TextDocument
- Owns the actual text and performs writes. It does not manage undo stacks.
Undo/redo rules in this editor
- New edit: capture the previous text, execute, then push the command to Undo and clear Redo.
- Undo: restore the saved text and move the command from Undo to Redo.
- Redo: execute the same request and move it from Redo to Undo.
- No change or empty history: no new entry and no redo branch lost.
- One request, one object: create a fresh command for each new edit so earlier undo state is not overwritten.
This example assumes all document writes go through one local history. Concurrent or external writes can invalidate snapshots. Failures must occur before mutation or provide their own rollback; the invoker cannot make arbitrary side effects transactional.
The boundaries in code
Command contract · TypeScript
export interface Command {
readonly label: string;
execute(): boolean; // true only when the receiver changed
undo(): void;
}Concrete requests · TypeScript
export class InsertText implements Command {
readonly label = "Insert text";
private before: string | null = null;
private document: TextDocument;
private text: string;
constructor(document: TextDocument, text: string) {
this.document = document;
this.text = text;
}
execute() {
if (!this.text) return false;
this.before = this.document.text;
this.document.write(this.before + this.text);
return true;
}
undo() {
if (this.before === null) throw new Error("Execute before undo");
this.document.write(this.before);
}
}
export class ReplaceText implements Command {
readonly label = "Replace text";
private before: string | null = null;
private document: TextDocument;
private text: string;
constructor(document: TextDocument, text: string) {
this.document = document;
this.text = text;
}
execute() {
if (this.text === this.document.text) return false;
this.before = this.document.text;
this.document.write(this.text);
return true;
}
undo() {
if (this.before === null) throw new Error("Execute before undo");
this.document.write(this.before);
}
}The invoker · TypeScript
export class CommandHistory {
private done: Command[] = [];
private undone: Command[] = [];
run(command: Command) {
if (this.done.includes(command) || this.undone.includes(command))
throw new Error("Create a fresh command for each new request");
if (!command.execute()) return false;
this.done.push(command);
this.undone = []; // a new successful edit starts a different future
return true;
}
undo() {
const command = this.done.at(-1);
if (!command) return false;
command.undo();
this.done.pop();
this.undone.push(command);
return true;
}
redo() {
const command = this.undone.at(-1);
if (!command || !command.execute()) return false;
this.undone.pop();
this.done.push(command);
return true;
}
snapshot() {
return Object.freeze({
done: Object.freeze(this.done.map((command) => command.label)),
undone: Object.freeze(this.undone.map((command) => command.label)),
});
}
}The receiver · TypeScript
export class TextDocument {
private value: string;
constructor(initial = "Hello") {
this.value = initial;
}
get text() {
return this.value;
}
write(text: string) {
this.value = text;
}
}TypeScript excerpts come from the running model. Kotlin is equivalent instructional source; compilation is unverified.
When to use it—and when to stop.
Useful when
Requests need to be passed around, triggered by different controls or managed by a shared history. The same contract can support other invokers such as a queue, with explicit execution policies.
Costs
More objects and bookkeeping. Whole-text snapshots use memory proportional to the document for every edit. A function or a small snapshot history may be simpler; large editors often use deltas or persistent data structures.
- Command versus Strategy: Command represents a particular request; Strategy represents an interchangeable way to perform a job.
- Command versus Memento: Command owns behavior; Memento captures state. They can work together. This example simply stores a string snapshot.
- Undo is not universal: external effects may need compensating actions, permission checks or no undo at all.
- A queue is not reliability: retries, persistence and idempotency need their own design. They are not implemented here.