☰
C++设计模式之命令模式
2026/10/26 2:35:30 网站建设 项目流程

前言


命令模式(Command Pattern)解决的问题是:把「一个请求」封装成对象,让调用者不需要知道请求怎么执行、由谁执行,也能对请求做排队、记录、撤销、组合。


最典型的场景是界面上的按钮。按钮被按下时,它不应该知道自己是开灯、保存文件还是发送邮件;它只需要持有一个「命令」对象,调用它的execute()。这样按钮和业务逻辑解耦,同一套按钮可以配置不同的命令,命令也可以被记录进历史栈实现撤销。


常见的误解是「命令模式就是函数指针」或「就是std::function」。用回调确实能实现「按下即执行」,但命令模式还要求命令是对象:有类型、有状态、可以多态、可以被存储和复制、可以支持撤销。std::function适合简单的回调,命令模式适合需要撤销、队列、日志、宏组合的场景。


本文按「角色划分 → C++17 实现 → 撤销机制 → 宏命令」的顺序讲,最后给出完整示例和常见坑点。


一、角色划分


命令模式有五个角色:




角色职责



Command(命令接口)声明execute(),通常还有undo()

ConcreteCommand绑定一个接收者,实现具体的执行与撤销

Receiver(接收者)真正干活的对象,知道业务细节

Invoker(调用者)持有命令,在合适的时机触发

Client(客户端)创建命令、绑定接收者、装配到调用者



关键关系是:调用者只依赖 Command 接口,不依赖 Receiver。接收者的类型和调用方式被封装在具体命令里。


这和策略模式(Strategy)容易混淆。策略模式关注「同一件事的不同算法」,调用者知道自己在选算法;命令模式关注「把请求本身对象化」,调用者通常不知道请求的具体内容,甚至可以在之后才决定是否执行。


二、接口设计


// 适用于 C++17
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};

三个要点:



  • 虚析构函数必须有。调用者通常用std::unique_ptr<Command>持有命令,删除时通过基类指针,没有虚析构就是未定义行为。

  • execute()通常是void。如果需要返回值,说明它更像一次查询而不是命令。

  • undo()不是所有命令都需要。不需要撤销时可以不放在接口里,或者提供一个空实现。放在接口里会让所有具体命令都被迫实现,接口更统一,但不够灵活。


如果某些命令不支持撤销,可以把接口拆成Command和UndoableCommand两层,调用者在记录历史前用dynamic_cast判断。这引入了运行期类型检查,是否值得取决于具体需求。


三、具体命令与接收者


以智能灯为例:


// 适用于 C++17
#include <iostream>

class 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_ptr<Light>或std::weak_ptr,并在执行前检查有效性。


命令对象本身可以是无状态的(除了接收者引用),这意味着同一个命令实例可以被反复执行。如果命令需要记录「执行前的状态」来支持撤销,就必须为每次执行保存快照,此时命令是有状态的,不能随意复用。


四、调用者、撤销与宏命令


调用者持有一个命令,并可选地维护历史:


// 适用于 C++17
#include <memory>
#include <utility>
#include <vector>

class RemoteControl {
public:
void set_command(std::unique_ptr<Command> 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_ptr<Command> command_;
std::vector<std::unique_ptr<Command>> history_;
};

press()里用std::move把命令移进历史。移动后command_变为空,下一次press()什么也不做。这防止同一个命令被重复执行两次,但也意味着「按一次按钮就消耗一个命令」。如果希望反复执行同一个命令,就不要移动,而是保存历史副本——但Command是多态类型,不能直接拷贝,需要提供clone()。


另一种设计是历史只记录「已执行的命令」,命令由调用者长期持有,press()执行但不转移所有权。两种都可以,取决于命令的生命周期语义。


undo_last()只撤销最近一条,符合栈式历史。若要支持重做(redo),需要两个栈:撤销时把命令从 undo 栈移到 redo 栈,重做时反向移动。


4.1 宏命令


命令对象可以组合:一个宏命令持有一组命令,execute()依次执行,undo()逆序撤销。


// 适用于 C++17
class MacroCommand : public Command {
public:
void add(std::unique_ptr<Command> 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::vector<std::unique_ptr<Command>> commands_;
};

逆序撤销是关键:如果执行顺序是「开灯、开空调」,撤销必须「关空调、关灯」,否则中间状态可能不一致。这也是组合模式(Composite)与命令模式结合的典型用法。


完整示例


// 适用于 C++17
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>

class 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::vector<std::unique_ptr<Command>> history;

history.push_back(std::make_unique<LightOnCommand>(light));
history.back()->execute();

history.push_back(std::make_unique<LightOffCommand>(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是动态创建又提前销毁的,历史里的命令就会持有悬垂引用。


常见坑点



  1. ❌ 命令接口没有虚析构:


``cpp class Command { public: virtual void execute() = 0; // 缺少 virtual ~Command() };`✅ 加virtual ~Command() = default;`,否则通过基类指针删除是 UB。



  1. ❌ 命令持有接收者的引用,但接收者先销毁:


``cpp auto cmd = std::make_unique<LightOnCommand>(*tempLight); delete tempLight; cmd->execute(); // 悬垂引用`✅ 用shared_ptr` 延长生命周期,或确保接收者活得更久。



  1. ❌ 试图拷贝多态命令:


``cpp std::unique_ptr<Command> copy = original; // 编译错误`✅ 提供clone()虚函数,返回std::unique_ptr<Command>`。



  1. ❌ 用std::function<void()>代替命令,却需要撤销:


``cpp std::function<void()> cmd = [&] { light.on(); }; // 无法 undo`` ✅ 需要撤销就定义命令类,把「执行」和「反向操作」成对封装。



  1. ❌ 撤销时按正序执行:


``cpp for (auto& c : commands_) c->undo(); // 顺序错了`` ✅ 宏命令逆序撤销。



  1. ❌ 命令执行后仍保留在调用者手里,导致重复执行:


``cpp void press() { command_->execute(); } // 连按两次执行两次`` ✅ 明确语义:要么执行后转移所有权,要么允许重复执行并在文档中说明。



  1. ❌ 在命令里保存接收者的值拷贝,导致撤销作用在副本上:


``cpp Light light_; // 拷贝,撤销改的是副本`` ✅ 保存引用或指针,确保操作的是同一个对象。



  1. ❌ 为了「将来可能扩展」给每个操作都套一层命令,导致简单调用被过度抽象。


✅ 只在需要排队、撤销、日志、组合时引入命令模式。


总结




组成作用注意



Command统一接口虚析构必须有

ConcreteCommand绑定接收者注意接收者生命周期

Receiver执行业务与调用者解耦

Invoker触发命令可维护历史栈

MacroCommand组合命令撤销要逆序



命令模式的价值在于把「请求」提升为一等对象。一旦请求是对象,就能被存储、排队、序列化、撤销和组合。代价是每个操作多一个类、多一层间接。判断是否值得,只需要问一句:这些请求需不需要被当作数据来管理。需要,就用;不需要,一个std::function或直接调用更合适。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询