首页 > 后端开发 > C++ > 正文

怎样实现C++的命令模式 请求封装与撤销操作支持

P粉602998670
发布: 2025-08-05 08:32:01
原创
322人浏览过

命令模式在复杂系统中的优势体现在解耦、可扩展性、事务处理支持、宏命令实现等方面。首先,它通过将请求封装为对象,使调用者与接收者解耦;其次,新增功能只需扩展新命令类,符合开闭原则;第三,命令对象可被记录、序列化,便于事务回滚与日志追踪;第四,支持宏命令组合,实现多操作一体化执行。_undo/redo的实现依赖于命令对象保存执行前状态或使用备忘录模式,并通过两个栈管理历史记录。命令模式常与备忘录模式协作提升撤销能力,与组合模式构建宏命令,与工厂模式解耦命令创建,与策略模式协同实现算法选择,从而增强系统健壮性与灵活性。

怎样实现C++的命令模式 请求封装与撤销操作支持

命令模式,说白了,就是把一个请求封装成一个对象。这样做的核心价值在于,它把请求的发送者和接收者彻底解耦了。你不再需要知道具体是哪个对象来执行这个操作,也不需要知道执行的细节,你只需要告诉一个“命令”对象去执行就行了。更妙的是,因为请求被封装了,它天然就支持了参数化、队列化,甚至能轻易实现撤销(Undo)和重做(Redo)操作,这在很多需要历史记录或事务处理的系统里简直是神来之笔。

怎样实现C++的命令模式 请求封装与撤销操作支持

解决方案

实现C++的命令模式,通常需要定义一个抽象的命令接口,具体的命令类实现这个接口,然后是命令的接收者(执行实际操作的对象),以及命令的调用者(触发命令的对象)。对于撤销操作,关键在于命令对象需要在执行前保存状态,或者提供一个逆向操作。

#include <iostream>
#include <vector>
#include <string>
#include <memory> // For std::shared_ptr
#include <stack>  // For undo/redo history

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

// 2. 接收者 (Receiver) - 实际执行操作的对象
class Light {
private:
    std::string location;
    bool isOn = false;
public:
    Light(const std::string& loc) : location(loc) {}

    void on() {
        if (!isOn) {
            std::cout << location << "灯亮了。" << std::endl;
            isOn = true;
        }
    }

    void off() {
        if (isOn) {
            std::cout << location << "灯灭了。" << std::endl;
            isOn = false;
        }
    }

    bool getIsOn() const { return isOn; }
};

// 3. 具体命令 (Concrete Commands)
class LightOnCommand : public ICommand {
private:
    Light& light;
    bool previousState; // 记录执行前的状态,用于撤销
public:
    LightOnCommand(Light& l) : light(l), previousState(l.getIsOn()) {}

    void execute() override {
        previousState = light.getIsOn(); // 确保记录的是当前状态
        light.on();
    }

    void undo() override {
        if (previousState) { // 如果之前是亮的,撤销就是让它亮
            light.on();
        } else { // 如果之前是灭的,撤销就是让它灭
            light.off();
        }
        std::cout << "撤销:将" << (previousState ? "亮" : "灭") << "状态恢复。" << std::endl;
    }
};

class LightOffCommand : public ICommand {
private:
    Light& light;
    bool previousState; // 记录执行前的状态,用于撤销
public:
    LightOffCommand(Light& l) : light(l), previousState(l.getIsOn()) {}

    void execute() override {
        previousState = light.getIsOn(); // 确保记录的是当前状态
        light.off();
    }

    void undo() override {
        if (previousState) { // 如果之前是亮的,撤销就是让它亮
            light.on();
        } else { // 如果之前是灭的,撤销就是让它灭
            light.off();
        }
        std::cout << "撤销:将" << (previousState ? "亮" : "灭") << "状态恢复。" << std::endl;
    }
};

// 4. 调用者 (Invoker) - 持有命令并触发执行
class RemoteControl {
private:
    std::stack<std::shared_ptr<ICommand>> history; // 已执行的命令历史
    std::stack<std::shared_ptr<ICommand>> redoHistory; // 撤销后的命令历史
public:
    void setCommand(std::shared_ptr<ICommand> command) {
        // 在这里,我们通常会直接执行,或者添加到队列中
        // 为了演示,我们直接执行并添加到历史
        command->execute();
        history.push(command);
        // 执行新命令后,清空redo历史,因为新的操作会覆盖之前的redo点
        while (!redoHistory.empty()) {
            redoHistory.pop();
        }
    }

    void undoLastCommand() {
        if (!history.empty()) {
            std::shared_ptr<ICommand> lastCommand = history.top();
            history.pop();
            lastCommand->undo();
            redoHistory.push(lastCommand); // 将撤销的命令放入redo历史
        } else {
            std::cout << "没有更多可撤销的操作了。" << std::endl;
        }
    }

    void redoLastUndo() {
        if (!redoHistory.empty()) {
            std::shared_ptr<ICommand> lastRedoCommand = redoHistory.top();
            redoHistory.pop();
            lastRedoCommand->execute(); // 重做就是再次执行
            history.push(lastRedoCommand); // 将重做的命令放回历史
        } else {
            std::cout << "没有更多可重做的操作了。" << std::endl;
        }
    }
};

