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.

Your answer

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

The picture to keep
The order slip

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.

In one line: Turn a request into an object, so it can be queued, logged and undone.

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.

Figure 1. The button creates a command and hands it to the history. The history executes it against the cart and keeps it, which is what makes undo possible.

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.
Things that cannot be undone

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.

Undo that guesses

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.

Design undo and redo

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.

TypeScript you just picked up
interface Command { execute(): void; undo(): void }
Any object with these two methods can go into the history.
private readonly done: Command[] = []
An array used as a stack. push adds to the end, pop removes from it.
this.done.pop()?.undo()
Optional chaining. Skips the call when pop returns undefined.
if (!command) return
A guard. After it, TypeScript knows command is not undefined.
type Command = { execute: () => void; undo: () => void }
The same contract as an object type holding two functions.
function addItem(item: string): Command
A function that returns a command. The closures remember item.
() => void items.push(item)
void discards the returned length, so the function returns nothing.

Checkpoint

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?

Say this in 60 seconds

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.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.