Command Pattern


  • Description: Encapsulate a request as an object — enables parameterizing callers, queueing/logging requests, and supporting undo/redo.
  • My Notion Note ID: K2C-2-14
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Core Idea

  • Turn a verb into a noun — wrap do_thing(args) in an object with execute().
  • Caller (Invoker) holds Command objects; doesn't know what they do.
  • Receiver does the work; Command knows the receiver + args.

Enables:

  • Undo/redo — add undo(); push executed commands onto a stack.
  • Queueing / scheduling — thread pool tasks, job queues, transaction logs.
  • Macro commands — composite of commands.
  • Replay — serialize commands → re-execute later (event sourcing).
  • Decoupling UI — same Command bound to menu item, toolbar, shortcut.

2. Structure

Roles:

  • Command — interface with execute() (and often undo()).
  • ConcreteCommand — binds Receiver + parameters; implements execute.
  • Receiver — does the actual work.
  • Invoker — triggers commands (button, scheduler, replay engine).
  • Client — creates ConcreteCommand objects and wires Invoker → Receiver.
Client → ConcreteCommand(receiver, args) → Invoker.set(command)
                                           Invoker.trigger() → command.execute() → receiver.action(args)

3. C++ Example

3.1 OO form with undo

#include <memory>
#include <vector>
#include <string>
#include <iostream>

class TextDoc {
    std::string text_;
public:
    void insert(size_t pos, std::string_view s) { text_.insert(pos, s); }
    void erase(size_t pos, size_t n) { text_.erase(pos, n); }
    const std::string& str() const { return text_; }
};

class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

class InsertCmd : public Command {
    TextDoc& doc_;
    size_t pos_;
    std::string text_;
public:
    InsertCmd(TextDoc& d, size_t p, std::string t)
        : doc_(d), pos_(p), text_(std::move(t)) {}
    void execute() override { doc_.insert(pos_, text_); }
    void undo()    override { doc_.erase(pos_, text_.size()); }
};

class History {
    std::vector<std::unique_ptr<Command>> done_;
    std::vector<std::unique_ptr<Command>> redo_;
public:
    void run(std::unique_ptr<Command> c) {
        c->execute();
        done_.push_back(std::move(c));
        redo_.clear();           // new action invalidates redo stack
    }
    void undo() {
        if (done_.empty()) return;
        done_.back()->undo();
        redo_.push_back(std::move(done_.back()));
        done_.pop_back();
    }
    void redo() {
        if (redo_.empty()) return;
        redo_.back()->execute();
        done_.push_back(std::move(redo_.back()));
        redo_.pop_back();
    }
};

int main() {
    TextDoc doc;
    History h;
    h.run(std::make_unique<InsertCmd>(doc, 0, "Hello"));
    h.run(std::make_unique<InsertCmd>(doc, 5, ", World"));
    std::cout << doc.str() << "\n";  // Hello, World
    h.undo();
    std::cout << doc.str() << "\n";  // Hello
    h.redo();
    std::cout << doc.str() << "\n";  // Hello, World
}

3.2 Lightweight form — std::function<void()>

When no undo, no introspection, no serialization — Command degenerates to a callable. std::function<void()> IS a Command.

#include <functional>
#include <queue>

class JobQueue {
    std::queue<std::function<void()>> jobs_;
public:
    void post(std::function<void()> f) { jobs_.push(std::move(f)); }
    void run_one() {
        if (jobs_.empty()) return;
        jobs_.front()();
        jobs_.pop();
    }
};

// Usage:
JobQueue q;
q.post([&doc] { doc.insert(0, "X"); });
q.post([] { std::cout << "tick\n"; });
q.run_one(); q.run_one();

Trade-off: lambdas can't undo() without explicit pairing. For undo, you need the OO form (or pair std::function<void()> for do + std::function<void()> for undo).

3.3 Macro command

class MacroCmd : public Command {
    std::vector<std::unique_ptr<Command>> cmds_;
public:
    void add(std::unique_ptr<Command> c) { cmds_.push_back(std::move(c)); }
    void execute() override { for (auto& c : cmds_) c->execute(); }
    void undo()    override {
        for (auto it = cmds_.rbegin(); it != cmds_.rend(); ++it) (*it)->undo();
    }
};

4. When to Use / When Not To

Use when:

  • Need undo/redo.
  • Need to queue, log, schedule, or serialize operations.
  • Want to parameterize widgets/menus with actions decoupled from receivers.
  • Building a transactional system — commands replayable for recovery.
  • Implementing macros or scripting on top of an app.

Don't use when:

  • Operation runs once, never queued or undone → direct call.
  • All you need is "pass a callable" → std::function / lambda directly.
  • The class explosion (one Command type per action) is unjustified by the use case.

5. Variants and Pitfalls

Variants:

  • Reversible Commandexecute + undo. State captured at execution time.
  • Snapshot Command — pair with Memento; before-state stored in command.
  • Logged Command — serializable for replay/event sourcing.
  • Queued / Async Command — futures (std::packaged_task ≈ Command + result).
  • Composite (Macro) — vector of subcommands; undo reverses in reverse order.

Pitfalls:

  • State capture at execute time vs construction time. If the document changes between construction and execute, undo of "insert at pos 5" may delete the wrong content. Capture state when execute() runs, not before.
  • Undo stack memory. Every text edit kept forever → unbounded growth. Cap the stack or compress consecutive commands (typing 100 chars → one CompositeInsertCmd).
  • Coupling to mutable receiver. Command stores Receiver&; receiver gets destroyed → dangling. Use weak_ptr or document lifetime ownership.
  • Order dependency in macros. Undo reverses execute order; assert reversibility per step.
  • Non-deterministic execution (commands depending on wall-clock, RNG) breaks replay. Inject the source so the command captures the values.

6. Related Patterns

  • Command vs Strategy — both encapsulate behavior. Command = a request to do something at a specific time, often undoable, often queued; one-shot. Strategy = a pluggable algorithm sitting inside a context, called repeatedly. Command knows the receiver + args; Strategy knows only the algorithm.
  • Command vs Memento — Command records the action; Memento snapshots the state. Together → full undo/redo: execute Command, save Memento; undo by restoring Memento (or by inverse-executing Command). Memento-based undo is simpler but heavier in memory.
  • Command vs Observer — Observer notifies; subscribers react. Command is the request itself. Observer can deliver Commands ("on click, run this Command").
  • Command vs Chain of Responsibility — CoR routes a request through handlers; the request itself is often a Command object.
  • Command vs Visitor — Visitor dispatches by element type; Command dispatches by action. Both turn behavior into objects.

7. References