0

0

命令模式在C++中怎样应用 实现撤销重做功能的典型结构

P粉602998670

P粉602998670

发布时间:2025-07-07 08:34:02

|

605人浏览过

|

来源于php中文网

原创

命令模式是实现撤销重做的理想选择,1.因为它将操作封装为独立对象,实现调用者与接收者的解耦;2.每个命令自带undo方法,天然支持可撤销性;3.通过维护undo和redo栈实现集中式历史管理;4.符合开闭原则,便于扩展新命令。设计命令类时需注意:1.准确捕获执行前状态以确保正确撤销;2.合理定义命令粒度,平衡精细与效率;3.处理异常并决定失败命令是否入栈;4.使用智能指针管理内存。构建高效历史管理器的关键点包括:1.选用合适数据结构如stack或deque;2.限制历史长度避免内存溢出;3.新命令执行时清空redo栈;4.添加脏状态标志提示保存;5.考虑命令序列化用于持久化存储;6.优化性能瓶颈操作。

命令模式在C++中怎样应用 实现撤销重做功能的典型结构

命令模式在C++中是实现撤销(Undo)和重做(Redo)功能的一种非常经典且高效的设计模式。它的核心思想是将请求封装成一个对象,从而允许你将请求参数化、队列化、记录日志,并且支持可撤销的操作。对于撤销重做,这意味着每个用户操作都被抽象为一个命令对象,这个对象知道如何执行自己,也知道如何撤销自己。

命令模式在C++中怎样应用 实现撤销重做功能的典型结构

解决方案

命令模式在C++中怎样应用 实现撤销重做功能的典型结构

实现撤销重做功能的典型结构通常涉及以下几个关键组件:

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

  1. Command 抽象基类/接口: 定义了所有具体命令都必须实现的接口,通常包括 execute()undo() 方法。
  2. ConcreteCommand 具体命令类: 继承自 Command,实现 execute() 来执行特定操作,并实现 undo() 来撤销该操作。每个具体命令类通常会持有其操作所需的接收者(Receiver)和参数。
  3. Receiver 接收者: 实际执行操作的对象。命令对象将请求委托给接收者来完成具体的工作。例如,一个绘图应用中,DocumentShape 对象可能是接收者。
  4. Invoker 调用者: 负责触发命令执行的对象,它持有并执行一个命令对象。例如,UI 按钮、菜单项等。调用者不直接知道接收者是谁,也不关心具体操作的细节,它只知道如何执行一个命令。
  5. CommandHistory 命令历史管理器: 这是实现撤销重做功能的关键。它通常维护两个栈:一个用于存储已执行的命令(undoStack),另一个用于存储已撤销的命令(redoStack)。

一个简化的C++代码结构示例如下:

命令模式在C++中怎样应用 实现撤销重做功能的典型结构
#include 
#include 
#include 
#include  // For std::unique_ptr
#include   // For std::stack

// 1. Receiver
class Document {
public:
    void addText(const std::string& text) {
        content_ += text;
        std::cout << "Document: Added '" << text << "'. Current content: " << content_ << std::endl;
    }

    void removeLastText(size_t len) {
        if (content_.length() >= len) {
            content_.resize(content_.length() - len);
            std::cout << "Document: Removed last " << len << " chars. Current content: " << content_ << std::endl;
        } else {
            std::cout << "Document: Cannot remove, content too short." << std::endl;
            content_.clear(); // Or handle error
        }
    }

    const std::string& getContent() const {
        return content_;
    }

private:
    std::string content_ = "";
};

// 2. Command Abstract Base Class
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo() = 0;
};

// 3. ConcreteCommand
class AddTextCommand : public Command {
public:
    AddTextCommand(Document& doc, const std::string& text)
        : document_(doc), textToAdd_(text), previousContentLength_(0) {}

    void execute() override {
        previousContentLength_ = document_.getContent().length(); // Store state for undo
        document_.addText(textToAdd_);
    }

    void undo() override {
        // Simple undo: remove the text we added. This assumes no other changes happened.
        // In a real scenario, you might store the exact text that was there before.
        document_.removeLastText(textToAdd_.length());
    }

private:
    Document& document_;
    std::string textToAdd_;
    size_t previousContentLength_; // To help with undo
};

// 5. CommandHistory (Manager)
class CommandHistory {
public:
    void executeAndPush(std::unique_ptr cmd) {
        cmd->execute();
        undoStack_.push(std::move(cmd));
        // Any new command clears the redo stack
        while (!redoStack_.empty()) {
            redoStack_.pop();
        }
    }