// 客户端代码 (Client)
// int main() {
//     Light livingRoomLight("客厅");
//     Light kitchenLight("厨房");

//     std::shared_ptr<ICommand> livingRoomLightOn = std::make_shared<LightOnCommand>(livingRoomLight);
//     std::shared_ptr<ICommand> livingRoomLightOff = std::make_shared<LightOffCommand>(livingRoomLight);
//     std::shared_ptr<ICommand> kitchenLightOn = std::make_shared<LightOnCommand>(kitchenLight);
//     std::shared_ptr<ICommand> kitchenLightOff = std::make_shared<LightOffCommand>(kitchenLight);

//     RemoteControl remote;

//     // 执行操作
//     remote.setCommand(livingRoomLightOn);
//     remote.setCommand(kitchenLightOn);
//     remote.setCommand(livingRoomLightOff);

//     std::cout << "\n--- 尝试撤销 ---\n";
//     remote.undoLastCommand(); // 撤销客厅灯灭 -> 客厅灯亮
//     remote.undoLastCommand(); // 撤销厨房灯亮 -> 厨房灯灭
//     remote.undoLastCommand(); // 撤销客厅灯亮 -> 客厅灯灭
//     remote.undoLastCommand(); // 没有更多可撤销的了

//     std::cout << "\n--- 尝试重做 ---\n";
//     remote.redoLastUndo(); // 重做客厅灯亮
//     remote.redoLastUndo(); // 重做厨房灯亮
//     remote.redoLastUndo(); // 重做客厅灯灭
//     remote.redoLastUndo(); // 没有更多可重做的了

//     std::cout << "\n--- 新操作会清除重做历史 ---\n";
//     remote.setCommand(kitchenLightOff); // 厨房灯灭
//     remote.redoLastUndo(); // 此时重做历史已被清空

//     return 0;
// }
登录后复制

命令模式在复杂系统中的优势体现在哪里?

在我看来,命令模式在复杂系统中的优势简直是多方面的,它不仅仅是解耦那么简单。首先,最直接的好处就是解耦:调用者(比如一个UI按钮)不再需要知道它背后的具体操作是谁来完成的,也不需要知道怎么完成。它只需要持有一个

ICommand
登录后复制
对象,然后调用
execute()
登录后复制
就行了。这使得系统变得非常灵活,你可以随时更换底层实现,而UI层根本不需要改动。

立即学习C++免费学习笔记(深入)”;

怎样实现C++的命令模式 请求封装与撤销操作支持

其次,它带来了极高的可扩展性。想象一下,如果你的系统需要增加一个新的功能,比如“打开窗帘”或者“调节空调温度”,你只需要创建新的

Command
登录后复制
类和对应的
Receiver
登录后复制
,而现有的
Invoker
登录后复制
(比如遥控器)几乎不需要任何改动。这符合“开闭原则”——对扩展开放,对修改关闭。这种设计让我在处理大型项目时,感觉代码像积木一样,可以随意组合和扩展,而不是牵一发而动全身。

再者,命令模式天生就是为事务处理和日志记录而生的。因为每个操作都被封装成了一个对象,你可以很方便地把这些命令对象序列化、存储起来,形成操作日志,或者在系统崩溃后进行恢复。这对于需要审计追踪、或者确保数据一致性的场景特别有用。比如,在一个数据库事务中,每一步操作都可以是一个命令,如果其中一步失败,你可以很方便地回滚所有已执行的命令。

怎样实现C++的命令模式 请求封装与撤销操作支持

还有一点,我觉得特别有意思的是,它可以很自然地实现宏命令(Macro Command)。就是把一系列命令组合成一个更大的命令。比如,一个“晚安模式”命令,它可能包含“关灯”、“拉窗帘”、“设置闹钟”等多个子命令。这在用户操作流程复杂,或者需要批量处理的场景下,简直是太方便了。你可以把用户的一系列操作录制下来,然后作为一个整体回放,或者保存成一个脚本。这种能力,如果不用命令模式,你可能得写一堆复杂的条件判断和函数调用,那代码简直没法看。

如何优雅地处理命令模式中的撤销与重做功能?

优雅地处理命令模式中的撤销和重做,这其实是命令模式的一个亮点,但实现起来也有些讲究。核心思想是,每个命令在执行时,都要有能力记录下足够的信息,以便在撤销时能恢复到执行前的状态

最常见的做法,就像我在代码示例里展示的,是在每个具体命令类内部,存储它所操作的接收者在执行

execute()
登录后复制
方法之前的状态。比如,
LightOnCommand
登录后复制
在执行
on()
登录后复制
之前,会先记录灯是亮着还是灭着的。这样,当调用
undo()
登录后复制
时,它就知道该把灯恢复到哪个状态。这种方式简单直观,对于状态相对简单的对象很有效。

AI封面生成器
AI封面生成器

专业的AI封面生成工具,支持小红书、公众号、小说、红包、视频封面等多种类型,一键生成高质量封面图片。

