Command接口定义含execute()和可选undo()的抽象基类,具体命令类继承它并封装接收者调用逻辑;推荐Command仅存必要参数,执行时传入Receiver引用,undo默认空实现以保持接口简洁。

Command 接口和具体命令类怎么写
命令模式的核心是把「请求」封装成对象,让调用者(Invoker)和执行者(Receiver)解耦。C++ 里最直接的方式是定义一个抽象基类 Command,带纯虚函数 execute() 和可选的 undo()。
注意:不要在 Command 构造时直接传入 Receiver 的裸指针并长期持有——容易悬空。推荐用 std::shared_ptr 或把 Receiver 作为引用/值在 execute() 调用时传入。
常见错误是把 Receiver 的逻辑全塞进 Command 里,结果 Command 变得臃肿、难以复用。正确做法是 Command 只保存必要参数(比如目标 ID、数值、状态码),真正干活交给 Receiver。
示例:
立即学习“C++免费学习笔记(深入)”;
class Receiver {
public:
void action(int value) { /* 实际逻辑 */ }
};
<p>class Command {
public:
virtual ~Command() = default;
virtual void execute(Receiver& r) = 0;
};</p><p>class ConcreteCommand : public Command {
int m_value;
public:
explicit ConcreteCommand(int v) : m_value(v) {}
void execute(Receiver& r) override {
r.action(m_value);
}
};Invoker 如何统一调度不同命令
Invoker 不需要知道具体是哪个 Command 子类,只依赖基类接口。它通常持有一个 std::vector<std::unique_ptr<Command>> 或支持栈式操作的容器(用于撤销/重做)。
关键点:Invoker 的 execute() 方法应接受 Receiver 实例(或其引用),而不是在内部 new Receiver——否则会破坏控制权,也难测试。
容易踩的坑:
- 把命令执行逻辑写死在 Invoker 里(比如 switch-case 判断命令类型),这等于绕过了模式价值
- 用 raw pointer 管理 Command 生命周期,导致提前析构或重复 delete
- 忽略异常安全:若某个
execute()抛异常,后续命令是否该跳过?Invoker 需明确策略
如何支持 undo / redo 而不污染 Command 接口
不是所有命令都需要 undo,硬加 undo() 到基类会导致接口膨胀。更灵活的做法是让需要撤销的命令实现 UndoableCommand 接口(继承自 Command),Invoker 检查动态类型再调用。
但实际项目中,更常见且轻量的做法是:在 Command 基类里声明虚函数 undo() 并默认空实现({}),子类按需重写。这样 Invoker 可无差别调用,无需类型判断。
性能影响很小,因为虚函数开销可忽略;兼容性也好,旧命令不用改,新命令加实现即可。
示例补充:
class Command {
public:
virtual void execute(Receiver& r) = 0;
virtual void undo(Receiver& r) {} // 默认空实现
};
<p>class SetVolumeCommand : public Command {
int m_old_vol, m_new_vol;
public:
SetVolumeCommand(int old_v, int new_v) : m_old_vol(old_v), m_new_vol(new_v) {}
void execute(Receiver& r) override { r.setVolume(m_new_vol); }
void undo(Receiver& r) override { r.setVolume(m_old_vol); }
};什么时候不该用 Command 模式
命令模式适合「请求可排队、可记录、可撤销、可参数化」的场景,比如 GUI 操作、宏录制、事务回滚。但它不是万能胶。
以下情况建议绕过:
- 逻辑极简单的一次性调用(比如
button.onClick = []{ save(); };),引入 Command 反而增加间接层 - 高频实时调用(如游戏每帧输入处理),虚函数+堆分配可能带来不可接受的开销
- Receiver 本身已高度抽象(如 event bus + handler 注册),再套一层 Command 属于叠床架屋
真正难的是边界判断:不是“能不能写”,而是“值不值得为这个操作建 Command”。多数人低估了维护命令生命周期和状态快照的成本。


















