Concepts / Behavioral patterns / Command

Command

Turning an action into an object you can queue, log and undo.

01The intuition

The problem it solves

A drawing program has a toolbar: move a shape, change its colour, delete it. Each button calls the right method directly. Then the teacher asks for undo. The obvious fix is a second method for every action that reverses it and a big if-else in undo() to remember which one ran last. Then they ask for redo, for a macro that replays ten actions, and for a log of what the student did, and every one of these means another copy of the same ladder.

class Canvas:
    def move(self, shape: "Shape", dx: int, dy: int) -> None:
        shape.x += dx; shape.y += dy
        self.last = ("move", shape, dx, dy)
    def recolour(self, shape: "Shape", colour: str) -> None:
        old = shape.colour; shape.colour = colour
        self.last = ("recolour", shape, old)
    def undo(self) -> None:
        kind = self.last[0]
        if kind == "move":
            _, shape, dx, dy = self.last; shape.x -= dx; shape.y -= dy
        elif kind == "recolour":
            _, shape, old = self.last; shape.colour = old
        # delete? redo? macros? the ladder grows in three places

Build it one piece at a time

Step one: turn each action into an object that knows how to do itself and how to undo itself. The object carries its own arguments, so nothing needs to remember them in a tuple.

from typing import Protocol

class Command(Protocol):
    def do(self) -> None: ...
    def undo(self) -> None: ...

class Move:
    def __init__(self, shape: "Shape", dx: int, dy: int) -> None:
        self.shape, self.dx, self.dy = shape, dx, dy
    def do(self) -> None:
        self.shape.x += self.dx; self.shape.y += self.dy
    def undo(self) -> None:
        self.shape.x -= self.dx; self.shape.y -= self.dy

class Recolour:
    def __init__(self, shape: "Shape", colour: str) -> None:
        self.shape, self.new, self.old = shape, colour, shape.colour
    def do(self) -> None:   self.shape.colour = self.new
    def undo(self) -> None: self.shape.colour = self.old

Step two: the canvas stops knowing about moves and colours. It runs any command handed to it and pushes it on a history stack. Undo pops and calls undo; it never asks what kind of command it was.

class Canvas:
    def __init__(self) -> None:
        self.done: list[Command] = []
        self.undone: list[Command] = []

    def run(self, cmd: Command) -> None:
        cmd.do()
        self.done.append(cmd)
        self.undone.clear()          # a new action forgets the redo trail

    def undo(self) -> None:
        if self.done:
            cmd = self.done.pop(); cmd.undo(); self.undone.append(cmd)

    def redo(self) -> None:
        if self.undone:
            cmd = self.undone.pop(); cmd.do(); self.done.append(cmd)

Step three: because actions are now plain objects, you can put them in lists. A macro is a command made of commands; a log is the history printed; a queue of commands can run on another thread.

class Macro:
    def __init__(self, steps: list[Command]) -> None:
        self.steps = steps
    def do(self) -> None:
        for s in self.steps: s.do()
    def undo(self) -> None:
        for s in reversed(self.steps): s.undo()

Why it works

The principle: an action that is an object can be stored, passed around, delayed and reversed like any other value. The invariant is that undo after do restores exactly the state before, and that the history stack reflects the order things happened. Where it breaks: a command that captures the old value too late (the recolour above records old at creation; if the shape changes between creation and do, undo restores the wrong colour), commands that touch shared state outside themselves, and a redo stack that is not cleared on a new action. Interviewers test with "undo, undo, new action, redo" and with "what does undo of a delete need to remember?"

The trade-off you must say out loud

One class per action is a lot of small files, and each holds state that could be stale. For actions with no undo, a plain function call is simpler. Say when a memento (snapshot of the whole state) is better than reversing step by step: when actions are many and cheap to snapshot, or when reversing is hard to get right. The follow-up is usually about memory: a thousand undos of a large document need commands that store diffs, not copies.

Where it shows up in the 100 Google problems

  • Text editor with undo and redo: insert and delete are commands; batched undo is a macro over them.
  • Chess: a move is a command that remembers the captured piece so takeback is exact.
  • 2048: a slide is a command; undo restores the grid and the random tile it spawned.
  • Payment processor: charge and refund are commands with idempotency keys, logged before they run.
  • In-memory database with transactions: each write is a command; rollback undoes the transaction's list in reverse.

02Used in these problems