Low-level designCore8 min

Command

Turn each request into an object that knows how to carry itself out, and how to take itself back. Whatever runs requests can then queue, log, undo and redo them without knowing what any of them does.

Watch it evolve1 / 10
The problem

A seating chart with undo

Planner moves and removes guests, and records each edit as an Action for undo and redo.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A seating chart with undo

A wedding planner's app arranges guests at tables. SeatingChart knows who sits where; Planner offers the edits a planner makes, move() and remove(), and lets them undo() and redo() because seating plans change a dozen times an hour.

To undo, the first version records each edit as an Action: an ActionType enum, the guest, and the tables it moved from and to. undo() and redo() each switch on the type to reverse or repeat it. Python writes table_of().

The first versionJava
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

class SeatingChart {
    private final Map<String, Integer> tables = new TreeMap<>();

    void seat(String guest, int table) {
        tables.put(guest, table);
    }

    void unseat(String guest) {
        tables.remove(guest);
    }

    int tableOf(String guest) {
        return tables.get(guest);
    }

    String describe() {
        Map<Integer, List<String>> byTable = new TreeMap<>();
        for (Map.Entry<String, Integer> e : tables.entrySet()) {
            byTable.computeIfAbsent(e.getValue(), t -> new ArrayList<>()).add(e.getKey());
        }
        List<String> parts = new ArrayList<>();
        for (Map.Entry<Integer, List<String>> e : byTable.entrySet()) {
            parts.add("T" + e.getKey() + ": " + String.join(", ", e.getValue()));
        }
        return String.join(" | ", parts);
    }
}

enum ActionType { MOVE, REMOVE }

record Action(ActionType type, String guest, int from, int to) {}

class Planner {
    private final SeatingChart chart;
    private final Deque<Action> done = new ArrayDeque<>();
    private final Deque<Action> undone = new ArrayDeque<>();

    Planner(SeatingChart chart) {
        this.chart = chart;
    }

    void move(String guest, int table) {
        done.push(new Action(ActionType.MOVE, guest, chart.tableOf(guest), table));
        undone.clear();
        chart.seat(guest, table);
    }

    void remove(String guest) {
        done.push(new Action(ActionType.REMOVE, guest, chart.tableOf(guest), 0));
        undone.clear();
        chart.unseat(guest);
    }

    void undo() {
        Action a = done.pop();
        ActionType type = a.type();
        switch (type) {
            case MOVE -> chart.seat(a.guest(), a.from());
            case REMOVE -> chart.seat(a.guest(), a.from());
        }
        undone.push(a);
    }

    void redo() {
        Action a = undone.pop();
        ActionType type = a.type();
        switch (type) {
            case MOVE -> chart.seat(a.guest(), a.to());
            case REMOVE -> chart.unseat(a.guest());
        }
        done.push(a);
    }
}

public class Main {
    public static void main(String[] args) {
        SeatingChart chart = new SeatingChart();
        chart.seat("Ana", 1);
        chart.seat("Ben", 1);
        chart.seat("Cai", 2);
        Planner planner = new Planner(chart);
        System.out.println("Start: " + chart.describe());
        planner.move("Ana", 2);
        System.out.println("Move Ana to 2: " + chart.describe());
        planner.remove("Ben");
        System.out.println("Remove Ben: " + chart.describe());
        planner.undo();
        System.out.println("Undo: " + chart.describe());
        planner.undo();
        System.out.println("Undo: " + chart.describe());
        planner.redo();
        System.out.println("Redo: " + chart.describe());
    }
}
*SeatingChart− tables: Map<String, Integer>~ seat(String, int)~ unseat(String)~ tableOf(String): int~ describe(): String«enum»ActionTypeMOVEREMOVE«record»Action− type: ActionType− guest: String− from: int− to: intPlanner− chart: SeatingChart− done: Deque<Action>− undone: Deque<Action>~ move(String, int)~ remove(String)~ undo()~ redo()

The class diagram of this version, drawn from its code: the planner records actions as data and interprets them itself.

Two edits, two undos and a redo:

Start: T1: Ana, Ben | T2: Cai
Move Ana to 2: T1: Ben | T2: Ana, Cai
Remove Ben: T2: Ana, Cai
Undo: T1: Ben | T2: Ana, Cai
Undo: T1: Ana, Ben | T2: Cai
Redo: T1: Ben | T2: Ana, Cai

2 · The change

A third kind of edit

