Actions as objects: undo and redo
A waiter writes your order on a slip instead of shouting it to the kitchen. The slip can wait in a queue, be handed to whichever cook is free, be crossed out if you change your mind, and tell the cashier later exactly what you ordered.
A command is that slip for your code: an object that describes one action (“move this shape 20 points right”) and can execute() it. For undo, it also knows how to undo() itself, because it remembers what it changed. The app keeps executed commands on an undo stack; pressing Undo takes the top one and reverses it.
The Command pattern encapsulates a request as an object, decoupling the invoker (a button, a keyboard shortcut, a queue) from the receiver that does the work, and making requests first-class: storable, composable (macro commands), loggable, undoable and replayable. Undo works either by inverse operations (each command knows its reverse) or by memento snapshots of the affected state; inverse operations are cheap but must capture exactly the state they overwrite, at execution time. Platform undo managers (UndoManager on Apple platforms, the browser’s editing history, IDE command stacks) are implementations of this.
protocol Command {
func execute()
func undo()
}
final class MoveShape: Command {
private let canvas: Canvas
private let shapeId: Shape.ID
private let offset: CGVector
init(canvas: Canvas, shapeId: Shape.ID, offset: CGVector) {
self.canvas = canvas; self.shapeId = shapeId; self.offset = offset
}
func execute() { canvas.move(shapeId, by: offset) }
func undo() { canvas.move(shapeId, by: CGVector(dx: -offset.dx, dy: -offset.dy)) }
}
final class History {
private var undoStack: [Command] = []
private var redoStack: [Command] = []
func perform(_ command: Command) {
command.execute()
undoStack.append(command)
redoStack.removeAll() // a new action makes redo history invalid
}
func undo() {
guard let command = undoStack.popLast() else { return }
command.undo()
redoStack.append(command)
}
func redo() {
guard let command = redoStack.popLast() else { return }
command.execute()
undoStack.append(command)
}
}interface Command {
fun execute()
fun undo()
}
class MoveShape(
private val canvas: Canvas,
private val shapeId: ShapeId,
private val dx: Float,
private val dy: Float,
) : Command {
override fun execute() = canvas.move(shapeId, dx, dy)
override fun undo() = canvas.move(shapeId, -dx, -dy)
}
class History {
private val undoStack = ArrayDeque<Command>()
private val redoStack = ArrayDeque<Command>()
fun perform(command: Command) {
command.execute()
undoStack.addLast(command)
redoStack.clear() // a new action makes redo history invalid
}
fun undo() {
val command = undoStack.removeLastOrNull() ?: return
command.undo()
redoStack.addLast(command)
}
fun redo() {
val command = redoStack.removeLastOrNull() ?: return
command.execute()
undoStack.addLast(command)
}
}public interface Command {
void execute();
void undo();
}
public final class MoveShape implements Command {
private final Canvas canvas;
private final ShapeId shapeId;
private final float dx, dy;
public MoveShape(Canvas canvas, ShapeId shapeId, float dx, float dy) {
this.canvas = canvas; this.shapeId = shapeId; this.dx = dx; this.dy = dy;
}
public void execute() { canvas.move(shapeId, dx, dy); }
public void undo() { canvas.move(shapeId, -dx, -dy); }
}
public final class History {
private final Deque<Command> undoStack = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void perform(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear(); // a new action makes redo history invalid
}
public void undo() {
Command command = undoStack.poll();
if (command == null) return;
command.undo();
redoStack.push(command);
}
public void redo() {
Command command = redoStack.poll();
if (command == null) return;
command.execute();
undoStack.push(command);
}
}interface Command {
execute(): void;
undo(): void;
}
class MoveShape implements Command {
constructor(
private readonly canvas: Canvas,
private readonly shapeId: string,
private readonly dx: number,
private readonly dy: number,
) {}
execute(): void { this.canvas.move(this.shapeId, this.dx, this.dy); }
undo(): void { this.canvas.move(this.shapeId, -this.dx, -this.dy); }
}
class History {
private undoStack: Command[] = [];
private redoStack: Command[] = [];
perform(command: Command): void {
command.execute();
this.undoStack.push(command);
this.redoStack = []; // a new action makes redo history invalid
}
undo(): void {
const command = this.undoStack.pop();
if (!command) return;
command.undo();
this.redoStack.push(command);
}
redo(): void {
const command = this.redoStack.pop();
if (!command) return;
command.execute();
this.undoStack.push(command);
}
}- Each command is small:
MoveShaperemembers only the shape and the offset, not a copy of the canvas. 200 moves cost almost no memory. - Undo reverses the command (move back by the same offset); redo executes it again.
- A new action clears redo: after you undo and then draw something new, the undone steps no longer make sense on top of it.
- Buttons, menus and shortcuts all just call
history.perform(MoveShape(...)); they don’t know how moving works.
Predict the output
A text editor starts empty. The user types 'a' (one command), types 'b', presses Undo, types 'c', then presses Redo. What's the text?
After 'a' and 'b' the text is ab. Undo removes 'b' and moves that command to the redo stack: a. Typing 'c' is a new command, which clears the redo stack: ac. Redo now has nothing to redo, so the text stays ac. If the redo stack weren't cleared, redo would insert 'b' at a position computed for a different text.
Tap the line with the bug.
final class SetColor: Command {
private let shape: Shape
private let newColor: Color
private let oldColor: Color
init(shape: Shape, newColor: Color) {
self.shape = shape
self.newColor = newColor
self.oldColor = shape.color
}
func execute() { shape.color = newColor }
func undo() { shape.color = oldColor }
}class SetColor(private val shape: Shape, private val newColor: Color) : Command {
private val oldColor = shape.color
override fun execute() { shape.color = newColor }
override fun undo() { shape.color = oldColor }
}public final class SetColor implements Command {
private final Shape shape;
private final Color newColor;
private final Color oldColor;
public SetColor(Shape shape, Color newColor) {
this.shape = shape;
this.newColor = newColor;
this.oldColor = shape.getColor();
}
public void execute() { shape.setColor(newColor); }
public void undo() { shape.setColor(oldColor); }
}class SetColor implements Command {
private readonly oldColor: Color;
constructor(private readonly shape: Shape, private readonly newColor: Color) {
this.oldColor = shape.color;
}
execute(): void { this.shape.color = this.newColor; }
undo(): void { this.shape.color = this.oldColor; }
}Hint: Redo executes the same command object again later. And a macro command may create several SetColor commands before running any of them. When is oldColor read?
The old color is captured when the command is created, not when it’s executed. That works only if creation and execution happen together, once. It breaks as soon as they don’t:
- A “recolor all selected” macro creates
SetColor(shape, red)for every selected shape first, then executes them. If the same shape is selected twice (or another command in the macro changes it),oldColoris already outdated when it runs. - Undo, a different change to the shape, then Redo:
execute()runs again, butoldColorstill holds the value from the first time, so the next Undo restores a color the shape no longer had.
Capture the state a command overwrites in execute(), every time it runs:
private var oldColor: Color?
func execute() { oldColor = shape.color; shape.color = newColor }
func undo() { if let oldColor { shape.color = oldColor } }private var oldColor: Color? = null
override fun execute() { oldColor = shape.color; shape.color = newColor }
override fun undo() { oldColor?.let { shape.color = it } }private Color oldColor;
public void execute() { oldColor = shape.getColor(); shape.setColor(newColor); }
public void undo() { if (oldColor != null) shape.setColor(oldColor); }private oldColor?: Color;
execute(): void { this.oldColor = this.shape.color; this.shape.color = this.newColor; }
undo(): void { if (this.oldColor !== undefined) this.shape.color = this.oldColor; }Group commands into one undo step
“Align left” moves five shapes at once. Undo should reverse all five moves in one step, not one at a time. Write a MacroCommand that holds a list of commands.
›
Executing runs the commands in order. Undoing must reverse them in the opposite order, because later commands may depend on earlier ones.
SolutionTry it yourself first
final class MacroCommand: Command {
private let commands: [Command]
init(_ commands: [Command]) { self.commands = commands }
func execute() { commands.forEach { $0.execute() } }
func undo() { commands.reversed().forEach { $0.undo() } }
}
history.perform(MacroCommand(selected.map { MoveShape(canvas: canvas, shapeId: $0.id, offset: offsetToLeft($0)) }))class MacroCommand(private val commands: List<Command>) : Command {
override fun execute() = commands.forEach { it.execute() }
override fun undo() = commands.asReversed().forEach { it.undo() }
}
history.perform(MacroCommand(selected.map { MoveShape(canvas, it.id, -it.x, 0f) }))public final class MacroCommand implements Command {
private final List<Command> commands;
public MacroCommand(List<Command> commands) { this.commands = List.copyOf(commands); }
public void execute() { commands.forEach(Command::execute); }
public void undo() { commands.reversed().forEach(Command::undo); } // Java 21
}class MacroCommand implements Command {
constructor(private readonly commands: Command[]) {}
execute(): void { this.commands.forEach((c) => c.execute()); }
undo(): void { [...this.commands].reverse().forEach((c) => c.undo()); }
}MacroCommand is itself a Command, so the history doesn’t need to know about groups at all. That’s the Composite idea applied to commands.
Undo in real apps
- Coalescing: typing “hello” shouldn’t take five undos. Merge consecutive commands of the same kind within a time window (
UndoManagergroups by run loop cycle; editors merge typing until a pause or cursor jump). - Inverse vs snapshot: inverse operations are compact but fragile when operations don’t commute; snapshots of the affected object (not the whole document) are simpler for complex edits. Persistent data structures make snapshots cheap.
- Collaborative editing: with other users editing concurrently, a local inverse can’t be applied blindly; operational transformation or CRDT-based undo transforms the inverse against later remote operations.
- Bounded history: cap the stack (by count or memory) and drop the oldest entries.
- Command encapsulates a request: invoker decoupled from receiver; storable, composable (macro), loggable, undoable.
- Undo via inverse operations (capture overwritten state at
execute, every time) or memento snapshots. - New command clears redo; macros undo in reverse order; coalesce typing; bound the history.
- Collaborative undo needs OT/CRDT-aware inverses.
Check this part
What does a command object hold for undo to work?
Why is the redo stack cleared when a new command runs?
When should a command read the value it will overwrite?