The user presses Ctrl+Z, and your code has nothing to work with: the insert that needs reversing was a direct method call that finished and vanished. Command is the Gang of Four behavioral pattern that turns a request into an object carrying everything needed to perform it, and usually to reverse it, which is how the catalog unlocks queuing, logging, and undoable operations from one move.
A command is really just a function call frozen solid: the receiver, the method, and the arguments packed into an object that can be executed later, executed elsewhere, or executed in reverse. Every undo stack you've ever pressed Ctrl+Z into is a pile of these objects waiting to run backward.
Here's the hardwired version. A toolbar button and a keyboard shortcut both insert text by calling the document directly, which works perfectly right up until the feature request that defines text editors arrives:
The problem is ontological: there is no thing in this program that represents "the insert that just happened". The call ran, the stack frame popped, and the only evidence is the changed text, which doesn't know what changed it. Undo, redo, macros, an operation log, and a job queue all need the same missing ingredient, an object that outlives the call it describes.
The command version builds that object. InsertCommand packs the document and the text together with an execute() that inserts and an undo() that removes exactly what was inserted. A History invoker runs commands and stacks them. The example inserts hello, then world, then undoes once. Before reading past the code, predict the final printed text and what the history stack still holds:
The document reads hello and the stack holds one command. The undo popped the most recent receipt, InsertCommand(doc, " world"), and asked it to reverse itself, which it could do because it still remembered its own text and could compute remove_last(6). The first insert's receipt remains on the stack, one more Ctrl+Z away from an empty document. Nothing about the document class changed to make any of this possible.
Five roles, more than most patterns, each small. The Command interface declares execute() and here undo(). ConcreteCommands (InsertCommand) bind a receiver to one action with its arguments. The Receiver (Document) does the actual work and knows nothing about commands. The Invoker (History) runs commands and does the bookkeeping without ever looking inside one. The Client wires it together, deciding which commands exist and who invokes them.
The pattern works like a restaurant order ticket. The waiter doesn't cook your meal, they write a ticket that captures the request and hand it to the kitchen, and the ticket itself can sit in a queue, be handed between staff, and survive as a record after the food is served. The analogy stops exactly where the pattern's honest limit is: a meal cannot be un-cooked. Commands only undo when a true inverse exists, and operations like sending an email or charging a card have no undo() worth the name, only compensating actions that are new commands in their own right.
Installing the pattern is four moves:
InsertCommand carries the receiver and the arguments inside itself, so execute() needs nothing from outside. A frozen call can wait in a queue, ride over a wire, or sit in history, none of which a finished method call can do.undo() is the half that earns the pattern its keep: insert pairs with remove, add pairs with subtract. Writing them side by side in one class keeps the pairing honest when either half changes.History.run() executes and then files the command on a stack. The invoker never looks inside what it runs, which is why one history class serves every command the app will ever grow.InsertCommand and hand it to the history. One action, many triggers, zero duplicated calls.Undo systems split into two camps, and this pattern is one of them. Command-based undo stores the inverse operation, which is cheap in memory and demands that every action know how to reverse itself. Snapshot-based undo stores copies of prior state and restores wholesale, costing memory for the simplicity of never computing an inverse, an approach with its own pattern name, Memento. Real editors mix them: cheap reversible edits ride commands, while operations too gnarly to invert checkpoint a snapshot first.
The JDK's Swing toolkit ships the command-style machinery wholesale: UndoManager manages a list of UndoableEdit objects, undoes them in reverse order, and on each new edit discards everything past the current position, which is the clear-the-redo-branch rule you've watched every editor enforce when you undo twice and then type.
You might think languages with first-class functions made this pattern obsolete, and for half its uses they did. A closure already freezes a receiver and arguments into a callable value, so a command whose only method is execute() is pure ceremony, one class where () => doc.insert(text) would do. The Go tab leans into this honestly: its command is a struct of two function values, Do and Undo, with closures capturing the receiver, and the GoF class diagram is nowhere in sight.
What closures don't give you is the second verb. The moment an action needs undo(), needs serializing into a job queue, or needs inspecting in a log, the request wants named structure, and an object with two paired methods says things a lambda can't. The Rust tab makes the same call with a trait, passing the receiver into execute(&mut doc) at run time, which keeps ownership clean and makes the receiver dependency visible in the signature.
The pattern is a short interface, and these five decide whether the undo stack can be trusted:
execute() is a closure wearing a class costume, and the closure was less code. Reach for the object when you need undo(), serialization, or queuing, and reach for a plain function when you don't.Two questions before you go. First: after two inserts and one undo, the document read hello. What object did the history pop, and what two facts did it carry that made the reversal possible? Second: your language has closures, so name the requirements that move an action from "use a function" to "use a command object". Both answers are on this page.
Then go spot it in the wild, where this pattern is unusually easy to catch. Every undo stack in every editor is a pile of command objects, every task queue that serializes jobs to workers is freezing calls into transportable objects, and database write-ahead logs replay frozen operations after a crash. Last, open your own app's toolbar handlers and ask of each one: if undo became a requirement tomorrow, does this click leave behind enough evidence to reverse itself? A no is this pattern's front door.
class Document:
def __init__(self):
self.text = ""
def insert(self, text: str) -> None:
self.text += text
doc = Document()
def on_toolbar_click() -> None:
# The button reaches straight into the document
doc.insert("hello")
def on_keyboard_shortcut() -> None:
# The shortcut duplicates the same hardwired call
doc.insert("hello")
# Example usage
on_toolbar_click()
print(doc.text) # hello
# Now the user presses Ctrl+Z. Undo what, exactly?
# The call is finished and left no trace of itself.from abc import ABC, abstractmethod
class Document: # Receiver: knows how to do the actual work
def __init__(self):
self.text = ""
def insert(self, text: str) -> None:
self.text += text
def remove_last(self, count: int) -> None:
self.text = self.text[:-count]
class Command(ABC): # Command: a frozen call with a reverse gear
@abstractmethod
def execute(self) -> None: ...
@abstractmethod
def undo(self) -> None: ...
class InsertCommand(Command): # ConcreteCommand
def __init__(self, doc: Document, text: str):
self.doc = doc # the receiver travels inside
self.text = text # so do the arguments
def execute(self) -> None:
self.doc.insert(self.text)
def undo(self) -> None:
self.doc.remove_last(len(self.text))
class History: # Invoker: runs commands and keeps the receipts
def __init__(self):
self._done: list[Command] = []
def run(self, command: Command) -> None:
command.execute()
self._done.append(command)
def undo(self) -> None:
if self._done:
self._done.pop().undo()
# Example usage
doc = Document()
history = History()
history.run(InsertCommand(doc, "hello"))
history.run(InsertCommand(doc, " world"))
print(doc.text) # hello world
history.undo()
print(doc.text) # hello