命令模式的核心是将请求封装为对象,实现发送者与接收者解耦;支持排队、撤销、日志和远程调用;需通过Command基类统一接口、注入接收者、保存状态快照以保障undo可靠性。

命令模式的核心是把“请求”封装成对象
不是用函数指针或 lambda 临时应付,而是让每个具体操作(比如打开文件、撤销上一步)变成一个独立的 Command 类实例。这样做的直接好处是:能排队、能撤销、能记录日志、能远程发送——因为“做什么”和“谁来做”彻底解耦了。
关键设计点:Command 基类必须有统一接口(通常是 execute()),但不关心执行逻辑;真正干活的代码放在子类里,通过构造时传入的接收者(Receiver)来调用具体方法。
常见错误是把接收者逻辑直接写死在 execute() 里,导致命令无法复用。正确做法是让命令只持有对 Receiver 的引用或指针,并在构造时注入。
怎么写一个可撤销的命令子类
撤销不是靠猜,而是靠命令自己保存执行前的状态,或者提供对应的反向操作。比如 LightOnCommand 要能关灯,就得在 execute() 里先记下灯原来是否开着,或者直接配一个 LightOffCommand 作为它的 undo() 配对。
实操建议:
-
undo()方法必须与execute()对称:如果execute()改了某个成员变量,undo()就得把它设回原值(所以构造时要存快照) - 不要在
undo()里重新 new 对象或做 I/O——它应该快且无副作用 - 如果接收者本身不支持状态回滚(比如网络请求已发出),那这个命令就不该实现
undo(),或者抛出std::runtime_error("cannot undo")
示例片段:
class LightOnCommand : public Command {
Light& light;
bool was_on;
public:
LightOnCommand(Light& l) : light(l), was_on(l.is_on()) {}
void execute() override { light.turn_on(); }
void undo() override { if (was_on) light.turn_on(); else light.turn_off(); }
};
Invoker 怎么安全持有并执行命令
Invoker 不该知道具体是哪个 Command 子类,只依赖基类指针。但它得管生命周期——尤其当命令带捕获上下文(比如 lambda 包着局部变量)时,裸指针会悬空。
推荐用 std::unique_ptr,既明确所有权,又避免误删。如果需要重复执行同一命令(比如快捷键连按),就用 std::shared_ptr,但得确认接收者在整个生命周期内有效。
容易踩的坑:
- 把栈上创建的
Command子类对象地址传给Invoker→ 立刻悬空 - 在
execute()里调用已销毁的Receiver成员 → 段错误,调试时很难定位 - 忘记在
Invoker的析构函数里清空命令队列 → 内存泄漏或重复释放
C++17 之后可以用 std::function 替代继承吗
可以简化简单场景,但会丢失类型信息和撤销能力。比如:
using CommandFunc = std::function<void>;
using UndoFunc = std::function<void>;
struct FunctionCommand : Command {
CommandFunc exec;
UndoFunc undo;
FunctionCommand(CommandFunc e, UndoFunc u) : exec(e), undo(u) {}
void execute() override { exec(); }
void undo() override { undo(); }
};</void></void>
这种写法适合脚本化绑定或配置驱动的命令(比如从 JSON 加载动作),但代价是:无法静态识别命令类型、不能加字段存状态、RTTI 失效、模板推导受限。真要支持撤销+重做+日志,还是老老实实写子类更可控。
最常被忽略的一点:命令对象之间的依赖关系(比如“保存”命令必须等“格式化”命令完成)不会自动建立,得靠外部协调器或显式设置前置条件——这和语言无关,是模式本身的约束。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











