18.C++设计模式-命令模式

在软件开发中,我们经常遇到这样的需求:需要将“请求”封装成一个对象,以便支持撤销操作、请求排队、日志记录或者将调用者与执行者解耦。命令模式(Command Pattern) 正是解决这类问题的利器。

但很多开发者能写出命令模式的代码,却未必真正理解它背后的哲学。如果说代码示例是“术”,那么设计思想就是“道”。本文将从理论到实践,再从实践升华到思维,带你彻底掌握命令模式的精髓。


一、什么是命令模式?

命令模式是一种行为型设计模式,核心思想是:将请求(Request)封装为一个独立的对象

在没有命令模式的代码中,调用者(Invoker)通常直接调用接收者(Receiver)的方法,两者紧密耦合。而命令模式在中间引入了一个 Command 接口,使得:

  • 调用者只知道如何触发命令,不关心具体业务逻辑。
  • 接收者只负责执行业务逻辑,不关心是谁触发的。
  • 命令对象承载了请求的所有信息(方法名、参数、接收者引用),使请求可以被存储、传递、排队甚至撤销。

核心角色

  1. Command(抽象命令):定义执行操作的接口(如 execute())。
  2. ConcreteCommand(具体命令):实现抽象命令,绑定接收者和动作。
  3. Receiver(接收者):真正执行业务逻辑的对象。
  4. Invoker(调用者):持有命令对象,通过命令对象发起请求。
  5. Client(客户端):创建具体命令并组装到调用者中。

逻辑结构图

uses

invokes

creates

configures

creates

«interface»

Command

+execute()

+undo()

ConcreteCommand

-receiver: Receiver

+execute()

+undo()

Receiver

+action()

Invoker

-command: Command

+setCommand(cmd)

+trigger()

Client

+main()

交互流程如下:

ReceiverConcreteCommandInvokerClientReceiverConcreteCommandInvokerClient用户触发操作new ConcreteCommand(R)setCommand(Cmd)execute()action()执行结果完成

二、典型应用场景

命令模式并非万能,但在以下场景中它是最佳选择:

场景说明示例
GUI 按钮/菜单将 UI 事件与业务逻辑解耦,同一个命令可绑定到多个 UI 元素点击菜单、快捷键、工具栏按钮都触发“保存”
撤销/重做(Undo/Redo)命令对象可保存状态或反向操作,支持历史栈管理文本编辑器、图形设计软件、游戏操作回放
任务队列/批处理命令对象可序列化、入队,异步或延迟执行线程池任务、消息队列消费者、定时任务
日志与事务恢复将命令持久化到磁盘,系统崩溃后可重放命令恢复状态数据库 WAL、分布式系统故障恢复
宏命令(Macro)将多个命令组合成一个复合命令批量执行IDE 中的重构操作、游戏中的连招系统

三、C++ 代码示例:智能遥控器

我们以一个智能家居遥控器为例,演示命令模式如何实现设备控制与撤销功能。

完整代码

#include <iostream>
#include <memory>
#include <stack>
#include <string>
#include <vector>

// ==================== 1. 抽象命令接口 ====================
class ICommand {
public:
    virtual ~ICommand() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;      // 支持撤销
    virtual std::string name() const = 0;
};

// ==================== 2. 接收者(具体设备) ====================
class Light {
    bool isOn_ = false;
public:
    void on()  { isOn_ = true;  std::cout << "  💡 灯已打开\n"; }
    void off() { isOn_ = false; std::cout << "  🌑 灯已关闭\n"; }
    bool state() const { return isOn_; }
};

class Stereo {
    int volume_ = 0;
public:
    void on()         { std::cout << "  🔊 音响已打开\n"; }
    void off()        { std::cout << "  🔇 音响已关闭\n"; }
    void setVolume(int v) { 
        volume_ = v; 
        std::cout << "  🎵 音量设为: " << v << "\n"; 
    }
    int volume() const { return volume_; }
};

// ==================== 3. 具体命令 ====================
class LightOnCommand : public ICommand {
    Light& light_;
public:
    explicit LightOnCommand(Light& l) : light_(l) {}
    void execute() override { light_.on(); }
    void undo() override    { light_.off(); }
    std::string name() const override { return "LightOn"; }
};

class LightOffCommand : public ICommand {
    Light& light_;
public:
    explicit LightOffCommand(Light& l) : light_(l) {}
    void execute() override { light_.off(); }
    void undo() override    { light_.on(); }
    std::string name() const override { return "LightOff"; }
};

class StereoVolumeCommand : public ICommand {
    Stereo& stereo_;
    int targetVol_;
    int prevVol_ = 0;  // 保存旧状态以支持撤销
public:
    StereoVolumeCommand(Stereo& s, int vol) : stereo_(s), targetVol_(vol) {}
    void execute() override { 
        prevVol_ = stereo_.volume(); 
        stereo_.setVolume(targetVol_); 
    }
    void undo() override { 
        stereo_.setVolume(prevVol_); 
    }
    std::string name() const override { return "StereoVolume"; }
};