Planners ask for swap: exchange the seats of two guests in one step, and undo it in one step. In this design a swap touches everything about actions:

Each kind of edit, interpreted in two switchesJava
class Planner {
    private final SeatingChart chart;
    private final Deque<Action> done = new ArrayDeque<>();
    private final Deque<Action> undone = new ArrayDeque<>();

    Planner(SeatingChart chart) {
        this.chart = chart;
    }

    void move(String guest, int table) {
        done.push(new Action(ActionType.MOVE, guest, chart.tableOf(guest), table));
        undone.clear();
        chart.seat(guest, table);
    }

    void remove(String guest) {
        done.push(new Action(ActionType.REMOVE, guest, chart.tableOf(guest), 0));
        undone.clear();
        chart.unseat(guest);
    }

    void undo() {
        Action a = done.pop();
        ActionType type = a.type();
        switch (type) {
            case MOVE -> chart.seat(a.guest(), a.from());
            case REMOVE -> chart.seat(a.guest(), a.from());
        }
        undone.push(a);
    }

    void redo() {
        Action a = undone.pop();
        ActionType type = a.type();
        switch (type) {
            case MOVE -> chart.seat(a.guest(), a.to());
            case REMOVE -> chart.unseat(a.guest());
        }
        done.push(a);
    }
}

Place

What swap needs there

ActionType

A new value

Action

A second guest and a second table, which every other kind of action carries empty

Planner

A swap() method that builds the record

undo() and redo()

A new case in each, kept in step with each other

The knowledge of what an edit is is split in three: swap() performs it, undo() knows how to reverse it, redo() how to repeat it. Each new edit spreads across the same places, and Action grows fields that only one kind uses. The practice checks flag the design: ActionType is switched on in two places.

3 · The refactor

Make each edit an object

Start from the first version. Put everything about one kind of edit, doing it and undoing it, into a class of its own, and keep a history of those objects:

ChartCommand has execute() and undo(). MoveGuest and RemoveGuest implement it. Each holds the chart, and in execute() remembers what it needs to undo: the table the guest came from.

Java
interface ChartCommand {
    void execute();
    void undo();
}

class MoveGuest implements ChartCommand {
    private final SeatingChart chart;
    private final String guest;
    private final int to;
    private int from;

    MoveGuest(SeatingChart chart, String guest, int to) {
        this.chart = chart;
        this.guest = guest;
        this.to = to;
    }

    public void execute() {
        from = chart.tableOf(guest);
        chart.seat(guest, to);
    }

    public void undo() {
        chart.seat(guest, from);
    }
}
*SeatingChart− tables: Map<String, Integer>~ seat(String, int)~ unseat(String)~ tableOf(String): int~ describe(): String«interface»ChartCommand+ execute()+ undo()MoveGuest− chart: SeatingChart− guest: String− to: int− from: int+ execute()+ undo()RemoveGuest− chart: SeatingChart− guest: String− from: int+ execute()+ undo()History− done: Deque<ChartCommand>− undone: Deque<ChartCommand>~ run(ChartCommand)~ undo()~ redo()

After the second step the program prints what it printed before. After the third, a swap is done and undone like any other edit:

Start: T1: Ana, Ben | T2: Cai
Move Ana to 2: T1: Ben | T2: Ana, Cai
Remove Ben: T2: Ana, Cai
Undo: T1: Ben | T2: Ana, Cai
Undo: T1: Ana, Ben | T2: Cai
Redo: T1: Ben | T2: Ana, Cai
Swap Ana and Ben: T1: Ana | T2: Ben, Cai
Undo: T1: Ben | T2: Ana, Cai
Edits as data
4
Edits as objects
0

Places in existing code that adding swap edits. Before: ActionType, Action, Planner and its two switches. After: none; SwapGuests is new.

4 · The pattern

Command: a request as an object

Command wraps a request in an object with a uniform method, usually execute(), that carries it out on some receiver. Because a request is now an object, it can be stored, passed around, queued, logged and, given an undo(), reversed. The invoker that runs commands depends only on the command interface.

*SeatingChart− tables: Map<String, Integer>~ seat(String, int)~ unseat(String)~ tableOf(String): int~ describe(): String«interface»ChartCommand+ execute()+ undo()MoveGuest− chart: SeatingChart− guest: String− to: int− from: int+ execute()+ undo()RemoveGuest− chart: SeatingChart− guest: String− from: int+ execute()+ undo()SwapGuests− chart: SeatingChart− first: String− second: String− firstTable: int− secondTable: int+ execute()+ undo()History− done: Deque<ChartCommand>− undone: Deque<ChartCommand>~ run(ChartCommand)~ undo()~ redo()