    bool undo() {
        if (!undoStack_.empty()) {
            std::unique_ptr cmd = std::move(undoStack_.top());
            undoStack_.pop();
            cmd->undo();
            redoStack_.push(std::move(cmd));
            return true;
        }
        std::cout << "Nothing to undo." << std::endl;
        return false;
    }

    bool redo() {
        if (!redoStack_.empty()) {
            std::unique_ptr cmd = std::move(redoStack_.top());
            redoStack_.pop();
            cmd->execute(); // Re-execute
            undoStack_.push(std::move(cmd));
            return true;
        }
        std::cout << "Nothing to redo." << std::endl;
        return false;
    }

private:
    std::stack> undoStack_;
    std::stack> redoStack_;
};

// 4. Invoker (Example Usage)
int main() {
    Document myDoc;
    CommandHistory history;

    std::cout << "--- Initial State ---" << std::endl;
    history.executeAndPush(std::make_unique(myDoc, "Hello, "));
    history.executeAndPush(std::make_unique(myDoc, "World!"));
    history.executeAndPush(std::make_unique(myDoc, " How are you?"));
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;

    std::cout << "\n--- Undo Operations ---" << std::endl;
    history.undo();
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;
    history.undo();
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;

    std::cout << "\n--- Redo Operations ---" << std::endl;
    history.redo();
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;
    history.redo();
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;

    std::cout << "\n--- New Command After Undo ---" << std::endl;
    history.executeAndPush(std::make_unique(myDoc, " New ending."));
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;

    std::cout << "\n--- Try Redo (Should Fail) ---" << std::endl;
    history.redo(); // Redo stack should be empty now
    std::cout << "Current Document: " << myDoc.getContent() << std::endl;

    return 0;
}

为什么命令模式是实现撤销重做的理想选择?

命令模式之所以是实现撤销重做的理想选择,核心在于它对操作的封装和解耦。首先,它将一个操作的所有必要信息(执行者、参数、执行逻辑)都打包到一个独立的命令对象中,这让操作本身变得像一个名词,可以被存储、传递和管理。这意味着,UI层(调用者)不需要知道具体的业务逻辑如何实现,它只需要知道如何“发出一个命令”。这种调用者与接收者的解耦极大地降低了系统复杂度。

其次,命令模式天生就支持可撤销性。只要你在每个命令对象中实现了 undo() 方法,就为撤销功能提供了基础。这比在业务逻辑中直接散布撤销代码要清晰和可维护得多。当用户执行一个新操作时,我们只需要将对应的命令对象压入“已执行命令栈”;当用户点击撤销时,从栈顶取出命令并调用其 undo() 方法,再将其压入“已撤销命令栈”。这种集中式的撤销/重做管理让整个机制变得异常简洁和强大。

再者,命令模式还带来了扩展性上的巨大优势。当需要添加新的操作时,你只需要创建新的 ConcreteCommand 类,而无需修改现有的调用者或历史管理器代码,这完美符合开闭原则。此外,它也方便实现宏命令(Composite Command),即将一系列命令组合成一个更大的命令,作为一个单元进行执行和撤销,这对于复杂的多步操作非常有用。

设计命令类时需要注意哪些细节?

设计命令类远不止简单地实现 execute()undo() 那么简单,有很多实际的细节需要仔细考量:

一个关键点是状态的捕获与管理undo() 方法必须能够将系统恢复execute() 之前的精确状态。这意味着 ConcreteCommand 对象不仅要持有执行操作所需的参数,还需要在 execute() 之前或之后捕获足够的信息以便进行撤销。例如,一个“移动对象”的命令,不仅要知道移动的目标位置,还需要知道对象原来的位置。如果操作会改变接收者的内部状态,那么撤销时可能需要恢复接收者的旧状态。这可能涉及深拷贝、快照(Memento模式的结合)或者只记录操作的反向参数。过度地保存状态可能导致内存占用过大,而保存不足则可能导致无法正确撤销。

讯飞听见会议
讯飞听见会议

科大讯飞推出的AI智能会议系统

下载

