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 after defining a versioned data representation and handler contract
  • 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. Named records or objects can make metadata and lifecycle explicit. A class is not automatically serializable, and functions can be wrapped with metadata too.
  • Commands that do too much. A command owns one request. It may delegate to a receiver or implement the operation itself; choose a boundary that makes dependencies and invariants clear.
  • 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 a diagnostic label, not a complete audit trail.
  4. Run a test that feeds a list of commands and reports failures per command.

Stop when dispatch, validation, effect ownership and failure are explicit; a small dispatch branch is acceptable.

Read This Only If Stuck​

Contract and transfer​

Separate an immutable request from one execution's result and undo state. Reusing a mutable command instance for two history entries can overwrite the first entry's backup. A class holding an open socket or editor reference is not a durable message. For deferred work, store operation name, schema version, validated values and target identity, then resolve the receiver at execution time.

The slicing editor sample assumes valid indices and only demonstrates dispatch: Python slicing tolerates out-of-range positions. L03 explicitly rejects invalid edits before changing document or history. Capture authorization and validation requirements at the appropriate boundary; a deserialized command name is not permission to execute an arbitrary method.

Transfer: a queued rename sees that the target was deleted after enqueueing. Define reject, ignore or redirect behavior and record the outcome. Serialization preserves a request's data; it cannot preserve the world that request expected.