The roles in the planner's code: ChartCommand is the command, History the invoker, and SeatingChart the receiver every command acts on.

Role

In the planner

Its job

Command

ChartCommand

The interface: execute(), and here undo()

Concrete command

MoveGuest, RemoveGuest, SwapGuests

One request each, with what it needs to undo it

Receiver

SeatingChart

Does the actual work the commands ask for

Invoker

History

Runs commands and keeps them, without knowing what they do

Client

main

Creates commands and hands them to the invoker

The command remembers table 1 when it runs, so undoing it needs no help from anyone else.

Undo is only one thing a request-as-object allows:

  • Queues and scheduling. Commands can be stored now and run later, or on another thread.
  • Logs and replay. Recording commands lets you replay a session, or rebuild state after a crash.
  • Macros. A command that holds a list of commands and runs them in order is a command too, a Composite of commands.

5 · What it costs

A class per request, and undo that must be exact

  • A class per kind of request. Three edits are three classes and an interface, where there was one method each.
  • Undo must restore exactly. A command saves what it needs when it runs. If the receiver can change in other ways between execute() and undo(), say a table is deleted, undo can restore a state that no longer makes sense.
  • Some requests can't be undone. Sending the seating plan to the caterer can't be taken back. Such commands need to say so, and the history should stop at them.
  • History uses memory. An unbounded undo stack grows for the whole session; cap it.

Situation

Command?

Requests must be undone, redone, queued or logged

Yes

One call, made immediately, never stored

No: call the method

Requests have no state and no undo, and the language has functions

Often a plain function or lambda is the command

Undo by restoring a snapshot is simpler than reversing each request

Consider Memento instead, or with it

6 · Getting it right

Capture at execute, restore at undo

  • Capture what undo needs in execute(), not in the constructor. MoveGuest reads the old table when it runs, so redo after other edits still works.
  • Keep commands thin. The receiver does the work; a command decides which receiver calls to make. A command full of seating logic duplicates the chart.
  • A new command clears redo. After an undo, running a new command makes the undone ones meaningless, so run() empties the redo stack.
  • Bound the history, and mark commands that can't be undone.

Language

The usual shape

Java

An interface and one class per command; Runnable or a lambda for a command with no undo

C++

An abstract class owned through std::unique_ptr; std::function for a command with no undo

JavaScript

Objects with execute() and undo(), or a pair of closures

Python

Classes matching a Protocol; a function, or functools.partial, for a command with no undo

7 · Look-alikes

Objects that stand for behavior

Several patterns put behavior in an object. What differs is what the object stands for:

Pattern

The object stands for

Typical use

Command

One request, often with its own undo

Undo, queues, logs, macros

Strategy

One way of doing a job, swapped in

Choosing an algorithm

Memento

A snapshot of state, not a request

Undo by restoring the snapshot

Chain of Responsibility

One handler among several for a request

Passing a request until something handles it

Command and Memento are the two classic ways to undo. Command undoes by reversing each request, which keeps history small but needs a correct reversal per command. Memento undoes by restoring a saved snapshot, which needs no reversal logic but stores more. Editors often combine them: a command that keeps a memento of what it changed.

8 · In practice

Commands you have already used

Java's Executor runs submitted Runnable tasks and decouples submitting a task from how it's run, on which thread and when. Each Runnable is a command with no undo: an object standing for a request, handed to an invoker that doesn't know what it does.

The practice checks recognize a Command when an interface of one or two methods has at least two implementations that each hold a receiver and call it, and something else runs them through the interface; a method named like undo() counts as a further sign.

Check yourself

4 questions

1. The planner adds "move a whole table of guests to another table". What changes?
2. Why does MoveGuest read the old table in execute() instead of its constructor?
3. After two undos, the planner runs a new move. What should happen to the two undone commands?
4. Sending the final plan to the caterer is a command. How should undo treat it?

Takeaways

Remember this

  • Command turns a request into an object with a uniform method, such as execute().
  • The invoker runs and stores commands without knowing what they do.
  • A command that captures what it changed can undo itself; a history of commands gives undo and redo.
  • Commands also enable queues, logs, replay and macros.
  • Capture undo state at execute time, and keep the work in the receiver.