Command
Support tickets about the cart all said the same thing: “I removed the wrong item and there is no way to get it back.” Product asked for an Undo button. It sounded like an afternoon of work.
The cart had add, remove and clear, called directly from 12 button handlers. To undo
something you have to know what was done. Nothing recorded it. A call to cart.clear()
ran, the items were gone, and the call itself left no trace anywhere.
A cart supports add, remove and clear, and users want Undo for all three. What would you have to store, and where, so that the last action can be reversed? Think about what clear() destroys.
The idea
In a restaurant the waiter does not shout your order at the cook. They write it on a slip. Now the request is a thing. It can wait in a queue on the rail. It can be handed to whichever cook is free. It can be struck out if you change your mind. And it is kept for the bill at the end. None of that is possible with a shout.
A method call happens and is gone. A command is the same request stored as an object: what to do, on what, with which values. Once it is an object you can keep it in an array, and an array of what was done is an undo history.
Four roles. The command is the interface, usually execute and undo. A concrete
command is one kind of request. The receiver is the object that does the real work,
here the cart. The invoker runs commands and keeps the history.
The pattern
// The receiver. It knows how to do the work and nothing about undo.
class Cart {
private items: string[] = []
add(item: string): void {
this.items.push(item)
}
remove(item: string): void {
const index = this.items.lastIndexOf(item)
if (index !== -1) this.items.splice(index, 1)
}
clear(): string[] {
const removed = this.items
this.items = []
return removed
}
list(): readonly string[] {
return this.items
}
}
interface Command {
execute(): void
undo(): void
}
class AddItem implements Command {
constructor(private readonly cart: Cart, private readonly item: string) {}
execute(): void {
this.cart.add(this.item)
}
undo(): void {
this.cart.remove(this.item)
}
}
class ClearCart implements Command {
private removed: string[] = []
constructor(private readonly cart: Cart) {}
execute(): void {
this.removed = this.cart.clear()
}
undo(): void {
for (const item of this.removed) this.cart.add(item)
}
}
// The invoker. It runs commands and remembers them.
class CartHistory {
private readonly done: Command[] = []
run(command: Command): void {
command.execute()
this.done.push(command)
}
undo(): void {
this.done.pop()?.undo()
}
}
const cart = new Cart()
const history = new CartHistory()
history.run(new AddItem(cart, 'Masala dosa'))
history.run(new AddItem(cart, 'Filter coffee'))
history.run(new ClearCart(cart))
console.log(cart.list()) // []
history.undo()
console.log(cart.list()) // [ 'Masala dosa', 'Filter coffee' ]
history.undo()
console.log(cart.list()) // [ 'Masala dosa' ]Follow ClearCart. When it executes, it keeps what it removed in its own removed
field. That is the answer to the question at the top: a command carries the information
needed to reverse itself. AddItem needs only the item name. ClearCart needs the whole
list, so it saves it.
CartHistory knows nothing about carts. It has an array of things with execute and
undo. Undo is one line: pop the last command and call undo on it. A new kind of
action, say ApplyCoupon, is a new class, and the history code does not change.
The 12 button handlers now create a command and pass it to history.run. They no longer
call the cart directly.
this.done.pop()?.undo() has one new piece of syntax. pop() returns undefined when
the array is empty. ?. means “call this only if the thing on the left exists”. So
pressing Undo with nothing to undo does nothing, and does not crash.
Redo is a second stack
class UndoRedoHistory {
private readonly done: Command[] = []
private undone: Command[] = []
run(command: Command): void {
command.execute()
this.done.push(command)
this.undone = []
}
undo(): void {
const command = this.done.pop()
if (!command) return
command.undo()
this.undone.push(command)
}
redo(): void {
const command = this.undone.pop()
if (!command) return
command.execute()
this.done.push(command)
}
}
const basket = new Cart()
const editor = new UndoRedoHistory()
editor.run(new AddItem(basket, 'Idli'))
editor.run(new AddItem(basket, 'Vada'))
editor.undo()
console.log(basket.list()) // [ 'Idli' ]
editor.redo()
console.log(basket.list()) // [ 'Idli', 'Vada' ]Undo moves a command from done to undone. Redo moves it back. One line is easy to
miss: run empties the redo stack. Once you do something new after an undo, the old
future is gone. Every editor you have used works this way.
The lighter TypeScript form
A command with no fields of its own can be two closures in an object.
type Command = { execute: () => void; undo: () => void }
const items: string[] = []
function addItem(item: string): Command {
return {
execute: () => void items.push(item),
undo: () => void items.pop(),
}
}
const command = addItem('Masala dosa')
command.execute()
console.log(items) // [ 'Masala dosa' ]
command.undo()
console.log(items) // []addItem('Masala dosa') does not add anything. It returns the instructions for adding,
and for un-adding, to be run later. The inner functions remember item because they are
closures. void in front of an expression throws its result away, so the arrow returns
nothing and matches () => void exactly.
Beyond undo
Undo is the example everybody learns first. In a backend, the same idea does more work.
- Job queues. A background job is a command that has been turned into JSON and put on
a queue:
{ type: 'SendInvoice', orderId: 42 }. A worker picks it up and executes it, perhaps on another machine, perhaps an hour later. That is the message queues page seen from the code side. - Retries. If a command object is all you need to perform the action, you can perform it again after a failure.
- Audit logs. Store every command with a user and a timestamp and you have a record of who did what.
- Macros and batches. A command that holds a list of commands and runs them in order.
You cannot unsend an SMS or uncharge a card. For actions with effects outside your
system, undo cannot restore the old state. It has to be a new, compensating action:
send a correction, issue a refund. Decide that per command, and do not offer an Undo
button for commands that have no honest way to reverse.
RemoveItem.undo() that calls cart.add(item) puts the item at the end of the list,
not back where it was. For a cart, fine. For a text editor or a ranked list, that is a
bug. A command must capture enough at execute time (the position, the old value) to
restore the exact previous state, and it has to capture it before it makes the change.
This is asked directly, usually as “design a text editor”. Say Command, then two stacks. Execute pushes onto the undo stack and clears the redo stack. Undo pops from one, reverses, and pushes onto the other. Then raise the follow up before they do: memory. A long session cannot keep every command for ever, so cap the history, at 100 entries say, and drop the oldest.
interface Command { execute(): void; undo(): void }private readonly done: Command[] = []this.done.pop()?.undo()if (!command) returntype Command = { execute: () => void; undo: () => void }function addItem(item: string): Command() => void items.push(item)Checkpoint
1. What makes undo possible in the Command pattern?
2. ClearCart.undo() has to restore the items. Where does it get them from?
3. In undo and redo with two stacks, the user undoes twice and then performs a new action. What happens to the redo stack?
The Command pattern turns a request into an object. Instead of calling the cart directly, I create a command that holds what to do and on what, with an execute method and an undo method, and I hand it to an invoker that runs it and keeps it in a history. Because each command captures what it needs to reverse itself, undo is just popping the last command and calling undo, and redo is a second stack that is cleared whenever a new command runs. The same idea gives me job queues, retries and audit logs, since a request that is an object can be stored, sent elsewhere and replayed. In TypeScript a simple command can be a pair of closures. The care points are capturing enough state before the change, and actions like sending a message that can only be compensated, not undone.
