C++命令模式实战:从解耦到撤销重做的完整实现

发布时间:2026/7/25 7:06:10
C++命令模式实战:从解耦到撤销重做的完整实现 1. 项目概述为什么我们需要命令模式在C的世界里尤其是在构建那些需要处理复杂用户交互、支持撤销/重做或者需要将操作请求者与执行者解耦的系统时我们常常会陷入一种困境。想象一下你正在开发一个图形编辑器用户可以通过菜单、工具栏按钮、快捷键等多种方式执行“复制”、“粘贴”、“删除”等操作。最直接的实现方式可能是这样的在每一个按钮的点击事件处理函数里直接调用对应图形对象的成员函数。// 一个“简单粗暴”的实现 void onCopyButtonClicked() { currentSelectedGraphic-copyToClipboard(); } void onDeleteButtonClicked() { currentSelectedGraphic-remove(); document-updateView(); }这种写法在初期看起来清晰明了但很快就会暴露出问题。首先紧耦合UI层按钮直接依赖并知晓具体业务对象Graphic的细节。如果Graphic类的接口变了所有调用它的UI代码都得改。其次难以扩展新操作每增加一个操作就要在UI层增加一个事件处理函数。再者无法实现宏命令或事务比如用户想一次性执行“复制A、删除B、粘贴到C”这一系列操作或者更常见的——撤销Undo和重做Redo功能在这种结构下几乎无法优雅地实现。命令模式Command Pattern正是为了解决这些问题而生的。它的核心思想非常直观将一个请求封装成一个对象从而使你可用不同的请求对客户进行参数化对请求排队或记录请求日志以及支持可撤销的操作。简单说就是把“做什么”这个动作从直接调用函数变成了创建一个代表这个动作的“命令对象”。这个对象知道“谁”来执行接收者以及“如何”执行执行方法。发出请求的调用者Invoker只和这个命令对象打交道完全不知道背后具体的接收者和执行细节。在C中实现命令模式不仅能让我们写出更清晰、更解耦的代码更是深入理解面向对象设计原则如开闭原则、单一职责原则的绝佳实践。它广泛应用于GUI框架、游戏开发技能/指令系统、事务系统、工作队列等场景。接下来我将带你从零开始用C实现一个功能完整的命令模式并深入探讨其高级应用和避坑指南。2. 核心概念与UML类图解析在动手写代码之前我们必须先厘清命令模式中几个关键角色的职责这有助于我们构建清晰的心智模型。命令模式通常涉及以下四个参与者命令Command这是一个抽象基类或接口通常只声明一个execute()方法。它是所有具体命令的契约。具体命令ConcreteCommand实现了Command接口的类。它内部持有一个接收者Receiver对象的引用并将调用接收者的一个或多个动作绑定到其execute()方法中。它就像一个“信封”里面装着“给谁”和“做什么”的信息。接收者Receiver知道如何执行与请求相关的操作。任何类都可以成为接收者。具体命令对象会将请求委托给接收者对象来执行实际的工作。调用者/触发者Invoker持有命令对象并在某个时间点调用命令对象的execute()方法来发出请求。它不需要知道命令的具体内容或接收者是谁。按钮、菜单项、定时器、远程控制等都可以扮演调用者的角色。有时还会有一个客户端Client角色负责创建具体的命令对象并设置其接收者。客户端将命令对象配置给调用者。它们之间的关系可以用以下UML类图来清晰表示---------------- --------------------- | Invoker | | Interface | |----------------| | Command | | - command |----|---------------------| |----------------| | execute(): void | | setCommand() | | undo(): void | | executeCmd() | --------------------- ---------------- ^ | | 实现 | ------------------------------------------------------ | | --------------------- --------------------- | ConcreteCommandA | | ConcreteCommandB | |---------------------| |---------------------| | - receiver: Receiver| | - receiver: Receiver| |---------------------| |---------------------| | execute() | | execute() | | undo() | | undo() | --------------------- --------------------- | | | 持有 | 持有 v v ---------------- ---------------- | ReceiverA | | ReceiverB | |----------------| |----------------| | actionA() | | actionB() | ---------------- ----------------关系解读Invoker聚合持有一个Command对象。这意味着Invoker知道有一个命令但不知道是哪个具体命令。ConcreteCommandA和ConcreteCommandB实现了Command接口。每个ConcreteCommand内部关联持有一个特定的Receiver对象如ReceiverA或ReceiverB。当Invoker调用command-execute()时多态机制会找到具体的命令对象如ConcreteCommandA该命令对象再调用其持有的receiver的特定方法如receiver-actionA()来完成实际工作。这种结构的精妙之处在于Invoker与Receiver完全解耦。Invoker只依赖于抽象的Command接口。我们可以随时给Invoker配置不同的ConcreteCommand从而改变其行为而Invoker的代码无需任何改动。这完美体现了“对扩展开放对修改关闭”的开闭原则。3. 基础实现一个简单的文本编辑器命令让我们从一个经典的例子开始实现一个文本编辑器的“复制”和“粘贴”命令。我们会创建最基础的命令结构并演示如何工作。首先定义我们的接收者Document它代表文档模型知道如何执行实际的操作。// Receiver: 文档类知道如何执行实际的操作 class Document { public: explicit Document(const std::string name) : name_(name), content_(), clipboard_() {} void copy(const std::string selectedText) { clipboard_ selectedText; std::cout 文档 \ name_ \: 已复制文本 \ selectedText \ 到剪贴板。 std::endl; } void paste() { if (!clipboard_.empty()) { content_ clipboard_; std::cout 文档 \ name_ \: 已粘贴剪贴板内容。当前内容: \ content_ \ std::endl; } else { std::cout 文档 \ name_ \: 剪贴板为空无法粘贴。 std::endl; } } const std::string getContent() const { return content_; } private: std::string name_; std::string content_; std::string clipboard_; // 模拟剪贴板 };接下来定义抽象的Command接口。为了后续支持撤销我们一并声明一个undo()方法。// Command: 抽象命令接口 class Command { public: virtual ~Command() default; virtual void execute() 0; virtual void undo() 0; // 为撤销功能预留接口 };现在创建具体的命令类。每个命令类内部持有一个Document接收者的指针或引用并在execute()方法中调用接收者的特定方法。// ConcreteCommand: 复制命令 class CopyCommand : public Command { public: CopyCommand(Document* doc, const std::string text) : document_(doc), selectedText_(text) { if (!doc) throw std::invalid_argument(Document cannot be null); } void execute() override { document_-copy(selectedText_); } void undo() override { // 复制操作通常不可撤销或者撤销意味着清空剪贴板这里我们先简单实现。 std::cout 撤销复制清空剪贴板模拟。 std::endl; // 在实际应用中可能需要更复杂的逻辑来管理剪贴板状态历史。 } private: Document* document_; std::string selectedText_; }; // ConcreteCommand: 粘贴命令 class PasteCommand : public Command { public: PasteCommand(Document* doc) : document_(doc), previousContent_() { if (!doc) throw std::invalid_argument(Document cannot be null); } void execute() override { previousContent_ document_-getContent(); // 记录执行前的状态用于撤销 document_-paste(); } void undo() override { // 撤销粘贴将文档内容恢复到执行前的状态 // 注意这是一个简化实现。真实场景可能需要更精细的状态管理。 std::cout 撤销粘贴恢复文档内容为 \ previousContent_ \ std::endl; // 这里为了简化我们只是输出信息。实际需要修改Document的内部状态。 // 假设Document有一个setContent方法。 // document_-setContent(previousContent_); } private: Document* document_; std::string previousContent_; // 用于撤销的状态 };然后我们创建调用者Button。它持有一个命令并在被点击时执行该命令。// Invoker: 按钮类触发命令执行 class Button { public: void setCommand(std::unique_ptrCommand cmd) { command_ std::move(cmd); } void onClick() { if (command_) { std::cout 按钮被点击 - ; command_-execute(); } else { std::cout 按钮未绑定任何命令。 std::endl; } } private: std::unique_ptrCommand command_; };最后在客户端main函数中我们将所有部分组装起来。int main() { // 1. 创建接收者 Document myDoc(我的报告); // 2. 创建具体命令并绑定接收者 auto copyCmd std::make_uniqueCopyCommand(myDoc, Hello, World!); auto pasteCmd std::make_uniquePasteCommand(myDoc); // 3. 创建调用者 Button copyButton, pasteButton; // 4. 将命令绑定到调用者 copyButton.setCommand(std::move(copyCmd)); pasteButton.setCommand(std::move(pasteCmd)); // 5. 模拟用户交互 copyButton.onClick(); // 输出按钮被点击 - 文档 我的报告: 已复制文本 Hello, World! 到剪贴板。 pasteButton.onClick(); // 输出按钮被点击 - 文档 我的报告: 已粘贴剪贴板内容。当前内容: Hello, World! // 尝试撤销这里命令对象已被Button持有我们暂时无法直接调用undo // 后续我们会改进设计让Invoker也能支持撤销。 return 0; }这个基础示例清晰地展示了命令模式的工作流程客户端创建命令并配置给调用者调用者触发命令命令委托接收者执行实际工作。整个过程中Button完全不知道Document的存在它只和Command接口交互。注意上面的PasteCommand::undo实现是示意性的。在实际项目中直接存储整个文档的先前内容来撤销一次粘贴可能效率低下特别是对于大文档。通常我们会采用更精细的操作记录如差异存储或命令模式与备忘录模式Memento结合使用。这里重点是展示命令模式如何为撤销操作提供框架。4. 进阶实现支持撤销、重做与宏命令基础版本实现了解耦但一个实用的命令系统必须支持撤销Undo和重做Redo。同时我们经常需要将多个命令组合成一个宏命令Macro Command来批量执行。让我们来升级我们的实现。4.1 实现可撤销的命令为了实现撤销每个具体命令不仅要知道execute()做什么还要知道如何undo()。这意味着命令对象需要存储足够的信息来将系统状态回退到命令执行之前。我们修改Command接口和具体命令的实现。首先我们增强Document类使其状态更容易被命令记录和恢复。class Document { public: // ... 其他成员同上 ... void setContent(const std::string content) { content_ content; } // 提供一个更细粒度的“插入文本”操作便于命令记录状态 void insertText(const std::string text, size_t position) { if (position content_.length()) position content_.length(); content_.insert(position, text); std::cout 文档 \ name_ \: 在位置 position 插入文本 \ text \。当前内容: \ content_ \ std::endl; } // 获取当前内容长度用于确定插入位置 size_t getLength() const { return content_.length(); } };现在我们实现一个更具体的InsertTextCommand它记录插入的位置和文本以便撤销。class InsertTextCommand : public Command { public: InsertTextCommand(Document* doc, const std::string text, size_t pos) : document_(doc), text_(text), position_(pos) { if (!doc) throw std::invalid_argument(Document cannot be null); } void execute() override { document_-insertText(text_, position_); } void undo() override { // 撤销插入从文档中删除我们之前插入的文本 // 这里我们假设Document有一个deleteText方法或者我们直接操作content_。 // 为了示例清晰我们简化处理直接重置文档内容到之前的状态 // 更好的方法是在execute()之前保存受影响部分的状态。 // 我们采用另一种常见策略在构造命令时就保存命令执行前的“受影响状态”。 // 但这里为了简化我们让命令自己计算如何撤销。 std::string currentContent document_-getContent(); if (position_ currentContent.length() text_.length() currentContent.length() - position_) { currentContent.erase(position_, text_.length()); document_-setContent(currentContent); std::cout 撤销插入已删除文本 \ text_ \。文档恢复为: \ document_-getContent() \ std::endl; } } private: Document* document_; std::string text_; size_t position_; };4.2 构建命令历史管理器Invoker的升级单一的按钮调用者不够了我们需要一个能够管理命令执行历史用于撤销/重做的调用者。我们创建一个CommandManager类。// Invoker的升级版命令管理器支持撤销和重做 class CommandManager { public: void executeCommand(std::unique_ptrCommand cmd) { if (cmd) { cmd-execute(); // 将已执行的命令存入撤销栈 undoStack_.push(std::move(cmd)); // 执行新命令后重做栈必须清空因为产生了新的历史分支 while (!redoStack_.empty()) { redoStack_.pop(); } } } void undo() { if (undoStack_.empty()) { std::cout 没有可以撤销的命令。 std::endl; return; } auto cmd std::move(undoStack_.top()); undoStack_.pop(); cmd-undo(); // 将撤销的命令放入重做栈 redoStack_.push(std::move(cmd)); } void redo() { if (redoStack_.empty()) { std::cout 没有可以重做的命令。 std::endl; return; } auto cmd std::move(redoStack_.top()); redoStack_.pop(); cmd-execute(); // 重做就是再次执行 // 再次执行后它又进入了撤销栈 undoStack_.push(std::move(cmd)); } bool canUndo() const { return !undoStack_.empty(); } bool canRedo() const { return !redoStack_.empty(); } private: std::stackstd::unique_ptrCommand undoStack_; std::stackstd::unique_ptrCommand redoStack_; };4.3 实现宏命令Composite Command宏命令是组合模式Composite Pattern与命令模式结合的典型应用。它本身也是一个Command但内部包含了一个子命令的列表。执行宏命令就是按顺序执行所有子命令撤销宏命令就是按逆序撤销所有子命令。// 宏命令组合多个命令 class MacroCommand : public Command { public: void addCommand(std::unique_ptrCommand cmd) { if (cmd) { commands_.push_back(std::move(cmd)); } } void execute() override { for (const auto cmd : commands_) { cmd-execute(); } std::cout 宏命令执行完毕共包含 commands_.size() 个子命令。 std::endl; } void undo() override { // 撤销时需要按相反顺序执行 for (auto it commands_.rbegin(); it ! commands_.rend(); it) { (*it)-undo(); } std::cout 宏命令撤销完毕。 std::endl; } private: std::vectorstd::unique_ptrCommand commands_; };4.4 综合演示让我们把增强的撤销/重做功能和宏命令结合起来演示。int main() { Document doc(进阶示例文档); CommandManager cmdManager; std::cout 演示单个命令的撤销/重做 std::endl; // 执行命令1 cmdManager.executeCommand(std::make_uniqueInsertTextCommand(doc, 第一段。, 0)); // 执行命令2 cmdManager.executeCommand(std::make_uniqueInsertTextCommand(doc, 第二段。, doc.getLength())); std::cout 当前文档内容: \ doc.getContent() \ std::endl; // 撤销一次 cmdManager.undo(); std::cout 撤销一次后文档内容: \ doc.getContent() \ std::endl; // 重做一次 cmdManager.redo(); std::cout 重做一次后文档内容: \ doc.getContent() \ std::endl; std::cout \n 演示宏命令 std::endl; // 创建一个宏命令包含两个插入操作 auto macro std::make_uniqueMacroCommand(); macro-addCommand(std::make_uniqueInsertTextCommand(doc, 宏A, 3)); // 在位置3插入 macro-addCommand(std::make_uniqueInsertTextCommand(doc, 宏B, 8)); // 在位置8插入 cmdManager.executeCommand(std::move(macro)); std::cout 执行宏命令后文档内容: \ doc.getContent() \ std::endl; // 撤销整个宏命令 cmdManager.undo(); std::cout 撤销宏命令后文档内容: \ doc.getContent() \ std::endl; return 0; }这个进阶实现展示了命令模式如何优雅地支持复杂功能。CommandManager作为智能调用者管理着命令的历史为撤销/重做提供了基础设施。MacroCommand则展示了如何将命令组合成更高级的操作单元。这些特性使得命令模式在构建交互式应用时极其强大。5. 性能考量、内存管理与智能指针的应用在C中实现命令模式尤其是支持撤销/重做时我们必须认真考虑对象的生命周期和内存管理。命令对象在被执行后可能不会立即销毁而是被存入历史栈中。这带来了几个关键问题对象所有权谁负责创建命令谁负责销毁命令调用者如CommandManager持有命令时它必须拥有命令的所有权。深拷贝与浅拷贝如果命令内部持有指向资源如动态内存、文件句柄的指针在命令被存入历史栈时需要进行深拷贝还是仅存储指针错误的拷贝会导致悬垂指针或内存泄漏。性能开销频繁创建和销毁大量小命令对象可能带来内存碎片和分配开销。对于执行非常频繁的命令如游戏中的每帧移动命令这可能是性能瓶颈。5.1 使用智能指针管理所有权在C11及以后的标准中使用智能指针是管理命令对象生命周期的首选方式它可以有效防止内存泄漏并明确所有权语义。std::unique_ptrCommand表示独占所有权。当命令被移交给CommandManager后客户端就不再拥有该命令。这适用于大多数场景因为一个命令在历史栈中只应有一份。auto cmd std::make_uniqueInsertTextCommand(doc, text, pos); cmdManager.executeCommand(std::move(cmd)); // 所有权转移 // 此后cmd 变为 nullptr无法再使用std::shared_ptrCommand表示共享所有权。如果多个地方需要引用同一个命令对象虽然不常见可以使用。但要注意命令的撤销操作可能会修改接收者状态共享命令对象可能导致意外行为。std::weak_ptrCommand用于观察命令对象而不拥有所有权。例如一个UI组件可能想观察某个命令是否可执行但不参与其生命周期管理。在我们的CommandManager示例中我们使用了std::unique_ptr这清晰地表明了命令一旦被executeCommand其所有权就转移给了管理器。历史栈std::stackstd::unique_ptrCommand负责在其析构时自动销毁所有命令。5.2 命令对象的轻量化与复制策略如果命令对象本身很大例如它包含了要插入的大段文本的副本频繁的拷贝和存储可能会消耗大量内存。有几种优化策略惰性状态存储不要在命令构造时就保存所有用于撤销的状态。可以在execute()方法中在执行实际操作之前捕获并保存必要的状态。对于InsertTextCommand我们只需要保存插入位置和文本而不需要保存整个文档。使用引用或指针指向共享数据如果命令操作的接收者状态是共享的如文档对象命令内部应存储指向接收者的指针或引用而不是副本。这要求确保接收者的生命周期长于所有引用它的命令。实现拷贝构造函数和赋值运算符如果命令需要被拷贝例如放入std::vector确保正确实现“深拷贝”或明确禁用拷贝使用delete。对于包含原始指针的命令深拷贝是必须的如果使用智能指针管理内部资源默认的拷贝行为可能就足够了但需要仔细评估。考虑命令池Command Pooling对于超高性能场景可以预分配一块固定大小的命令对象内存池重复使用避免动态内存分配的开销。这增加了复杂性仅在性能分析表明命令分配是热点时才考虑。5.3 一个关于生命周期的“坑”一个常见的错误是命令对象持有对局部接收者对象的引用或指针而该接收者在命令执行前就被销毁了。例如void riskyFunction() { Document tempDoc(临时文档); auto cmd std::make_uniqueInsertTextCommand(tempDoc, 文本, 0); // 危险保存了局部对象的地址 // ... 将cmd交给某个管理器但管理器可能稍后才执行 ... } // tempDoc 在这里被销毁cmd 内部的 document_ 指针变成悬垂指针 // 之后当管理器执行 cmd-execute() 时程序行为未定义很可能崩溃。解决方案确保接收者对象如Document的生命周期覆盖所有可能执行或撤销其命令的时间段。通常接收者是应用程序核心模型的一部分如主文档、游戏世界其生命周期与整个功能模块或应用本身一致。如果接收者可能被销毁那么所有关联的命令也必须从历史栈中移除或者命令内部使用std::weak_ptr来安全地引用接收者并在执行前检查其是否存在。6. 在现代C中的实现变体与最佳实践现代CC11/14/17/20提供了新的语言特性可以让命令模式的实现更简洁、更安全、更高效。6.1 使用std::function和 Lambda 表达式对于简单的、无状态的或一次性使用的命令我们不一定需要为每个操作都定义一个具体的命令类。std::function和 Lambda 表达式可以让我们以更函数式的方式创建命令对象。我们可以定义一个接受std::function的通用命令包装器class FunctionCommand : public Command { public: using ExecuteFunc std::functionvoid(); using UndoFunc std::functionvoid(); FunctionCommand(ExecuteFunc exec, UndoFunc undo nullptr) : executeFunc_(std::move(exec)), undoFunc_(std::move(undo)) {} void execute() override { if (executeFunc_) executeFunc_(); } void undo() override { if (undoFunc_) undoFunc_(); } private: ExecuteFunc executeFunc_; UndoFunc undoFunc_; };使用Lambda表达式创建命令变得非常直观Document doc(Lambda文档); std::string textToInsert 动态文本; size_t insertPos 0; std::string previousState; // 用于捕获撤销所需的状态 auto lambdaCmd std::make_uniqueFunctionCommand( // execute lambda [doc, textToInsert, insertPos, previousState]() { previousState doc.getContent(); // 捕获执行前状态 doc.insertText(textToInsert, insertPos); }, // undo lambda [doc, previousState doc.getContent()]() mutable { // 注意这里用拷贝捕获初始状态 doc.setContent(previousState); std::cout Lambda命令撤销。 std::endl; } ); cmdManager.executeCommand(std::move(lambdaCmd));优点代码紧凑无需为简单操作定义新类。缺点对于复杂的命令Lambda可能变得冗长且难以维护需要小心处理捕获变量的生命周期尤其是引用捕获。6.2 使用模板实现类型安全的命令如果我们希望命令接口能处理不同类型的接收者或者希望编译期检查更多约束可以使用模板。template typename Receiver class SimpleCommand : public Command { public: using Action void (Receiver::*)(); // 成员函数指针类型 SimpleCommand(Receiver* r, Action a) : receiver_(r), action_(a) {} void execute() override { if (receiver_ action_) { (receiver_-*action_)(); // 调用成员函数 } } void undo() override { /* 简单命令可能不支持撤销 */ } private: Receiver* receiver_; Action action_; }; // 使用示例 Document doc; auto simpleCmd std::make_uniqueSimpleCommandDocument(doc, Document::paste);模板化命令在需要高度通用性的库代码中很有用但它对撤销功能的支持较弱且语法相对复杂。6.3 线程安全考虑如果命令可能在多线程环境中被创建和执行例如从UI线程接收命令在后台线程执行则需要考虑线程安全。命令对象的不可变性理想情况下命令对象在创建后应是不可变的除了执行/撤销标记。所有执行所需的数据应在构造时传入。这避免了多线程同时修改命令状态。接收者访问同步命令的execute()和undo()方法会访问接收者对象。如果接收者被多个线程共享必须在接收者内部或命令调用处进行适当的同步如使用互斥锁std::mutex。命令队列典型的“生产者-消费者”模式。一个线程如UI线程生产命令对象并将其推入线程安全的队列如std::queue配合互斥锁和条件变量。另一个线程如工作线程从队列中取出并执行命令。CommandManager本身也需要是线程安全的。#include queue #include mutex #include condition_variable class ThreadSafeCommandQueue { public: void push(std::unique_ptrCommand cmd) { std::lock_guardstd::mutex lock(mutex_); queue_.push(std::move(cmd)); condVar_.notify_one(); } std::unique_ptrCommand pop() { std::unique_lockstd::mutex lock(mutex_); condVar_.wait(lock, [this] { return !queue_.empty(); }); auto cmd std::move(queue_.front()); queue_.pop(); return cmd; } private: std::queuestd::unique_ptrCommand queue_; std::mutex mutex_; std::condition_variable condVar_; };6.4 最佳实践总结优先使用智能指针用std::unique_ptrCommand明确所有权转移避免手动内存管理错误。保持命令轻量命令应主要包含行为描述做什么和必要的撤销信息避免存储大型数据副本。为复杂操作定义具体命令类对于重要的、可复用的业务操作定义专门的命令类代码更清晰、更易维护。利用Lambda处理简单逻辑对于一次性、简单的回调式操作使用std::function和Lambda可以减少样板代码。仔细设计撤销机制根据操作性质选择正确的状态保存策略全量快照、逆操作、差异记录。考虑性能影响。注意生命周期确保接收者对象的生命周期长于引用它的所有命令。考虑并发如果涉及多线程确保命令的创建、存储和执行是线程安全的。7. 实战案例模拟一个简单的绘图工具命令系统为了将前面所有概念融会贯通我们设计一个简易的绘图工具。这个工具支持画线、画矩形、改变颜色并且所有操作都支持撤销和重做。7.1 定义接收者画布和图形// 图形基类 class Shape { public: virtual ~Shape() default; virtual void draw() const 0; virtual void move(int dx, int dy) 0; virtual std::unique_ptrShape clone() const 0; // 用于实现原型模式方便复制 }; // 简单的线段 class Line : public Shape { public: Line(int x1, int y1, int x2, int y2) : x1_(x1), y1_(y1), x2_(x2), y2_(y2) {} void draw() const override { std::cout 绘制线段: ( x1_ , y1_ ) - ( x2_ , y2_ ) std::endl; } void move(int dx, int dy) override { x1_ dx; y1_ dy; x2_ dx; y2_ dy; } std::unique_ptrShape clone() const override { return std::make_uniqueLine(*this); } private: int x1_, y1_, x2_, y2_; }; // 简单的矩形 class Rectangle : public Shape { public: Rectangle(int x, int y, int w, int h) : x_(x), y_(y), width_(w), height_(h) {} void draw() const override { std::cout 绘制矩形: 左上角( x_ , y_ ), 宽 width_ , 高 height_ std::endl; } void move(int dx, int dy) override { x_ dx; y_ dy; } std::unique_ptrShape clone() const override { return std::make_uniqueRectangle(*this); } private: int x_, y_, width_, height_; }; // 画布管理所有图形 class Canvas { public: void addShape(std::unique_ptrShape shape) { shapes_.push_back(std::move(shape)); std::cout 图形已添加到画布。当前图形数: shapes_.size() std::endl; } bool removeLastShape() { if (shapes_.empty()) return false; shapes_.pop_back(); std::cout 已移除最后一个图形。当前图形数: shapes_.size() std::endl; return true; } void drawAll() const { std::cout 绘制画布 std::endl; for (const auto shape : shapes_) { shape-draw(); } std::cout std::endl; } // 获取最后添加的图形用于移动等操作 Shape* getLastShape() { return shapes_.empty() ? nullptr : shapes_.back().get(); } private: std::vectorstd::unique_ptrShape shapes_; };7.2 定义具体命令// 添加图形命令 class AddShapeCommand : public Command { public: AddShapeCommand(Canvas* canvas, std::unique_ptrShape shape) : canvas_(canvas), shape_(std::move(shape)) {} void execute() override { if (canvas_ shape_) { // 执行前我们不需要保存状态因为撤销就是删除这个图形。 canvas_-addShape(shape_-clone()); // 添加一个副本 // 注意为了能撤销我们需要记住添加的是哪个图形实际上我们依赖画布的顺序。 // 更稳健的做法是命令记录添加的图形的索引或ID。这里简化处理假设撤销总是删除最后一个图形。 } } void undo() override { if (canvas_) { canvas_-removeLastShape(); } } private: Canvas* canvas_; std::unique_ptrShape shape_; // 存储图形的原型用于执行时克隆 }; // 移动图形命令 class MoveShapeCommand : public Command { public: MoveShapeCommand(Shape* shape, int dx, int dy) : shape_(shape), dx_(dx), dy_(dy) { if (!shape) throw std::invalid_argument(Shape cannot be null); } void execute() override { shape_-move(dx_, dy_); std::cout 移动图形。 std::endl; } void undo() override { shape_-move(-dx_, -dy_); // 反向移动即为撤销 std::cout 撤销移动。 std::endl; } private: Shape* shape_; int dx_, dy_; };7.3 组装并运行int main() { Canvas canvas; CommandManager cmdManager; std::cout 创建绘图命令... std::endl; // 命令1添加一条线 auto line std::make_uniqueLine(0, 0, 100, 100); cmdManager.executeCommand(std::make_uniqueAddShapeCommand(canvas, std::move(line))); // 命令2添加一个矩形 auto rect std::make_uniqueRectangle(50, 50, 80, 60); cmdManager.executeCommand(std::make_uniqueAddShapeCommand(canvas, std::move(rect))); canvas.drawAll(); // 命令3移动最后一个图形矩形 Shape* lastShape canvas.getLastShape(); if (lastShape) { cmdManager.executeCommand(std::make_uniqueMoveShapeCommand(lastShape, 10, 20)); } canvas.drawAll(); std::cout \n--- 执行撤销 --- std::endl; cmdManager.undo(); // 撤销移动 canvas.drawAll(); cmdManager.undo(); // 撤销添加矩形 canvas.drawAll(); cmdManager.undo(); // 撤销添加线段 canvas.drawAll(); std::cout \n--- 执行重做 --- std::endl; cmdManager.redo(); // 重做添加线段 cmdManager.redo(); // 重做添加矩形 cmdManager.redo(); // 重做移动 canvas.drawAll(); return 0; }这个案例展示了命令模式在一个更接近真实应用场景下的使用。它清晰地分离了UI交互命令的触发、业务逻辑命令的执行和数据模型画布和图形。通过CommandManager我们获得了完整的撤销/重做能力。你可以在此基础上轻松扩展新的图形类型新的Shape子类和新的操作命令新的Command子类而无需修改现有的命令管理或画布逻辑这充分体现了命令模式对扩展的友好性。命令模式是C中构建灵活、可维护交互系统的强大工具。从简单的解耦开始到支持撤销重做、宏命令再到现代C下的优化实现它展示了一个良好设计模式如何随着需求增长而优雅地演进。理解其精髓——将“请求”对象化——并能根据具体场景权衡实现细节是每一位C开发者迈向高级软件设计的重要一步。在实际项目中结合智能指针、Lambda表达式等现代特性命令模式能帮助你构建出既强大又清晰的代码架构。