Behavioral · DWG NO. 017

Command

Turn a request into a standalone object, so it can be queued, logged, or undone instead of executing immediately.

TypeBehavioral
ScopeSingle action
ComplexityLow–Medium
Common inUndo/redo, task queues, macro recording

"Do this action" and "undo this action" need to travel together

A text editor's toolbar button calling `document.insertText(x)` directly has no way to undo that specific action later, and no way to queue several actions to run in sequence or replay them.

Wrap the action and its undo in one object

Each command implements `execute()` and `undo()`. An invoker (like an undo stack) holds a history of executed commands and can call `undo()` on the most recent one without knowing what kind of action it was.

Invoker(undo stack)execute()InsertTextCmdmutatesDocument

Undoable text-insert commands

`undo()` reverses exactly what `execute()` did, so the invoker can pop the stack without knowing what kind of edit it was.

undoable-commands.js
class Document { text = ""; }

class InsertTextCommand {
  constructor(doc, text) { this.doc = doc; this.text = text; }
  execute() { this.doc.text += this.text; }
  undo() { this.doc.text = this.doc.text.slice(0, -this.text.length); }
}

class CommandStack {
  #history = [];
  run(command) { command.execute(); this.#history.push(command); }
  undoLast() { this.#history.pop()?.undo(); }
}

const doc = new Document();
const stack = new CommandStack();

stack.run(new InsertTextCommand(doc, "Hello"));
stack.run(new InsertTextCommand(doc, ", world"));
console.log(doc.text); // "Hello, world"

stack.undoLast();
console.log(doc.text); // "Hello"

Trade-offs