
C++命令模式与实际应用:从理论到实战的优雅解耦
大家好,今天我想和大家深入聊聊C++中的命令模式。这个模式在我多年的项目经历中,尤其是处理用户操作、任务队列和撤销/重做功能时,简直是“救命稻草”。它听起来有点抽象,但一旦用起来,你就会发现它能把复杂的调用关系梳理得清清楚楚。让我们抛开枯燥的理论,直接看看它如何在实际编码中“大显身手”。
一、为什么需要命令模式?一个真实的痛点
还记得我早期参与的一个图形编辑器项目吗?我们需要实现一堆功能:绘制图形、删除元素、修改颜色、调整大小。最原始的写法可能是这样的:
// 糟糕的写法:UI层直接耦合具体操作
void onDrawCircleClicked() {
canvas->drawCircle(x, y, radius);
}
void onDeleteShapeClicked() {
canvas->deleteSelectedShape();
}
// ...无数个这样的回调函数
很快问题就来了:撤销(Undo)功能怎么做? 每个操作执行的方式和需要回退的数据都不同,代码里充满了各种“if-else”和状态保存,像一团乱麻。这时,命令模式的价值就凸显了——它将一个请求封装成一个独立的对象,从而使你可用不同的请求对客户进行参数化,并支持请求的排队、记录、撤销等操作。
二、命令模式的核心骨架
命令模式通常包含几个关键角色:
- 命令接口(Command):声明执行操作的接口。
- 具体命令(ConcreteCommand):将一个接收者对象绑定于一个动作,实现Execute方法。
- 调用者(Invoker):要求命令执行请求。
- 接收者(Receiver):知道如何实施与执行一个请求相关的操作。
下面是一个最精简的C++实现框架:
#include
#include
// 接收者:真正干活的对象
class Receiver {
public:
void Action(const std::string& param) {
std::cout << "Receiver: 执行操作,参数为 " << param << std::endl;
}
void UndoAction(const std::string& param) {
std::cout << "Receiver: 撤销操作,参数为 " << param << std::endl;
}
};
// 1. 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void Execute() = 0;
virtual void Undo() = 0;
};
// 2. 具体命令
class ConcreteCommand : public Command {
private:
std::shared_ptr receiver_;
std::string param_;
public:
ConcreteCommand(std::shared_ptr recv, const std::string& param)
: receiver_(recv), param_(param) {}
void Execute() override {
receiver_->Action(param_);
}
void Undo() override {
receiver_->UndoAction(param_);
}
};
// 3. 调用者(例如一个按钮或遥控器)
class Invoker {
private:
std::shared_ptr command_;
public:
void SetCommand(std::shared_ptr cmd) {
command_ = cmd;
}
void OnButtonPressed() {
if (command_) command_->Execute();
}
void OnUndoPressed() {
if (command_) command_->Undo();
}
};
// 简单使用示例
int main() {
auto receiver = std::make_shared();
auto cmd = std::make_shared(receiver, "Hello Command");
Invoker invoker;
invoker.SetCommand(cmd);
invoker.OnButtonPressed(); // 输出:Receiver: 执行操作,参数为 Hello Command
invoker.OnUndoPressed(); // 输出:Receiver: 撤销操作,参数为 Hello Command
return 0;
}
这个框架虽然简单,但已经包含了命令模式的精髓:把“做什么”和“谁来做”、“什么时候做”分开了。Invoker根本不知道Receiver的具体细节,它只和Command接口打交道。
三、实战进阶:实现一个可撤销的图形编辑器
让我们回到开头的图形编辑器问题。假设我们有一个简单的画布(Canvas)作为Receiver,需要支持绘制矩形和撤销操作。
#include
#include
#include
// 接收者:画布
class Canvas {
std::vector shapes_;
public:
void DrawRectangle(const std::string& color) {
shapes_.push_back("矩形[" + color + "]");
std::cout << "画布: 绘制了一个" << color << "色的矩形。当前图形数: " << shapes_.size() << std::endl;
}
void EraseLastShape() {
if (!shapes_.empty()) {
std::cout << "画布: 擦除了最后一个图形: " << shapes_.back() <DrawRectangle(color_);
}
void Undo() override {
canvas_->EraseLastShape();
}
};
// 关键!命令管理器(增强的Invoker),用于支持多级撤销/重做
class CommandManager {
std::stack<std::unique_ptr> undoStack_;
std::stack<std::unique_ptr> redoStack_;
public:
void ExecuteCommand(std::unique_ptr cmd) {
cmd->Execute();
undoStack_.push(std::move(cmd));
// 执行新命令时,清空重做栈
while(!redoStack_.empty()) redoStack_.pop();
}
void Undo() {
if (undoStack_.empty()) return;
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
cmd->Undo();
redoStack_.push(std::move(cmd));
}
void Redo() {
if (redoStack_.empty()) return;
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
cmd->Execute();
undoStack_.push(std::move(cmd));
}
};
int main() {
Canvas canvas;
CommandManager cmdManager;
std::cout << "--- 开始操作 ---" << std::endl;
cmdManager.ExecuteCommand(std::make_unique(&canvas, "红色"));
cmdManager.ExecuteCommand(std::make_unique(&canvas, "蓝色"));
std::cout << "n--- 执行撤销 ---" << std::endl;
cmdManager.Undo(); // 撤销蓝色矩形
std::cout << "n--- 执行重做 ---" << std::endl;
cmdManager.Redo(); // 重做蓝色矩形
return 0;
}
运行这个程序,你会看到清晰的操作流水账。CommandManager 成为了系统的控制中心,任何需要支持撤销的操作,只需封装成 CanvasCommand 的子类并交给它执行即可。添加新的图形(如圆形)只需新增一个 `DrawCircleCommand` 类,完全符合开闭原则。
四、踩坑提示与性能考量
在实际使用中,我有几点经验分享:
1. 命令对象的生命周期: 示例中用了 `unique_ptr` 来管理命令对象的所有权,这很安全。如果命令需要异步执行或跨线程,则需考虑使用 `shared_ptr` 并注意线程安全。
2. 命令的参数存储: 如果命令执行需要复杂的初始状态(如图形的所有顶点坐标),务必在命令对象构造时深拷贝这些数据。我曾因为存储了指针,而原始数据被修改,导致撤销时状态错误。
3. 性能与内存: 对于高频、简单的操作(如每秒数千次的移动命令),为每个操作都 new 一个命令对象可能带来开销。这时可以考虑使用对象池或内联存储(如将简单命令的数据直接存储在栈或固定大小容器中)。
4. 复合命令(宏命令): 这是命令模式一个强大的扩展。你可以创建一个 `MacroCommand`,它内部维护一个命令列表,其 `Execute()` 和 `Undo()` 按顺序调用所有子命令。这对于实现“组合操作”(如“分组”、“批量移动”)非常有用。
class MacroCommand : public CanvasCommand {
std::vector<std::unique_ptr> commands_;
public:
void AddCommand(std::unique_ptr 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();
}
}
};
五、总结:何时该使用命令模式?
经过上面的探讨,命令模式并非银弹,但在以下场景中,它能极大地提升代码的健壮性和可维护性:
- 需要支持撤销/重做功能。 这是它的“杀手级”应用。
- 需要将操作排队、记录日志或远程发送。 命令对象是序列化的绝佳候选。
- 需要支持事务操作。 一组命令要么全部成功,要么全部回滚。
- UI动作与业务逻辑需要解耦。 让菜单项、按钮等仅持有命令对象。
命令模式的魅力在于,它通过一个简单的“中间层”(命令对象),将发起请求的对象与执行请求的对象解耦。这种解耦带来的灵活性,在应对需求变化时显得尤为珍贵。希望这篇结合实战的文章,能帮助你在下一个C++项目中,优雅地驾驭命令模式。

评论(0)