命令的粒度也是一个需要权衡的因素。一个命令应该代表一个原子性的、不可再分的最小操作单元,还是可以是一个包含多个子操作的复合命令?例如,在文本编辑器中,每次按键算一个命令,还是一个句子、一个段落的输入算一个命令?这直接影响到撤销的精细程度和用户体验。过于细碎的命令可能导致历史记录过长、撤销操作繁琐;过于粗糙则可能让用户觉得撤销不够灵活。通常,会根据用户对“一步撤销”的期望来定义命令粒度,并利用复合命令来处理需要批量处理的场景。

异常处理和错误回滚是另一个容易被忽视的方面。如果 execute() 方法在执行过程中抛出异常或失败,应该如何处理?是直接让其失败,还是尝试回滚部分操作?如果命令执行失败,它就不应该被推入撤销栈。这要求 execute() 方法内部逻辑健壮,或者外部有机制捕获失败并决定是否将其视为一个可撤销的操作。

最后,内存管理是C++特有的挑战。谁拥有 Command 对象?它们是临时创建的,还是由历史管理器持有?使用 std::unique_ptrstd::shared_ptr 来管理命令对象的生命周期是常见的做法,以避免内存泄漏。当命令从栈中弹出时,如果不再需要,应该确保其被正确销毁。

如何构建一个高效的撤销重做历史管理器?

构建一个高效的撤销重做历史管理器(CommandHistory)不仅仅是维护两个栈那么简单,它涉及到对内存、性能和用户体验的综合考量。

首先是数据结构的选择。通常,std::stack<:unique_ptr>> 是一个很好的起点。std::unique_ptr 确保了命令对象的所有权清晰,当智能指针超出作用域或被替换时,底层命令对象会被自动销毁,有效避免了内存泄漏。std::stack 提供了LIFO(后进先出)的语义,非常适合撤销和重做操作。undoStack 存储已执行的命令,redoStack 存储已撤销的命令。

历史记录的长度限制是实际应用中必须面对的问题。无限增长的命令历史会消耗大量内存,尤其当命令对象需要存储大量状态信息时。因此,历史管理器通常会设置一个最大容量。当 undoStack 达到上限时,最老的命令(栈底的命令)就需要被删除。这可能需要将 std::stack 替换为 std::dequestd::list,以便在两端进行高效的插入和删除操作,或者在 std::stack 外部通过一个 std::vector 来模拟栈的行为,并手动管理其大小。

处理新命令的执行是历史管理器逻辑中的一个关键点。每当用户执行一个新的操作(即一个新的命令被 executeAndPushundoStack)时,redoStack 必须被清空。这是因为任何新的操作都会改变当前状态,使得之前被撤销的操作(在 redoStack 中)不再有效或无法以有意义的方式重做。

为了提升用户体验,可以考虑添加一个“脏”状态标志。历史管理器可以维护一个布尔变量,指示当前文档或应用状态是否与最近一次保存的状态一致。每当有命令被执行或撤销/重做,并且该操作改变了文档内容时,就将此标志设为“脏”(dirty),提示用户保存。当用户保存后,再将此标志设为“干净”。

在更复杂的场景下,你可能需要考虑命令的序列化和反序列化,以便将操作历史保存到文件并在下次启动时恢复。这对于崩溃恢复或跨会话的撤销重做非常有用。这要求你的命令对象能够被有效地序列化和反序列化,可能需要额外的工作来管理其内部状态。

此外,性能优化也可能成为考量。如果命令对象非常大,或者 execute/undo 操作非常耗时,那么频繁地执行和撤销可能会导致UI卡顿。这时,可以考虑异步执行命令、批量处理命令(宏命令),或者对某些命令进行优化,使其 undo 操作尽可能轻量。例如,一个删除大量元素的命令,其 undo 可能需要重新创建所有被删除的元素,这会是性能瓶颈。

相关专题

更多
treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

529

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

5

2025.12.22

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

989

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

50

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

194

2025.12.29

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

366

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

559

2023.08.10

PHP 高并发与性能优化
PHP 高并发与性能优化

本专题聚焦 PHP 在高并发场景下的性能优化与系统调优,内容涵盖 Nginx 与 PHP-FPM 优化、Opcode 缓存、Redis/Memcached 应用、异步任务队列、数据库优化、代码性能分析与瓶颈排查。通过实战案例(如高并发接口优化、缓存系统设计、秒杀活动实现),帮助学习者掌握 构建高性能PHP后端系统的核心能力。

95

2025.10.16

桌面文件位置介绍
桌面文件位置介绍

本专题整合了桌面文件相关教程,阅读专题下面的文章了解更多内容。

0

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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