The Programmable Remote (Command)
Wrap a request in an object and undo, macros, and a replayable log fall out almost for free.
The idea
Normally, when your code wants something done, it calls the thing directly: light.on(). The request happens right now, right there, and then it is gone. You cannot store it, queue it, undo it, or hand it to someone else. The Command pattern makes one move: wrap the request in an object with a single execute() method, holding everything needed to perform it — which object to poke, and how.
Once a request is a package instead of a phone call, the remote control stops knowing what a light is. It holds seven command objects and knows exactly one thing: when a button is pressed, call execute() on whatever is in that slot. Which gadget responds, and how, is decided by whoever loaded the slots. That is the same shape as a waitress carrying an order slip to a kitchen she knows nothing about.
Give every command an undo() next to execute() and a global undo button appears. For a light that is trivial. The ceiling fan is honest about the real lesson: undoing high pressed from medium must land on medium, so the command has to snapshot the receiver's state before acting. And since a command is just an object, a command can hold a list of other commands — one press of party mode runs four of them, undone in reverse.
The payoff is the log. Because each command fully describes what to do, you can write it to disk as it executes, then replay the file against a fresh house and arrive at the same state. That is event sourcing in about forty lines, and it is the seed of Redis's append-only file, a database's write-ahead log, and Raft's replicated log.
The bench — 4 exercises
The Slot Exorcism
Turn a remote built from a wall of if slot == N conditionals into a dumb invoker holding command objects, so adding a new gadget touches nothing that already exists.
- Write a naive 7-slot remote whose button handlers call six receivers directly (two lights, ceiling fan, garage door, stereo, hot tub), and pin its behavior with your own tests.
- Introduce a Command interface with only execute(), and one command class per button action.
- Rewrite the remote as an array of commands plus setCommand(slot, onCmd, offCmd); handlers do nothing but slots[i].execute().
- Replace every empty-slot null check with a single do-nothing NoCommand object.
- Add a brand-new gadget to slot 7 by writing one command class and one wiring line — nothing else.
hint
If the remote file still names any receiver type, a request is leaking out of its package.
hint
Everything the command needs belongs in its constructor, not in the invoker.
DONE WHEN
· Your pre-refactor tests still pass unchanged
· grep for receiver type names in the remote file returns nothing
· No if or switch on slot numbers outside array indexing
· Adding the new gadget required zero edits to existing files
Undo, Including the Hard Kind
Add undo() to every command, including stateful ceiling-fan speeds, and prove it with a random press/undo fuzzer against a reference model.
- Add undo() to the Command interface and to NoCommand (which does nothing).
- Make the fan speed commands snapshot the previous speed in execute() and restore it in undo().
- Wire the remote's undo button to the last executed command.
- Write a tiny independent simulator of the receivers as ground truth, and drive a few hundred seeded random press/undo sequences against both it and your remote in lockstep.
hint
execute() must read before it writes.
hint
Fuzz failures are almost always a command restoring a constant instead of what was actually there — print the seed so runs reproduce.
DONE WHEN
· Fan at medium, press high, press undo, fan reads medium
· Every receiver's state matches the reference model after every fuzz step
· Undoing an empty slot needs no null check anywhere
Party Mode and the Replayable Journal
Build a macro command out of other commands, then journal every executed command to disk and rebuild the whole house from the log alone.
- Write a MacroCommand holding an ordered list of commands: execute() runs them forward, undo() runs them in reverse.
- Load party mode (lights on, stereo on with CD, hot tub on, fan low) into slot 7.
- Make the remote append one JSON line per executed command — command name plus constructor args — to journal.jsonl.
- Write a replayer that reads the journal, rebuilds commands against a fresh set of receivers, and executes them in order.
- Run a 40-press session on house A, then replay into an empty house B and compare every receiver's final state.
hint
If replay diverges, your entries are not self-contained — a command that reads mutable state at construction time rebuilds differently.
hint
The line in the file must be the whole truth; you may log macros flattened or nested, as long as replay matches.
DONE WHEN
· One press of slot 7 sets all four receivers; one undo restores them in reverse order
· cat journal.jsonl shows one readable line per press
· House B rebuilt from the log alone is state-identical to house A
· Deleting house A entirely and replaying still reproduces it
The Job Queue (stretch)
Reuse the exact same Command interface as a background job type, and feel that a button press and a worker task are the same shape.
- Build 30 job commands that each sleep briefly and write a result.
- Put them on a queue and run four workers (threads, goroutines, or async tasks) that pull and call execute().
- Verify all results are present and wall-clock time is well under the serial time.
- Check that the worker file imports nothing but Command.
hint
If you had to change the Command interface to make this work, back up — the whole point is that you do not have to.
DONE WHEN
· All 30 results present with no duplicates or losses
· Parallel run finishes in roughly a third of the serial time or better
· Worker code names no concrete job type
Go deeper (after the bench)
Read Head First Design Patterns Ch. 6 alongside this module — the diner and order-slip opening before exercise 01, the ceiling-fan undo pages before exercise 02, and the macro section is exercise 03 almost verbatim. Once the replay gate passes, read Martin Fowler's free "Event Sourcing" article (martinfowler.com/eaaDev/EventSourcing.html) and the callout will click.