
前言命令模式Command Pattern解决的问题是把「一个请求」封装成对象让调用者不需要知道请求怎么执行、由谁执行也能对请求做排队、记录、撤销、组合。最典型的场景是界面上的按钮。按钮被按下时它不应该知道自己是开灯、保存文件还是发送邮件它只需要持有一个「命令」对象调用它的execute()。这样按钮和业务逻辑解耦同一套按钮可以配置不同的命令命令也可以被记录进历史栈实现撤销。常见的误解是「命令模式就是函数指针」或「就是std::function」。用回调确实能实现「按下即执行」但命令模式还要求命令是对象有类型、有状态、可以多态、可以被存储和复制、可以支持撤销。std::function适合简单的回调命令模式适合需要撤销、队列、日志、宏组合的场景。本文按「角色划分 → C17 实现 → 撤销机制 → 宏命令」的顺序讲最后给出完整示例和常见坑点。一、角色划分命令模式有五个角色角色职责Command命令接口声明execute()通常还有undo()ConcreteCommand绑定一个接收者实现具体的执行与撤销Receiver接收者真正干活的对象知道业务细节Invoker调用者持有命令在合适的时机触发Client客户端创建命令、绑定接收者、装配到调用者关键关系是调用者只依赖 Command 接口不依赖 Receiver。接收者的类型和调用方式被封装在具体命令里。这和策略模式Strategy容易混淆。策略模式关注「同一件事的不同算法」调用者知道自己在选算法命令模式关注「把请求本身对象化」调用者通常不知道请求的具体内容甚至可以在之后才决定是否执行。二、接口设计// 适用于 C17class Command {public:virtual ~Command() default;virtual void execute() 0;virtual void undo() 0;};三个要点虚析构函数必须有。调用者通常用std::unique_ptrCommand持有命令删除时通过基类指针没有虚析构就是未定义行为。execute()通常是void。如果需要返回值说明它更像一次查询而不是命令。undo()不是所有命令都需要。不需要撤销时可以不放在接口里或者提供一个空实现。放在接口里会让所有具体命令都被迫实现接口更统一但不够灵活。如果某些命令不支持撤销可以把接口拆成Command和UndoableCommand两层调用者在记录历史前用dynamic_cast判断。这引入了运行期类型检查是否值得取决于具体需求。三、具体命令与接收者以智能灯为例// 适用于 C17#include iostreamclass Light {public:void on() { on_ true; std::cout light on\n; }void off() { on_ false; std::cout light off\n; }bool is_on() const { return on_; }private:bool on_ false;};class LightOnCommand : public Command {public:explicit LightOnCommand(Light light) : light_(light) {}void execute() override { light_.on(); }void undo() override { light_.off(); }private:Light light_;};LightOnCommand持有Light这是一个非拥有引用。接收者必须比命令活得久。如果接收者可能提前销毁就要改用std::shared_ptrLight或std::weak_ptr并在执行前检查有效性。命令对象本身可以是无状态的除了接收者引用这意味着同一个命令实例可以被反复执行。如果命令需要记录「执行前的状态」来支持撤销就必须为每次执行保存快照此时命令是有状态的不能随意复用。四、调用者、撤销与宏命令调用者持有一个命令并可选地维护历史// 适用于 C17#include memory#include utility#include vectorclass RemoteControl {public:void set_command(std::unique_ptrCommand cmd) {command_ std::move(cmd);}void press() {if (!command_) return;command_-execute();history_.push_back(std::move(command_)); // 交给历史command_ 置空}void undo_last() {if (history_.empty()) return;history_.back()-undo();history_.pop_back();}private:std::unique_ptrCommand command_;std::vectorstd::unique_ptrCommand history_;};press()里用std::move把命令移进历史。移动后command_变为空下一次press()什么也不做。这防止同一个命令被重复执行两次但也意味着「按一次按钮就消耗一个命令」。如果希望反复执行同一个命令就不要移动而是保存历史副本——但Command是多态类型不能直接拷贝需要提供clone()。另一种设计是历史只记录「已执行的命令」命令由调用者长期持有press()执行但不转移所有权。两种都可以取决于命令的生命周期语义。undo_last()只撤销最近一条符合栈式历史。若要支持重做redo需要两个栈撤销时把命令从 undo 栈移到 redo 栈重做时反向移动。4.1 宏命令命令对象可以组合一个宏命令持有一组命令execute()依次执行undo()逆序撤销。// 适用于 C17class MacroCommand : public Command {public:void add(std::unique_ptrCommand cmd) {commands_.push_back(std::move(cmd));}void execute() override {for (auto cmd : commands_) {cmd-execute();}}void undo() override {for (auto it commands_.rbegin(); it ! commands_.rend(); it) {(*it)-undo();}}private:std::vectorstd::unique_ptrCommand commands_;};逆序撤销是关键如果执行顺序是「开灯、开空调」撤销必须「关空调、关灯」否则中间状态可能不一致。这也是组合模式Composite与命令模式结合的典型用法。完整示例// 适用于 C17#include iostream#include memory#include string#include utility#include vectorclass Command {public:virtual ~Command() default;virtual void execute() 0;virtual void undo() 0;};class Light {public:void on() { on_ true; std::cout [light] on\n; }void off() { on_ false; std::cout [light] off\n; }private:bool on_ false;};class LightOnCommand : public Command {public:explicit LightOnCommand(Light light) : light_(light) {}void execute() override { light_.on(); }void undo() override { light_.off(); }private:Light light_;};class LightOffCommand : public Command {public:explicit LightOffCommand(Light light) : light_(light) {}void execute() override { light_.off(); }void undo() override { light_.on(); }private:Light light_;};int main() {Light light;std::vectorstd::unique_ptrCommand history;history.push_back(std::make_uniqueLightOnCommand(light));history.back()-execute();history.push_back(std::make_uniqueLightOffCommand(light));history.back()-execute();std::cout -- undo --\n;while (!history.empty()) {history.back()-undo();history.pop_back();}return 0;}按标准语义输出依次是[light] on、[light] off、-- undo --、[light] on、[light] off。撤销顺序与执行顺序相反。这里light定义在main中生命周期覆盖整个history所以命令里的引用始终有效。如果Light是动态创建又提前销毁的历史里的命令就会持有悬垂引用。常见坑点❌ 命令接口没有虚析构cpp class Command { public: virtual void execute() 0; // 缺少 virtual ~Command() };✅ 加virtual ~Command() default;否则通过基类指针删除是 UB。❌ 命令持有接收者的引用但接收者先销毁cpp auto cmd std::make_uniqueLightOnCommand(*tempLight); delete tempLight; cmd-execute(); // 悬垂引用✅ 用shared_ptr 延长生命周期或确保接收者活得更久。❌ 试图拷贝多态命令cpp std::unique_ptrCommand copy original; // 编译错误✅ 提供clone()虚函数返回std::unique_ptrCommand。❌ 用std::functionvoid()代替命令却需要撤销cpp std::functionvoid() cmd [] { light.on(); }; // 无法 undo ✅ 需要撤销就定义命令类把「执行」和「反向操作」成对封装。❌ 撤销时按正序执行cpp for (auto c : commands_) c-undo(); // 顺序错了 ✅ 宏命令逆序撤销。❌ 命令执行后仍保留在调用者手里导致重复执行cpp void press() { command_-execute(); } // 连按两次执行两次 ✅ 明确语义要么执行后转移所有权要么允许重复执行并在文档中说明。❌ 在命令里保存接收者的值拷贝导致撤销作用在副本上cpp Light light_; // 拷贝撤销改的是副本 ✅ 保存引用或指针确保操作的是同一个对象。❌ 为了「将来可能扩展」给每个操作都套一层命令导致简单调用被过度抽象。✅ 只在需要排队、撤销、日志、组合时引入命令模式。总结组成作用注意Command统一接口虚析构必须有ConcreteCommand绑定接收者注意接收者生命周期Receiver执行业务与调用者解耦Invoker触发命令可维护历史栈MacroCommand组合命令撤销要逆序命令模式的价值在于把「请求」提升为一等对象。一旦请求是对象就能被存储、排队、序列化、撤销和组合。代价是每个操作多一个类、多一层间接。判断是否值得只需要问一句这些请求需不需要被当作数据来管理。需要就用不需要一个std::function或直接调用更合适。