// 宏命令:组合多个命令
class MacroCommand : public ICommand {
    std::vector<std::shared_ptr<ICommand>> commands_;
    std::string name_;
public:
    MacroCommand(std::string n, std::vector<std::shared_ptr<ICommand>> cmds)
        : name_(std::move(n)), commands_(std::move(cmds)) {}

    void execute() override {
        for (auto& cmd : commands_) cmd->execute();
    }
    void undo() override {
        // 逆序撤销
        for (auto it = commands_.rbegin(); it != commands_.rend(); ++it)
            (*it)->undo();
    }
    std::string name() const override { return name_; }
};

// ==================== 4. 调用者(遥控器) ====================
class RemoteControl {
    std::shared_ptr<ICommand> currentCmd_;
    std::stack<std::shared_ptr<ICommand>> undoStack_;
public:
    void setCommand(std::shared_ptr<ICommand> cmd) {
        currentCmd_ = std::move(cmd);
    }

    void pressButton() {
        if (!currentCmd_) {
            std::cout << "⚠️  未设置命令\n";
            return;
        }
        std::cout << "[按下] " << currentCmd_->name() << "\n";
        currentCmd_->execute();
        undoStack_.push(currentCmd_);
    }

    void pressUndo() {
        if (undoStack_.empty()) {
            std::cout << "⚠️  没有可撤销的操作\n";
            return;
        }
        auto cmd = undoStack_.top();
        undoStack_.pop();
        std::cout << "[撤销] " << cmd->name() << "\n";
        cmd->undo();
    }
};

// ==================== 5. 客户端组装 ====================
int main() {
    Light livingRoomLight;
    Stereo stereo;

    // 创建命令
    auto lightOn  = std::make_shared<LightOnCommand>(livingRoomLight);
    auto lightOff = std::make_shared<LightOffCommand>(livingRoomLight);
    auto stereoVol = std::make_shared<StereoVolumeCommand>(stereo, 80);

    // 创建宏命令:"派对模式"
    auto partyMode = std::make_shared<MacroCommand>(
        "PartyMode",
        std::vector<std::shared_ptr<ICommand>>{lightOn, stereoVol}
    );

    RemoteControl remote;

    // 测试单个命令
    remote.setCommand(lightOn);
    remote.pressButton();

    remote.setCommand(stereoVol);
    remote.pressButton();

    // 测试撤销
    remote.pressUndo();  // 撤销音量调节
    remote.pressUndo();  // 撤销开灯

    std::cout << "\n--- 测试宏命令 ---\n";
    remote.setCommand(partyMode);
    remote.pressButton();

    std::cout << "\n--- 撤销宏命令 ---\n";
    remote.pressUndo();  // 逆序撤销:先关音响,再关灯

    return 0;
}

运行输出

[按下] LightOn
  💡 灯已打开
[按下] StereoVolume
  🎵 音量设为: 80
[撤销] StereoVolume
  🎵 音量设为: 0
[撤销] LightOn
  🌑 灯已关闭

--- 测试宏命令 ---
[按下] PartyMode
  💡 灯已打开
  🎵 音量设为: 80

--- 撤销宏命令 ---
[撤销] PartyMode
  🎵 音量设为: 0
  🌑 灯已关闭

四、深入底层:命令模式的六大核心思维

掌握了代码之后,我们需要进一步理解命令模式背后的设计哲学。这才是区分“会用模式”和“精通模式”的关键。

1. “动词”的名词化(Reification)

这是命令模式最根本的思维跃迁。

  • 常规思维:行为(Behavior)是依附于对象的。我们习惯于 object.doSomething(),行为是瞬时的、不可见的、执行完就消失的。
  • 命令模式思维行为本身也是一种数据。我们将“做某事”这个动作,从方法调用中剥离出来,封装成一个独立的实体(对象)。

💡 核心洞察:一旦行为变成了对象,它就获得了数据的“特权”——可以被存储在变量中、作为参数传递、放入容器排队、序列化到磁盘、甚至在运行时动态组合。

类比理解:没有命令模式时,你直接对厨师喊“炒个宫保鸡丁!”(口头指令,说完即忘);有了命令模式,你在点餐单上写下“宫保鸡丁”并交给服务员。这张点餐单就是命令对象,它可以被贴在厨房窗口排队、可以被修改、可以被取消、可以被复印一份作为日志留存。

2. 时间维度的解耦(Temporal Decoupling)

大多数函数调用是同步且即时的:调用者发出请求的瞬间,接收者必须立即响应。命令模式引入了时间轴上的弹性

