C++设计模式之行为型模式:命令模式(Command)

命令模式(Command)是行为型设计模式的一种,它通过将请求封装为对象,使请求的发送者与接收者解耦,同时支持请求的参数化、队列化、日志记录和撤销操作。这种模式将"做什么"与"谁去做"分离,提高了系统的灵活性和可扩展性。

一、核心思想与角色

命令模式的核心是"封装请求为对象",通过命令对象介导发送者与接收者的交互。其核心角色如下:

角色名称 核心职责
抽象命令(Command) 定义命令的接口,声明执行命令的方法(如execute())和撤销方法(如undo())。
具体命令(ConcreteCommand) 实现抽象命令接口,持有接收者对象,在execute()中调用接收者的具体方法,完成请求。
接收者(Receiver) 执行命令的实际对象,包含具体的业务逻辑(如开灯、关灯的具体实现)。
调用者(Invoker) 持有命令对象,负责调用命令的execute()方法,不关心命令如何执行。
客户端(Client) 创建具体命令对象,指定其接收者,并将命令对象交给调用者执行。

核心思想:将请求封装为命令对象,使发送者(调用者)无需知道接收者的具体信息,只需通过命令对象即可触发请求;同时,命令对象可被存储、传递和复用,支持复杂的操作管理(如撤销、重试)。

二、实现示例(智能家电控制系统)

假设我们需要设计一个智能遥控器(调用者),可控制多种家电(接收者,如灯光、电视)的开关操作,并支持撤销功能。使用命令模式可灵活扩展设备类型和操作:

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

// 2. 接收者1:灯光
class Light {
private:
    std::string location; // 位置(如客厅、卧室)

public:
    Light(const std::string& loc) : location(loc) {}

    // 具体业务逻辑:开灯
    void on() {
        std::cout << location << "灯光已打开" << std::endl;
    }

    // 具体业务逻辑:关灯
    void off() {
        std::cout << location << "灯光已关闭" << std::endl;
    }
};

// 2. 接收者2:电视
class TV {
private:
    std::string location;
    int channel; // 频道

public:
    TV(const std::string& loc) : location(loc), channel(0) {}

    void on() {
        std::cout << location << "电视已打开" << std::endl;
    }

    void off() {
        std::cout << location << "电视已关闭" << std::endl;
    }

    void setChannel(int ch) {
        channel = ch;
        std::cout << location << "电视已切换到频道" << channel << std::endl;
    }

    int getChannel() const { return channel; }
};

// 1. 抽象命令
class Command {
public:
    virtual void execute() = 0;   // 执行命令
    virtual void undo() = 0;      // 撤销命令
    virtual ~Command() = default;
};

// 3. 具体命令1:开灯命令
class LightOnCommand : public Command {
private:
    Light* light; // 持有接收者(灯光)

public:
    LightOnCommand(Light* l) : light(l) {}

    // 执行:调用接收者的on()
    void execute() override {
        light->on();
    }

    // 撤销:调用接收者的off()(与execute相反)
    void undo() override {
        light->off();
    }
};

// 3. 具体命令2:关灯命令
class LightOffCommand : public Command {
private:
    Light* light;

public:
    LightOffCommand(Light* l) : light(l) {}

    void execute() override {
        light->off();
    }

    void undo() override {
        light->on();
    }
};

// 3. 具体命令3:开电视并切换频道命令
class TVOnWithChannelCommand : public Command {
private:
    TV* tv;
    int prevChannel; // 记录之前的频道(用于撤销)

public:
    TVOnWithChannelCommand(TV* t, int channel) : tv(t), prevChannel(0) {
        this->prevChannel = tv->getChannel(); // 保存当前频道
    }

    void execute() override {
        prevChannel = tv->getChannel(); // 执行前再次保存当前频道
        tv->on();
        tv->setChannel(5); // 切换到5频道
    }

    void undo() override {
        tv->setChannel(prevChannel); // 恢复到之前的频道
        tv->off(); // 关闭电视
    }
};