AI封面生成器 108
查看详情 AI封面生成器

然而,当接收者的状态非常复杂时,直接在命令里存储所有状态可能会导致命令对象变得非常臃肿,甚至出现循环依赖。这时候,我们通常会引入备忘录模式(Memento Pattern)来协作。命令对象不再直接存储状态,而是让接收者生成一个“备忘录”对象(memento),这个备忘录包含了接收者在某个时刻的所有内部状态。命令对象只存储这个备忘录,在撤销时,再把备忘录还给接收者,让接收者恢复到之前的状态。这种方式把状态存储的职责从命令中解耦出去,让设计更清晰。

对于撤销和重做的管理,通常会使用两个栈:一个

history
登录后复制
栈(或
undoStack
登录后复制
)用来存储已执行的命令,另一个
redoHistory
登录后复制
栈(或
redoStack
登录后复制
)用来存储被撤销的命令。

  • 当一个命令被执行时,它被推入
    history
    登录后复制
    栈,并且
    redoHistory
    登录后复制
    栈会被清空(因为任何新的操作都会使之前的“重做点”失效)。
  • 当执行撤销时,
    history
    登录后复制
    栈顶的命令被弹出,调用其
    undo()
    登录后复制
    方法,然后这个命令被推入
    redoHistory
    登录后复制
    栈。
  • 当执行重做时,
    redoHistory
    登录后复制
    栈顶的命令被弹出,调用其
    execute()
    登录后复制
    方法(是的,重做就是再次执行),然后这个命令被推回
    history
    登录后复制
    栈。

这里面有个小挑战,就是并非所有命令都能被撤销。有些操作是破坏性的,或者不可逆的,比如“删除文件”。对于这类命令,你可能需要在设计时就明确它们不支持撤销,或者提供一个警告。另外,性能也是一个考量点。如果每次操作都存储大量的状态,那么撤销栈可能会占用大量内存。这时,可能需要考虑增量式状态存储,只记录状态的变化,而不是整个状态的快照。或者,对于非常长的操作序列,可以考虑限制撤销历史的深度

命令模式与其他设计模式如何协作以提升系统健壮性?

命令模式本身已经很强大了,但它并不是孤立存在的。在实际的复杂系统中,它经常会与其他设计模式“手拉手”协作,共同构建出更健壮、更灵活的架构。

首先,我前面提到了备忘录模式(Memento Pattern)。这俩简直是天作之合,尤其是在处理撤销/重做功能时。当命令需要保存接收者的复杂状态以便撤销时,如果命令自己去管理这些状态,会变得非常臃肿且耦合。备忘录模式让接收者负责创建和恢复自己的状态(通过一个备忘录对象),命令对象只需要持有这个轻量级的备忘录,在需要时将其传回接收者。这让命令专注于“做什么”和“如何撤销”,而状态的“如何保存和恢复”则由接收者和备忘录模式来处理,职责分离得非常清晰。

接着是组合模式(Composite Pattern)。这个模式允许你将对象组合成树形结构以表示“部分-整体”的层次结构。在命令模式中,这意味着你可以创建一个

MacroCommand
登录后复制
(宏命令),它本身也是一个
ICommand
登录后复制
,但它内部包含了一个或多个其他的
ICommand
登录后复制
对象。当你执行这个
MacroCommand
登录后复制
时,它会依次执行其内部的所有子命令。这对于实现复杂的用户操作序列、批处理任务或者脚本录制功能非常有用。比如,一个“启动工作环境”的宏命令,可能包含“打开IDE”、“启动数据库服务”、“拉取最新代码”等一系列独立命令。这种组合能力让系统在功能层面具有极高的灵活性。

然后是工厂模式(Factory Method / Abstract Factory)。在很多情况下,你可能需要根据用户的输入、配置或者某些运行时条件来动态地创建命令对象。如果直接在客户端代码中

new
登录后复制
出具体的命令,会导致客户端与具体命令类紧密耦合。引入工厂模式,比如一个
CommandFactory
登录后复制
,它可以根据传入的参数(例如一个字符串表示的命令类型)来返回相应的
ICommand
登录后复制
实例。这样,客户端只需要知道如何向工厂请求命令,而不需要知道具体命令类的名称和构造细节,进一步降低了耦合度,也使得系统更容易扩展新的命令类型。

最后,我想提一下它和策略模式(Strategy Pattern)区别与联系。有时候这俩容易混淆。策略模式关注的是“如何做一件事”,它封装的是算法或行为的不同实现,这些实现可以互换。而命令模式关注的是“做什么”,它封装的是一个请求本身。一个命令对象通常包含了一个动作的接收者和执行这个动作所需的所有参数。虽然它们都使用了多态性来封装行为,但它们的意图和应用场景有所不同。不过,在某些复杂的场景下,一个命令的

execute()
登录后复制
方法内部可能会使用策略模式来选择具体的执行算法,这也不是不可能。这种模式间的协同,正是设计模式的魅力所在,它们不是孤立的银弹,而是可以互相配合、共同解决复杂问题的工具集。

以上就是怎样实现C++的命令模式 请求封装与撤销操作支持的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号