维度直接调用命令模式
触发时机创建即执行创建与执行分离,可延迟、定时、异步
生命周期调用栈帧,转瞬即逝堆上对象,可长期存活
执行顺序由调用者代码流决定可由队列/调度器重新编排
错误处理调用者直接捕获异常命令可自行重试、降级或转入死信队列

这种思维在分布式系统、消息队列、任务调度中至关重要。你不是在“调用一个方法”,而是在“投递一个意图”

3. 意图与实现的分离(Intent vs. Implementation)

命令模式强制进行了一次认知分层:

  • Invoker(调用者) 只关心 “什么时候做”(When)和 “做什么类型的操作”(What type)。
  • Receiver(接收者) 只关心 “怎么做”(How)。
  • ConcreteCommand 是唯一知道“将哪个 Receiver 的哪个方法映射到这个意图”的地方。

🧠 思维模型:这就像军队中的指挥链。将军(Invoker)下达“进攻”命令,他不需要知道士兵如何瞄准。士兵(Receiver)只需执行战术动作,不需要理解战略全局。命令文件(ConcreteCommand)是连接战略意图与战术执行的唯一翻译官。

4. 可逆性思维(Reversibility as First-Class Concern)

在传统编程中,“撤销”往往是事后补丁。命令模式将可逆性提升为架构级的一等公民

  • 每个命令在 execute()主动保存恢复所需的上下文。
  • undo() 不是独立的函数,而是命令对象自身的固有契约。
  • 撤销历史天然就是一个命令栈,无需额外的数据结构设计。

这意味着:在设计正向操作的同时,就必须同时设计逆向操作。这不是可选的附加功能,而是命令完整性的一部分。

5. 组合优于继承的行为编排

当行为被对象化后,我们可以用组合模式来编排复杂行为,而无需创建新的子类。宏命令(Macro Command) 本质上是“命令的命令”,它不包含任何业务逻辑,只包含对其他命令的引用和执行顺序。

🔑 高阶思维:行为的复杂度不再通过类的继承层次来表达,而是通过对象图的拓扑结构来表达。这是从“类型系统驱动”到“实例关系驱动”的思维升级。

6. C++ 语境下的现代思维演进

在经典 GoF 命令模式诞生时,C++ 还没有 lambda 和 std::function。今天我们在 C++ 中应用命令模式时,思维也需要进化:

经典思维现代 C++ 思维
每个命令必须是一个类简单命令可用 std::function<void()> + lambda 替代
手动管理 Receiver 指针生命周期使用 shared_ptr / weak_ptr 表达所有权语义
命令接口只有 execute()可扩展为 execute() + undo() + serialize() + priority()
运行时多态(虚函数表)对于性能敏感场景,可用模板+概念实现静态多态命令

关键判断标准:如果命令只是“一次性执行、无需撤销、无需存储”,lambda + std::function 足矣;如果需要撤销、序列化、跨线程传递、动态组合中的任何一项,回归完整的命令类体系。


五、关键设计要点与注意事项

✅ 优点

  • 完全解耦:调用者与接收者零依赖,新增命令无需修改现有代码(符合 OCP)。
  • 天然支持 Undo/Redo:只需实现 undo() 并维护命令栈。
  • 可扩展性强:宏命令、队列化、日志化都是在此基础上叠加。
  • 易于测试:可以用 Mock Command 替代真实命令进行单元测试。

⚠️ 注意事项

  • 简单场景不要过度设计:如果只有一个固定操作且不需要撤销,直接调用即可。
  • 内存管理:C++ 中建议使用 std::shared_ptr<ICommand> 管理命令生命周期,避免悬垂引用。
  • Receiver 引用安全:确保 Receiver 的生命周期 ≥ Command 的生命周期,或使用 weak_ptr + 安全检查。
  • undo 的状态保存:对于有状态的操作(如音量调节),必须在 execute() 时保存旧值;无状态操作(如开关灯)可直接执行反向操作。
  • 线程安全:若命令在多线程环境执行/排队,需注意命令对象的线程安全性。

六、总结

命令模式的本质是将“动词”名词化——把原本散落在各处的函数调用,变成可以传递、存储、组合的一等对象。

命令模式思维

动词名词化

行为即数据

可存储/传递/组合

时间解耦

创建≠执行

意图投递而非同步调用

意图与实现分离

When/What vs How

变更隔离

可逆性一等公民

execute与undo共生

状态自包含

组合编排

宏命令

对象图代替继承树

何时使用? 当你发现自己需要撤销、排队、日志、解耦调用关系中的任意一项时,命令模式就是你的首选。

掌握命令模式,本质上是在训练一种元认知能力:不再把程序看作一系列过程调用的序列,而是看作一组可操纵的行为对象的交互网络。当你开始自然地思考“这个操作能不能被排队?能不能被撤销?能不能被参数化?”时,你就真正内化了命令模式的精髓。这种思维方式不仅适用于设计模式,更是构建事件驱动架构、CQRS、工作流引擎、插件系统等现代软件体系的认知基石。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值