// 3. 空命令(用于初始化遥控器按钮,避免空指针)
class NoCommand : public Command {
public:
    void execute() override {} // 什么也不做
    void undo() override {}    // 什么也不做
};

// 4. 调用者:智能遥控器
class RemoteControl {
private:
    std::vector<Command*> onCommands;  // 存储"开"命令
    std::vector<Command*> offCommands; // 存储"关"命令
    Command* lastCommand; // 记录最后执行的命令(用于撤销)

public:
    // 构造函数:初始化按钮为无命令
    RemoteControl(int buttonCount) {
        Command* noCommand = new NoCommand();
        for (int i = 0; i < buttonCount; ++i) {
            onCommands.push_back(noCommand);
            offCommands.push_back(noCommand);
        }
        lastCommand = noCommand;
    }

    // 设置按钮对应的命令
    void setCommand(int slot, Command* onCmd, Command* offCmd) {
        if (slot >= 0 && slot < onCommands.size()) {
            onCommands[slot] = onCmd;
            offCommands[slot] = offCmd;
        }
    }

    // 按下"开"按钮
    void pressOnButton(int slot) {
        if (slot >= 0 && slot < onCommands.size()) {
            onCommands[slot]->execute();
            lastCommand = onCommands[slot]; // 记录最后执行的命令
        }
    }

    // 按下"关"按钮
    void pressOffButton(int slot) {
        if (slot >= 0 && slot < offCommands.size()) {
            offCommands[slot]->execute();
            lastCommand = offCommands[slot]; // 记录最后执行的命令
        }
    }

    // 按下撤销按钮
    void pressUndoButton() {
        std::cout << "撤销操作:";
        lastCommand->undo();
    }

    // 析构函数:释放命令对象
    ~RemoteControl() {
        // 释放所有命令(注意:NoCommand可能被多个按钮共享,这里简化处理)
        for (auto cmd : onCommands) {
            delete cmd;
        }
        // offCommands中的命令可能与onCommands重复,避免重复释放
        lastCommand = nullptr;
    }
};

// 客户端代码:配置遥控器并使用
int main() {
    // 创建接收者
    Light* livingRoomLight = new Light("客厅");
    TV* livingRoomTV = new TV("客厅");

    // 创建具体命令(绑定接收者)
    Command* lightOn = new LightOnCommand(livingRoomLight);
    Command* lightOff = new LightOffCommand(livingRoomLight);
    Command* tvOn = new TVOnWithChannelCommand(livingRoomTV, 5);

    // 创建调用者(遥控器,2个按钮)
    RemoteControl* remote = new RemoteControl(2);

    // 配置按钮:按钮0控制灯光,按钮1控制电视
    remote->setCommand(0, lightOn, lightOff);
    remote->setCommand(1, tvOn, new LightOffCommand(livingRoomLight)); // 简化:电视关闭复用灯光关闭命令

    // 操作遥控器
    std::cout << "=== 按下客厅灯光开按钮 ===" << std::endl;
    remote->pressOnButton(0);

    std::cout << "\n=== 按下客厅电视开按钮 ===" << std::endl;
    remote->pressOnButton(1);

    std::cout << "\n=== 按下撤销按钮 ===" << std::endl;
    remote->pressUndoButton();

    std::cout << "\n=== 按下客厅灯光关按钮 ===" << std::endl;
    remote->pressOffButton(0);

    std::cout << "\n=== 按下撤销按钮 ===" << std::endl;
    remote->pressUndoButton();

    // 释放资源
    delete remote;
    delete livingRoomTV;
    delete livingRoomLight;

    return 0;
}

