Skip to main content

Command: Making Actions First-Class

What This Concept Is

Command takes a request -- "delete this file", "transfer $50", "submit this form" -- and packages it as an object. That object bundles the operation, the target (receiver), and any parameters into one thing you can pass around, store, or defer.

Four roles:

  • Receiver -- the object that actually does the work.
  • Command -- an object with an execute() method that calls the receiver.
  • Invoker -- the thing that triggers commands (a button, a menu, a scheduler).
  • Client -- the wiring code that creates the command and associates it with an invoker.

Invoker and receiver do not see each other. They are connected through the command.

Why It Matters Here

Once actions are objects, a lot becomes possible:

  • history and undo
  • queueing and scheduling
  • logging and replay
  • remote execution (serialize the command, ship it, run it over there)
  • macro commands (compose them)

Every one of those requires a way to refer to an operation after the moment it was requested. Commands give you that reference.

Concrete Example

A text editor where buttons, keystrokes, and macros all run through commands.

from abc import ABC, abstractmethod

class Editor:
def __init__(self): self.text = ""
def insert(self, s, at): self.text = self.text[:at] + s + self.text[at:]
def delete(self, start, end): self.text = self.text[:start] + self.text[end:]

class Command(ABC):
@abstractmethod
def execute(self): ...

class InsertCommand(Command):
def __init__(self, editor: Editor, s: str, at: int):
self.editor, self.s, self.at = editor, s, at
def execute(self):
self.editor.insert(self.s, self.at)

class DeleteCommand(Command):
def __init__(self, editor: Editor, start: int, end: int):
self.editor, self.start, self.end = editor, start, end
def execute(self):
self.editor.delete(self.start, self.end)

class Button: # invoker
def __init__(self, cmd: Command): self.cmd = cmd
def click(self): self.cmd.execute()

ed = Editor()
Button(InsertCommand(ed, "hello", 0)).click()
Button(InsertCommand(ed, " world", 5)).click()
print(ed.text) # "hello world"

Button knows nothing about Editor. It only knows "I have a command; on click, execute it." Same Button class, three entirely different effects.

Common Confusion / Misconception

  • "It is just a function with a fancy name." In languages with first-class functions, a bound method or a closure is a Command. The pattern adds value when you need to attach metadata (description, undo, timestamp) or serialize the action -- things a bare function cannot do idiomatically.
  • Commands that do too much. A command should coordinate one request, not contain the business logic. Delegate to a receiver; do not inline everything.
  • Mutable commands. If execute writes back to the command itself beyond capturing undo state, replaying becomes risky. Treat commands as value-ish after construction.
  • Over-application. Not every method call deserves a command. Wrap actions you will reuse, queue, record, or undo.

How To Use It

  1. Identify which operations need first-class handling (undo, queue, log, remote).
  2. Define a Command interface with execute() (and maybe undo(), see next concept).
  3. For each operation, create a class holding a reference to the receiver and any parameters, with execute() delegating to the receiver.
  4. Give invokers a field of type Command and have them call execute() when triggered.
  5. Keep construction and execution separate in time; that separation is the whole point.

Check Yourself

  1. Name the four roles and give one example of each from a UI app.
  2. Why does the invoker not depend on the receiver?
  3. What changes about the code the moment an operation becomes first-class?
  4. When should you not promote a function call into a Command?

Mini Drill or Application

Take a small CLI tool with four operations (add, delete, rename, tag).

  1. Wrap each in a Command class with execute().
  2. Replace the argument-parsing dispatch with a map {"add": AddCommand, ...}.
  3. Print the command class name before running it -- you now have free action logging.
  4. Run a test that feeds a list of commands and reports failures per command.

Stop when the CLI's main function has no if cmd == "add": ... branch.

Read This Only If Stuck