C++设计模式总结-汇总了全部23种设计模式的详细说明
第20种:命令模式
一、基本介绍
命令模式(Command Pattern)是一种行为型设计模式,其核心思想是将 “请求” 封装为独立的对象,从而允许通过不同的请求对客户端进行参数化。这种封装机制使得我们可以灵活地对请求进行排队、记录日志、撤销 / 重做操作,以及支持事务处理等高级功能。
命令模式的本质是将行为请求者与行为实现者解耦。在传统的函数调用中,调用者必须直接依赖具体的实现类。而命令模式通过引入中间层(命令对象),使得调用者只需与命令对象交互,无需关心具体的接收者是谁以及如何实现。如同智能家居遥控器:用户通过统一按钮控制不同设备(空调、灯光、窗帘),无需了解具体设备的操作细节。
模式四大核心角色
角色 | 职责说明 | 类比示例 |
---|---|---|
抽象命令Command | 声明执行操作的接口,通常包含 execute () 方法,可以扩展 undo () 等其他方法 | 遥控器按钮通用协议 |
具体命令ConcreteCommand | 实现抽象命令接口,持有接收者对象的引用 ,实现具体的执行逻辑 | "开灯"按钮的电路设计 |
调用者 Invoker | 触发命令执行的调用者,负责触发命令执行,维护命令对象的引用 | 遥控器外壳按键 |
接收者Receiver | 真正执行请求的对象,负责实现具体的业务逻辑 | 智能灯泡设备 |
这四个角色通过松耦合的方式协作,形成一个灵活的请求处理体系。
与策略模式的区别
// 命令模式关注操作封装
class Command { virtual void execute() = 0; };
// 策略模式侧重算法替换
class Strategy { virtual void algorithm() = 0; };
策略模式实现算法族的互换,而命令模式强调操作的生命周期管理和执行控制。
为什么需要命令模式?
让我们通过一个生活中的例子来理解命令模式的价值:
假设我们有一个万能遥控器,可以控制多种家电设备。传统的做法是在遥控器上为每个设备单独设置按键,这样当新增设备时就需要修改遥控器的代码。而使用命令模式后,我们可以将每个设备的操作封装为独立的命令对象,遥控器只需要管理这些命令对象即可。新增设备时只需添加新的命令类,无需修改遥控器本身。
这种设计带来的优势包括:
- 解耦调用者与接收者;
- 支持请求的参数化配置;
- 方便实现撤销 / 重做功能;
- 便于记录请求日志;
- 支持请求队列与事务处理;
二、内部原理剖析
1. 请求封装机制
每个具体命令对象包含接收者引用和操作参数,通过execute()方法桥接调用:
class LightOnCommand : public Command {
Light* light; // 接收者
public:
explicit LightOnCommand(Light* l) : light(l) {}
void execute() override {
light->turnOn(); // 委托接收者执行
}
};
该设计将"按下按钮"与"灯泡亮起"解耦为两个独立对象。
2. 动态命令处理
调用者通过命令队列支持复杂操作管理:
class RemoteControl { // Invoker
std::queue<Command*> cmdQueue;
public:
void addCommand(Command* cmd) {
cmdQueue.push(cmd);
}
void executeAll() {
while(!cmdQueue.empty()) {
cmdQueue.front()->execute();
cmdQueue.pop();
}
}
};
此机制支持批量操作和异步执行。
三、应用场景详解
1. GUI应用中的撤销 / 重做功能的操作控制
在文本编辑器、绘图软件等 GUI 应用中,撤销 / 重做是常见需求。命令模式可以完美支持这一功能:
- 每次操作(如插入文字、绘制图形)都封装为命令对象;
- 维护两个栈:已执行命令栈和已撤销命令栈;
- 撤销操作时,将当前命令弹出执行 undo (),并压入撤销栈;
- 重做操作时,从撤销栈弹出命令执行 redo (),并压入执行栈;
class TextEditor {
public:
void executeCommand(Command* cmd) {
cmd->execute();
history.push(cmd);
redoStack.clear();
}
void undo() {
if (!history.empty()) {
Command* cmd = history.top();
cmd->undo();
redoStack.push(cmd);
history.pop();
}
}
void redo() {
if (!redoStack.empty()) {
Command* cmd = redoStack.top();
cmd->redo();
history.push(cmd);
redoStack.pop();
}
}
private:
stack<Command*> history;
stack<Command*> redoStack;
};
2. 日志记录与事务处理
在需要保证数据一致性的系统中,命令模式可以结合日志实现事务回滚:
- 执行命令前记录操作日志;
- 如果命令执行失败,根据日志回滚操作;
- 支持事务的提交和回滚操作;
class DatabaseCommand : public Command {
public:
void execute() override {
// 执行数据库操作
logManager->logOperation("UPDATE table SET ...");
}
void undo() override {
// 根据日志回滚操作
logManager->undoLastOperation();
}
};
四、使用方法指南
步骤1:创建抽象命令类,定义命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
步骤2:实现具体命令的类
class LightOnCommand : public Command {
public:
LightOnCommand(Light* light) : lightReceiver(light) {}
void execute() override {
lightReceiver->turnOn();
prevState = lightReceiver->getState();
}
void undo() override {
lightReceiver->setState(prevState);
}
private:
Light* lightReceiver;
LightState prevState;
};
class LightOffCommand : public Command {
public:
LightOffCommand(Light* light) : lightReceiver(light) {}
void execute() override {
lightReceiver->turnOff();
prevState = lightReceiver->getState();
}
void undo() override {
lightReceiver->setState(prevState);
}
private:
Light* lightReceiver;
LightState prevState;
};
步骤3:定义接收者类
class Light {
public:
void turnOn() { state = ON; }
void turnOff() { state = OFF; }
LightState getState() const { return state; }
void setState(LightState newState) { state = newState; }
private:
LightState state = OFF;
};
步骤4: 实现调用者类
class RemoteControl {
public:
void setCommand(Command* cmd) { currentCommand = cmd; }
void pressButton() {
if (currentCommand) {
currentCommand->execute();
history.push(currentCommand);
}
}
void pressUndoButton() {
if (!history.empty()) {
Command* lastCmd = history.top();
lastCmd->undo();
history.pop();
}
}
private:
Command* currentCommand = nullptr;
stack<Command*> history;
};
步骤3:客户端使用示例
int main() {
Light livingRoomLight;
Command* onCmd = new LightOnCommand(&livingRoomLight);
Command* offCmd = new LightOffCommand(&livingRoomLight);
RemoteControl remote;
remote.setCommand(onCmd);
remote.pressButton(); // 开灯
remote.setCommand(offCmd);
remote.pressButton(); // 关灯
remote.pressUndoButton(); // 撤销关灯操作,恢复开灯状态
delete onCmd;
delete offCmd;
return 0;
}
五、常见问题与解决方案
问题1:内存泄漏风险
现象:频繁创建/销毁命令对象导致内存碎片。
解决方案:
- 使用对象池技术复用命令对象;
- 采用智能指针自动管理生命周期;
std::unique_ptr<Command> cmd(new LightOnCommand(light));
问题2:撤销/重做实现
技术难点:需要精确记录操作历史。
实现方案:
class HistoryManager {
std::stack<Command*> undoStack;
std::stack<Command*> redoStack;
public:
void execute(Command* cmd) {
cmd->execute();
undoStack.push(cmd);
}
void undo() {
if(!undoStack.empty()) {
auto cmd = undoStack.top();
cmd->undo();
redoStack.push(cmd);
undoStack.pop();
}
}
};
问题3:多线程竞争
隐患:并发执行导致状态不一致。
解决方法:
- 使用互斥锁保护关键操作;
- 采用命令队列实现序列化执行;
std::mutex mtx;
void ThreadSafeExecute(Command* cmd) {
std::lock_guard<std::mutex> lock(mtx);
cmd->execute();
}
六、总结:命令模式的优势与适用场景
1. 核心优势总结
- 解耦性:调用者与接收者完全分离,提高系统灵活性;
- 扩展性:新增命令无需修改现有代码,符合开闭原则;
- 事务支持:通过撤销 / 重做机制实现可靠的事务处理;
- 日志记录:所有操作都可以方便地记录和回放;
- 异步处理:天然支持任务队列和多线程处理;
2. 适用场景分析
- 需要支持撤销 / 重做功能的系统;
- 需要记录操作日志的系统;
- 需要处理异步请求的系统;
- 需要实现命令队列或批处理的系统;
- 需要参数化配置操作的系统;
3. 设计权衡建议
- 复杂度:当命令逻辑简单时,使用命令模式可能增加系统复杂度;
- 性能:对于高频操作,需注意命令对象的创建开销;
- 内存管理:在 C++ 中需特别注意命令对象的生命周期管理;
4。 未来发展趋势
- 结合 lambda 表达式简化命令实现;
- 与 Actor 模型结合实现分布式命令处理;
- 在游戏开发中结合 ECS(实体组件系统)模式;
通过深入理解命令模式的核心思想和实现细节,我们可以在实际项目中灵活运用这一模式,构建出更具弹性和扩展性的软件系统。命令模式不仅是一种设计模式,更是一种将请求抽象化的思维方式,这种思维方式在现代软件开发中具有广泛的应用价值。