当前位置:

首页 > 编程开发 > C++命令模式实现:封装请求与撤销操作

C++命令模式实现:封装请求与撤销操作

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

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

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

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

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

解决方案

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

#include 
#include 
#include 
#include  // For std::shared_ptr
#include   // 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> history; // 已执行的命令历史
    std::stack> redoHistory; // 撤销后的命令历史
public:
    void setCommand(std::shared_ptr command) {
        // 在这里,我们通常会直接执行,或者添加到队列中
        // 为了演示,我们直接执行并添加到历史
        command->execute();
        history.push(command);
        // 执行新命令后,清空redo历史,因为新的操作会覆盖之前的redo点
        while (!redoHistory.empty()) {
            redoHistory.pop();
        }
    }

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

    void redoLastUndo() {
        if (!redoHistory.empty()) {
            std::shared_ptr 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 livingRoomLightOn = std::make_shared(livingRoomLight);
//     std::shared_ptr livingRoomLightOff = std::make_shared(livingRoomLight);
//     std::shared_ptr kitchenLightOn = std::make_shared(kitchenLight);
//     std::shared_ptr kitchenLightOff = std::make_shared(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++的命令模式 请求封装与撤销操作支持

其次,它带来了极高的可扩展性。想象一下,如果你的系统需要增加一个新的功能,比如“打开窗帘”或者“调节空调温度”,你只需要创建新的Command类和对应的Receiver,而现有的Invoker(比如遥控器)几乎不需要任何改动。这符合“开闭原则”——对扩展开放,对修改关闭。这种设计让我在处理大型项目时,感觉代码像积木一样,可以随意组合和扩展,而不是牵一发而动全身。

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

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

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

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

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

最常见的做法,就像我在代码示例里展示的,是在每个具体命令类内部,存储它所操作的接收者在执行execute()方法之前的状态。比如,LightOnCommand在执行on()之前,会先记录灯是亮着还是灭着的。这样,当调用undo()时,它就知道该把灯恢复到哪个状态。这种方式简单直观,对于状态相对简单的对象很有效。

然而,当接收者的状态非常复杂时,直接在命令里存储所有状态可能会导致命令对象变得非常臃肿,甚至出现循环依赖。这时候,我们通常会引入备忘录模式(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++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

c语言函数递归 怎么选?常见方案对比分析
c语言函数递归 怎么选?常见方案对比分析

递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的

Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解

理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的

如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏
如何正确使用 dealloc 以避免 iOS 应用中的内存泄漏

理解 dealloc 的角色与时机在 iOS 应用开发中,内存管理是保障应用性能与稳定性的基石。dealloc 方法是 Objective-C 中对象生命周期结束时的关键回调,它标志着对象即将被系统回收内存。正确理解其触发时机至关重要:当一个对象的引用计数降为零时,运行时系统会自动调用该对象的 de

深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制
深入理解 Objective-C 中的 dealloc 方法:内存管理核心机制

内存管理的基石在Objective-C的世界里,内存管理是开发者必须掌握的核心技能之一。作为一门在手动引用计数(MRC)时代诞生的语言,Objective-C要求程序员对对象的生命周期有清晰的认识。dealloc方法正是这一生命周期中至关重要的终点站。它是一个实例方法,当对象的引用计数降为零时,系统

理解 native2ascii:Java 国际化开发中的字符编码工具
理解 native2ascii:Java 国际化开发中的字符编码工具

native2ascii 工具的基本定位在Ja va应用程序的国际化与本地化开发过程中,处理非拉丁字符集是一个常见且关键的环节。Ja va内部使用Unicode字符集来统一表示全球各种语言的文字,但其属性文件(.properties)在历史上要求使用ASCII编码,或者更准确地说,要求非ASCII字

如何使用 native2ascii 转换中文字符为 Unicode 转义序列
如何使用 native2ascii 转换中文字符为 Unicode 转义序列

理解 native2ascii 工具的基本用途在软件开发,特别是涉及国际化处理的场景中,开发者常常需要处理不同编码的文本资源。native2ascii 是 Ja va 开发工具包(JDK)中提供的一个命令行实用程序,其主要功能是将包含本地字符编码(非ASCII字符)的文件,转换为包含 Unicode

Java native2ascii 命令详解:解决属性文件乱码问题
Java native2ascii 命令详解:解决属性文件乱码问题

native2ascii 命令的由来与作用在Ja va开发中,处理国际化资源文件是一个常见需求。资源文件通常以.properties格式存储,用于支持多语言界面。然而,Ja va属性文件默认采用ISO-8859-1字符集编码,这导致了一个直接的问题:当文件中包含非拉丁字符(如中文、日文、韩文等)时,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。