三、代码解析

  1. 接收者(Receiver)

    • LightTV是具体的家电,包含实际的业务逻辑(on()off()等),它们不知道命令的存在,只负责执行具体操作。
  2. 抽象命令(Command)

    定义了execute()(执行)和undo()(撤销)接口,所有具体命令都需实现这两个方法。

  3. 具体命令(ConcreteCommand)

    • 每个命令绑定一个接收者(如LightOnCommand绑定Light),在execute()中调用接收者的对应方法(如light->on())。
    • undo()方法实现与execute()相反的操作(如开灯的撤销是关灯),对于复杂命令(如TVOnWithChannelCommand),需要记录执行前的状态(如之前的频道)用于恢复。
  4. 调用者(Invoker)
    RemoteControl(遥控器)持有多个命令对象,提供按钮操作接口(pressOnButton()pressUndoButton()),通过调用命令的execute()undo()完成操作,无需知道具体的接收者和执行细节。

  5. 空命令(NoCommand)

    作为默认命令初始化遥控器按钮,避免空指针异常,体现了"null对象模式"的思想。

  6. 客户端使用

    客户端负责创建接收者、命令和调用者,将命令绑定到调用者的按钮上,最终通过调用者触发命令执行。

四、核心优势与适用场景

优势
  1. 解耦发送者与接收者:调用者无需知道接收者的具体类型和操作细节,只需通过命令对象交互。
  2. 支持命令队列和日志:命令对象可被存储在队列中批量执行,或记录到日志中实现故障恢复(如数据库事务日志)。
  3. 支持撤销/重做 :通过undo()redo()方法,可实现操作的撤销和重复执行(需记录命令历史)。
  4. 易于扩展 :新增命令只需实现Command接口,无需修改现有调用者和接收者(符合开闭原则)。
  5. 参数化操作 :可通过命令对象传递参数(如TVOnWithChannelCommand中的频道),使操作更灵活。
适用场景
  1. 需要抽象出操作并参数化:如GUI中的菜单操作、遥控器按钮、数据库事务。
  2. 需要支持撤销/重做:如文本编辑器的撤销功能、绘图软件的操作回退。
  3. 需要将操作队列化或日志化:如任务调度系统、命令批处理、操作日志记录。
  4. 需要解耦请求发送者和接收者:如分布式系统中的命令分发、中间件的事件处理。

五、与其他模式的区别

模式 核心差异点
命令模式 将请求封装为对象,支持队列、日志、撤销,强调"请求的封装与管理"。
职责链模式 请求沿链传递,由第一个能处理的对象处理,强调"请求的分发与传递"。
策略模式 封装算法家族,使算法可动态替换,强调"算法的选择与切换"。
观察者模式 一个对象改变时通知多个观察者,强调"一对多的依赖关系与事件通知"。

六、实践建议

  1. 设计完善的撤销机制 :对于需要撤销的命令,确保undo()方法能准确恢复到执行前的状态,必要时在execute()中记录历史状态。
  2. 使用空命令避免空指针 :如示例中的NoCommand,简化调用者的空判断逻辑。
  3. 合理管理命令生命周期:命令对象可能被长期存储(如日志),需注意内存管理,避免资源泄漏。
  4. 结合备忘录模式 :对于复杂状态的撤销,可在命令中使用备忘录模式存储对象状态,简化undo()实现。

命令模式的核心价值在于"将请求标准化、对象化",通过命令对象实现了请求发送与执行的解耦,同时为复杂操作管理(如队列、日志、撤销)提供了灵活的解决方案。在需要对操作进行抽象、扩展和精细化管理的场景中,命令模式是一种非常有效的设计选择。

相关推荐
暴力求解3 小时前
数据结构---栈和队列详解(上)
开发语言·数据结构·c++
xindoo3 小时前
AI Agent 设计模式:从理论到实践的完整指南
人工智能·设计模式
小苏兮4 小时前
【C++】list的使用与模拟实现
开发语言·c++·list
小胖xiaopangss5 小时前
栈的压入弹出序列--牛客
数据结构·c++·算法
Shimmer_ocean5 小时前
P1420 最长连号
c++·模拟
程序员莫小特5 小时前
老题新解|求三角形面积
开发语言·数据结构·c++·算法·信息学奥赛一本通
小欣加油5 小时前
leetcode 526 优美的排列
c++·算法·leetcode·职场和发展·深度优先·剪枝
KL41805 小时前
[QT]常用控件一
开发语言·c++·qt
mark-puls6 小时前
Qt标签页控件QTabWidget全面指南:创建现代化多页界面
开发语言·qt·设计模式