C++命令模式与实际应用插图

C++命令模式与实际应用:从理论到实战的优雅解耦

大家好,今天我想和大家深入聊聊C++中的命令模式。这个模式在我多年的项目经历中,尤其是处理用户操作、任务队列和撤销/重做功能时,简直是“救命稻草”。它听起来有点抽象,但一旦用起来,你就会发现它能把复杂的调用关系梳理得清清楚楚。让我们抛开枯燥的理论,直接看看它如何在实际编码中“大显身手”。

一、为什么需要命令模式?一个真实的痛点

还记得我早期参与的一个图形编辑器项目吗?我们需要实现一堆功能:绘制图形、删除元素、修改颜色、调整大小。最原始的写法可能是这样的:

// 糟糕的写法:UI层直接耦合具体操作
void onDrawCircleClicked() {
    canvas->drawCircle(x, y, radius);
}
void onDeleteShapeClicked() {
    canvas->deleteSelectedShape();
}
// ...无数个这样的回调函数

很快问题就来了:撤销(Undo)功能怎么做? 每个操作执行的方式和需要回退的数据都不同,代码里充满了各种“if-else”和状态保存,像一团乱麻。这时,命令模式的价值就凸显了——它将一个请求封装成一个独立的对象,从而使你可用不同的请求对客户进行参数化,并支持请求的排队、记录、撤销等操作。

二、命令模式的核心骨架

命令模式通常包含几个关键角色:

  1. 命令接口(Command):声明执行操作的接口。
  2. 具体命令(ConcreteCommand):将一个接收者对象绑定于一个动作,实现Execute方法。
  3. 调用者(Invoker):要求命令执行请求。
  4. 接收者(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++项目中,优雅地驾驭命令模式。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。