1. 项目概述为什么我们需要命令模式在C的世界里尤其是在构建那些需要处理复杂用户交互、支持撤销/重做或者需要将操作请求排队、记录日志的系统时我们常常会遇到一个核心矛盾操作的发起者比如一个按钮点击事件和操作的具体执行者比如一个负责修改文档数据的对象之间耦合度太高了。按钮类直接调用了文档类的某个方法一旦执行逻辑需要改变或者你想给这个操作加上日志、延迟执行、撤销等功能你就得回过头去修改按钮类的代码。这种牵一发而动全身的感觉相信写过GUI程序或者游戏逻辑的开发者都深有体会。命令模式Command Pattern就是为了解决这个“紧耦合”问题而生的。它的核心思想非常直观把一个请求或操作封装成一个对象。这样一来你可以像处理其他任何对象一样来处理这个“命令”对象你可以将它参数化、放入队列、记录到日志中或者支持撤销操作。对于C开发者而言深入理解并实现命令模式不仅仅是掌握了一种设计模式更是获得了一种构建灵活、可扩展且易于维护的软件架构的关键工具。无论是开发一个复杂的文本编辑器、一个游戏引擎的输入系统还是一个需要支持宏命令的自动化工具命令模式都能大显身手。2. 命令模式的核心思想与UML类图解析2.1 模式的核心参与者命令模式通常涉及以下几个关键角色理解它们之间的关系是掌握其实现的关键命令接口Command这是一个抽象基类通常只声明一个execute()纯虚函数。它是所有具体命令的契约定义了命令对象的统一执行入口。在C中我们常用一个包含纯虚函数的类来实现。具体命令ConcreteCommand这是命令接口的实现类。它持有一个对**接收者Receiver**对象的引用并将一个或多个接收者的动作绑定到自身。execute()方法的具体实现就是去调用这些接收者的具体方法来完成真正的业务逻辑。一个具体命令对象本质上就是“知道做什么接收者的方法”和“对谁做接收者对象”。接收者Receiver真正知道如何执行与请求相关操作的对象。任何类都可以成为接收者。具体命令对象会将请求委托给接收者对象来执行。例如在一个文档编辑器中“文档类”就是接收者它拥有insertText(),deleteText(),formatParagraph()等方法。调用者/触发者Invoker负责要求命令执行请求的对象。它持有一个或多个命令对象的引用或指针。调用者并不关心具体是哪个命令也不关心命令是如何实现的它只负责在适当的时机如按钮被点击、定时器触发调用命令对象的execute()方法。调用者通常还可以提供支持命令队列、日志和撤销的设施。客户端Client创建具体命令对象并设置其接收者的那个角色。客户端将创建好的命令对象传递给调用者。在GUI程序中界面初始化代码或控制器常常扮演客户端的角色。2.2 UML类图与关系用文字描述可能有点抽象我们来看一下它们之间的静态结构关系注意这里用文字描述代替图表以便于在任何环境下阅读调用者 (Invoker) - 持有命令指针/引用 setCommand(Command*) executeCommand() ↑ 关联持有命令 | 命令接口 (Command) |—— 具体命令 (ConcreteCommand) execute() 0 - 持有接收者指针/引用 execute() { receiver-action(); } ↑ 实现 | 客户端 (Client) ——创建并组装—— 接收者 (Receiver) action()关系解读调用者与命令接口是关联关系。调用者只知道有一个可以执行的东西命令但不知道其具体细节。具体命令继承自命令接口并关联着一个接收者。它把对接收者的调用封装在execute()里。客户端负责创建接收者和具体命令对象并将两者绑定最后把命令对象交给调用者。这种结构的最大好处在于解耦调用者与接收者之间没有直接依赖。调用者只需要面对一个统一的命令接口。你可以随时更换、组合或扩展新的命令而无需修改调用者的代码。注意在实际的C代码中我们需要特别注意对象生命周期和内存管理。如果命令对象是在堆上动态创建的调用者或客户端需要负责在适当的时候释放内存或者更现代的做法是使用智能指针如std::unique_ptrCommand来自动管理。3. 在C中实现命令模式的详细步骤理论说再多不如一行代码。让我们通过一个经典的“文本编辑器”例子来一步步实现一个支持复制、粘贴和撤销的命令模式系统。3.1 第一步定义命令接口这是所有命令的基石。我们使用一个抽象基类。// Command.h #ifndef COMMAND_H #define COMMAND_H class Command { public: virtual ~Command() default; // 基类虚析构函数确保派生类正确释放 virtual void execute() 0; // 执行命令 virtual void undo() 0; // 撤销命令为支持撤销功能 }; #endif // COMMAND_H这里我们增加了一个undo()纯虚函数这是支持撤销操作的关键。一个完整的命令对象应该知道如何执行自己也应该知道如何撤销自己。3.2 第二步实现接收者接收者是真正干活的类。在这个例子里我们模拟一个简单的文档。// Document.h / Document.cpp #include string #include iostream #include stack class Document { private: std::string content_; std::stackstd::string history_; // 用于简单演示撤销的历史栈 public: Document(const std::string initialContent ) : content_(initialContent) {} void copy(size_t pos, size_t len) { // 模拟复制操作这里简单打印 std::string copied content_.substr(pos, len); std::cout [Document] 复制了文本: \ copied \ 到剪贴板模拟\n; // 实际中这里会将文本存入某个全局或上下文相关的剪贴板 } void paste(const std::string text, size_t pos) { history_.push(content_); // 执行前保存状态用于撤销 content_.insert(pos, text); std::cout [Document] 在位置 pos 粘贴了文本: \ text \\n; std::cout [Document] 当前内容: \ content_ \\n; } void deleteText(size_t pos, size_t len) { history_.push(content_); // 执行前保存状态 std::string deleted content_.substr(pos, len); content_.erase(pos, len); std::cout [Document] 删除了文本: \ deleted \\n; std::cout [Document] 当前内容: \ content_ \\n; } // 一个简单的撤销实现恢复到上一次历史状态 void undoLastChange() { if (!history_.empty()) { content_ history_.top(); history_.pop(); std::cout [Document] 撤销操作内容恢复为: \ content_ \\n; } else { std::cout [Document] 没有更多历史可以撤销。\n; } } const std::string getContent() const { return content_; } };这个Document类提供了copy,paste,deleteText等具体操作并且用一个栈来简陋地记录历史状态以支持撤销。在实际项目中更精细的撤销/重做通常由命令对象本身来维护状态。3.3 第三步创建具体命令类现在我们将Document的每个操作封装成一个具体的命令类。CopyCommand.h/cpp:#ifndef COPY_COMMAND_H #define COPY_COMMAND_H #include “Command.h” #include “Document.h” #include memory class CopyCommand : public Command { private: std::shared_ptrDocument document_; // 使用智能指针管理接收者 size_t position_; size_t length_; public: CopyCommand(std::shared_ptrDocument doc, size_t pos, size_t len) : document_(std::move(doc)), position_(pos), length_(len) {} void execute() override { if (document_) { document_-copy(position_, length_); } } void undo() override { // 复制操作通常不需要撤销或者撤销意味着清空剪贴板 // 这里为了接口统一我们什么也不做或者可以输出一条信息。 std::cout “[CopyCommand] 复制操作不可撤销或无需撤销。\n”; } }; #endif // COPY_COMMAND_HPasteCommand.h/cpp:#ifndef PASTE_COMMAND_H #define PASTE_COMMAND_H #include “Command.h” #include “Document.h” #include memory #include string class PasteCommand : public Command { private: std::shared_ptrDocument document_; std::string textToPaste_; size_t position_; std::string previousState_; // 用于撤销保存粘贴前的内容 public: PasteCommand(std::shared_ptrDocument doc, const std::string text, size_t pos) : document_(std::move(doc)), textToPaste_(text), position_(pos) {} void execute() override { if (document_) { previousState_ document_-getContent(); // 执行前保存状态 document_-paste(textToPaste_, position_); } } void undo() override { if (document_ !previousState_.empty()) { // 简单的撤销将文档内容恢复为执行前的状态 // 注意这种全局恢复可能不是最高效的对于复杂操作需要更精细的状态管理 std::cout “[PasteCommand] 执行撤销...\n”; // 这里我们需要一个方法来设置文档内容。为了示例我们假设Document有setContent方法。 // 实际中undo逻辑可能更复杂比如记录被插入文本的位置和长度然后删除它。 // 我们修改一下Document类增加一个“恢复状态”的方法或者命令自己记录足够的信息。 // 为了简化我们这里调用document的撤销方法如果存在。 document_-undoLastChange(); // 利用Document简陋的历史栈 } } }; #endif // PASTE_COMMAND_H在PasteCommand中我们演示了两种撤销思路一是命令自己保存受影响的状态previousState_二是委托给接收者document_-undoLastChange()。在大型应用中前者更常见因为命令对象可以精确记录最小化的变更信息如“在位置X插入了字符串S”撤销时只需删除该字符串效率更高。3.4 第四步构建调用者调用者通常是UI控件、菜单项、快捷键管理器或一个全局的命令管理器。// Invoker.h / Invoker.cpp #include “Command.h” #include memory #include vector class Invoker { private: std::vectorstd::shared_ptrCommand commandHistory_; // 命令历史用于宏命令或高级撤销 std::shared_ptrCommand currentCommand_; // 当前设置的命令 public: void setCommand(std::shared_ptrCommand cmd) { currentCommand_ std::move(cmd); } void executeCommand() { if (currentCommand_) { currentCommand_-execute(); commandHistory_.push_back(currentCommand_); // 记录到历史 } else { std::cout “[Invoker] 没有设置任何命令。\n”; } } void undoLastCommand() { if (!commandHistory_.empty()) { auto lastCmd commandHistory_.back(); lastCmd-undo(); commandHistory_.pop_back(); // 撤销后从历史中移除简单模型 } else { std::cout “[Invoker] 没有命令可以撤销。\n”; } } // 可以添加重做(redo)、执行宏命令等方法 };这个调用者不仅可以执行命令还维护了一个简单的命令历史栈从而实现了跨命令的撤销功能。这是一个比在Document内部维护状态更清晰、更符合命令模式哲学的做法因为撤销的逻辑被提升到了命令调度层。3.5 第五步客户端组装一切最后在main函数或应用程序的初始化代码中我们将所有部分组装起来。// main.cpp #include “Document.h” #include “CopyCommand.h” #include “PasteCommand.h” #include “Invoker.h” #include iostream #include memory int main() { // 1. 创建接收者 auto myDoc std::make_sharedDocument(“Hello, World!”); // 2. 创建调用者例如一个工具栏按钮或菜单项 Invoker copyButton, pasteButton; // 3. 客户端创建具体命令并设置给调用者 // 假设我们有一个选中区域位置7长度5 (“World”) auto copyCmd std::make_sharedCopyCommand(myDoc, 7, 5); copyButton.setCommand(copyCmd); // 粘贴命令准备粘贴“C” auto pasteCmd std::make_sharedPasteCommand(myDoc, “C ”, 7); // 在”World”前粘贴 pasteButton.setCommand(pasteCmd); std::cout “初始文档: \”” myDoc-getContent() “\”\n\n”; // 4. 用户交互模拟点击按钮 std::cout “用户点击‘复制’按钮:\n”; copyButton.executeCommand(); std::cout “\n用户点击‘粘贴’按钮:\n”; pasteButton.executeCommand(); std::cout “\n用户点击‘撤销’按钮:\n”; pasteButton.undoLastCommand(); // 撤销刚才的粘贴操作 // 5. 动态改变命令展示灵活性 std::cout “\n--- 动态命令替换示例 ---\n”; // 假设粘贴按钮现在要执行一个删除命令 auto deleteCmd std::make_shared/*假设的DeleteCommand*/(myDoc, 7, 3); // pasteButton.setCommand(deleteCmd); // 同样的调用者可以执行完全不同的操作 // pasteButton.executeCommand(); // 现在执行的是删除 return 0; }通过这个完整的例子你可以清晰地看到命令模式如何将“发起请求的对象”按钮、“执行的请求”命令对象和“执行操作的对象”文档分离开来。增加一个新的操作如“加粗”你只需要创建一个新的BoldCommand类并在客户端组装它完全不需要修改现有的Invoker或Document类。4. 命令模式的高级应用与变体掌握了基础实现后我们来看看命令模式在C项目中的一些高级用法和常见变体这些才是体现其威力的地方。4.1 支持撤销与重做Undo/Redo我们前面的例子已经初步实现了撤销。一个健壮的撤销/重做系统通常需要精确的状态记录命令对象应记录执行时改变的最小数据集而不是整个文档的快照。例如PasteCommand记录插入的位置和内容撤销时只需删除那段内容。命令历史栈使用两个栈——undoStack和redoStack。执行命令时将其压入undoStack并清空redoStack。撤销时从undoStack弹出命令执行其undo()然后压入redoStack。重做时从redoStack弹出命令执行其execute()然后压入undoStack。复合命令宏命令将多个命令组合成一个命令使其可以作为一个单元被撤销/重做。4.2 复合命令宏命令复合命令是命令模式的一个强大变体它本身也是一个命令但内部包含了一个子命令列表。class MacroCommand : public Command { private: std::vectorstd::shared_ptrCommand commands_; public: void addCommand(std::shared_ptrCommand cmd) { commands_.push_back(std::move(cmd)); } void execute() override { for (auto cmd : commands_) { cmd-execute(); } } void undo() override { // 注意撤销顺序应与执行顺序相反 for (auto it commands_.rbegin(); it ! commands_.rend(); it) { (*it)-undo(); } } };这样你可以将一系列操作如“全选”-“复制”-“移动到文末”-“粘贴”录制为一个宏命令然后一次性执行或撤销。这在自动化脚本、批量操作中非常有用。4.3 命令队列与线程池命令对象是天然的任务单元。你可以将命令放入一个队列中然后由另一个线程或线程池按顺序或优先级取出并执行。这是实现异步操作、任务调度、日志回放等功能的经典方式。class CommandQueue { std::queuestd::shared_ptrCommand queue_; std::mutex mtx_; std::condition_variable cv_; bool stop_ false; public: void push(std::shared_ptrCommand cmd) { std::lock_guardstd::mutex lock(mtx_); queue_.push(std::move(cmd)); cv_.notify_one(); } std::shared_ptrCommand pop() { std::unique_lockstd::mutex lock(mtx_); cv_.wait(lock, [this](){ return stop_ || !queue_.empty(); }); if (stop_ queue_.empty()) return nullptr; auto cmd std::move(queue_.front()); queue_.pop(); return cmd; } void stop() { { std::lock_guardstd::mutex lock(mtx_); stop_ true; } cv_.notify_all(); } }; // 工作线程 void workerThread(CommandQueue queue) { while (auto cmd queue.pop()) { cmd-execute(); } }在这个模型中UI线程快速地将用户操作封装成命令并推入队列然后立即返回保持界面响应。后台的工作线程则不断从队列中取出命令并执行繁重的操作如文件I/O、网络请求、复杂计算。4.4 空对象与默认行为有时调用者可能没有设置命令。为了避免空指针检查可以引入一个“空命令”Null Command对象其execute()和undo()方法为空操作。这遵循了“空对象模式”简化了调用者的代码。class NullCommand : public Command { public: void execute() override { /* 什么都不做 */ } void undo() override { /* 什么都不做 */ } }; // 在Invoker中可以将currentCommand_默认初始化为一个NullCommand的共享实例。5. 在C中应用命令模式的实战心得与避坑指南经过多年在C项目中使用命令模式我积累了一些实战经验和需要特别注意的“坑”。5.1 内存管理与对象生命周期这是C实现命令模式最需要小心的地方。优先使用智能指针std::shared_ptrCommand是管理命令对象生命周期的安全选择特别是当命令可能在多个地方被引用时如历史栈、重做栈、队列。std::unique_ptrCommand则适用于所有权单一的场景。注意接收者的生命周期命令对象通常持有接收者的引用或指针。必须确保在命令执行时接收者对象仍然有效。如果接收者可能先于命令被销毁考虑使用std::weak_ptr来持有接收者并在执行前检查其有效性。命令的复制与移动命令对象可能包含状态如执行参数、用于撤销的备份。如果需要将命令存入容器或传递请确保你的命令类定义了正确的拷贝构造函数/移动构造函数和赋值运算符或者禁用拷贝只允许移动。5.2 性能考量命令对象的创建开销如果命令执行得非常频繁如游戏中的每一帧输入频繁的动态内存分配new命令对象可能成为性能瓶颈。可以考虑使用对象池或自定义分配器来复用命令对象。状态存储开销为了支持撤销命令可能需要保存大量数据。评估是否需要全状态保存如整个文档序列化还是增量保存如只记录差异。对于大型数据增量保存通常是更好的选择。5.3 设计权衡何时用何时不用非常适合命令模式的场景需要撤销/重做功能这是命令模式的杀手级应用。需要将操作参数化、队列化或日志化如任务调度系统、事务处理、操作记录与回放。需要支持宏命令或脚本将一系列操作组合执行。GUI系统菜单项、工具栏按钮、快捷键绑定每个都可以对应一个命令对象。网络请求将请求封装成命令便于排队、重试、取消。可能不太适合或需要简化的场景命令极其简单且唯一如果操作只有一个固定的接收者和一个固定的方法直接调用可能更简单。对性能有极端要求命令对象的抽象会带来一定的间接性开销。在性能关键的循环中可能需要更直接的方式。命令不需要状态如果操作没有参数也不需要撤销那么一个简单的函数指针或std::function可能就足够了。5.4 一个常见的陷阱过度设计命令模式是一个强大的工具但不要为了用模式而用模式。如果你的系统只有两三个简单的、不需要撤销的操作直接调用可能使代码更清晰。判断的标准是变化点如果你的系统未来很可能需要支持撤销、队列、日志或动态改变操作那么提前引入命令模式是值得的否则可以等到需要时再重构。5.5 与现代C特性的结合std::function与Lambda表达式对于非常简单的命令你可以使用std::functionvoid()作为命令接口并用Lambda来创建命令。这省去了定义具体命令类的麻烦但牺牲了类型安全性和可能的状态管理能力复杂的撤销逻辑难以用Lambda表达。class SimpleInvoker { std::functionvoid() command_; public: void setCommand(std::functionvoid() cmd) { command_ std::move(cmd); } void execute() { if (command_) command_(); } }; // 使用 Document doc; SimpleInvoker invoker; invoker.setCommand([doc]() { doc.paste(“text”, 0); });模板化命令如果你希望命令接口能处理不同类型的接收者和参数可以使用模板。但这会增加复杂性通常只在框架或库的开发中考虑。命令模式在C中是一种将“行为请求”对象化的优雅方法它通过解耦请求发送者和接收者极大地提升了代码的灵活性和可维护性。从简单的GUI事件处理到复杂的交易系统其思想贯穿其中。理解其核心掌握其C实现的细节特别是内存和生命周期管理再结合项目实际需求灵活运用和变通你就能真正驾驭这个模式写出更干净、更强大的C代码。
C++命令模式实战:解耦请求与实现,构建可撤销的灵活架构
1. 项目概述为什么我们需要命令模式在C的世界里尤其是在构建那些需要处理复杂用户交互、支持撤销/重做或者需要将操作请求排队、记录日志的系统时我们常常会遇到一个核心矛盾操作的发起者比如一个按钮点击事件和操作的具体执行者比如一个负责修改文档数据的对象之间耦合度太高了。按钮类直接调用了文档类的某个方法一旦执行逻辑需要改变或者你想给这个操作加上日志、延迟执行、撤销等功能你就得回过头去修改按钮类的代码。这种牵一发而动全身的感觉相信写过GUI程序或者游戏逻辑的开发者都深有体会。命令模式Command Pattern就是为了解决这个“紧耦合”问题而生的。它的核心思想非常直观把一个请求或操作封装成一个对象。这样一来你可以像处理其他任何对象一样来处理这个“命令”对象你可以将它参数化、放入队列、记录到日志中或者支持撤销操作。对于C开发者而言深入理解并实现命令模式不仅仅是掌握了一种设计模式更是获得了一种构建灵活、可扩展且易于维护的软件架构的关键工具。无论是开发一个复杂的文本编辑器、一个游戏引擎的输入系统还是一个需要支持宏命令的自动化工具命令模式都能大显身手。2. 命令模式的核心思想与UML类图解析2.1 模式的核心参与者命令模式通常涉及以下几个关键角色理解它们之间的关系是掌握其实现的关键命令接口Command这是一个抽象基类通常只声明一个execute()纯虚函数。它是所有具体命令的契约定义了命令对象的统一执行入口。在C中我们常用一个包含纯虚函数的类来实现。具体命令ConcreteCommand这是命令接口的实现类。它持有一个对**接收者Receiver**对象的引用并将一个或多个接收者的动作绑定到自身。execute()方法的具体实现就是去调用这些接收者的具体方法来完成真正的业务逻辑。一个具体命令对象本质上就是“知道做什么接收者的方法”和“对谁做接收者对象”。接收者Receiver真正知道如何执行与请求相关操作的对象。任何类都可以成为接收者。具体命令对象会将请求委托给接收者对象来执行。例如在一个文档编辑器中“文档类”就是接收者它拥有insertText(),deleteText(),formatParagraph()等方法。调用者/触发者Invoker负责要求命令执行请求的对象。它持有一个或多个命令对象的引用或指针。调用者并不关心具体是哪个命令也不关心命令是如何实现的它只负责在适当的时机如按钮被点击、定时器触发调用命令对象的execute()方法。调用者通常还可以提供支持命令队列、日志和撤销的设施。客户端Client创建具体命令对象并设置其接收者的那个角色。客户端将创建好的命令对象传递给调用者。在GUI程序中界面初始化代码或控制器常常扮演客户端的角色。2.2 UML类图与关系用文字描述可能有点抽象我们来看一下它们之间的静态结构关系注意这里用文字描述代替图表以便于在任何环境下阅读调用者 (Invoker) - 持有命令指针/引用 setCommand(Command*) executeCommand() ↑ 关联持有命令 | 命令接口 (Command) |—— 具体命令 (ConcreteCommand) execute() 0 - 持有接收者指针/引用 execute() { receiver-action(); } ↑ 实现 | 客户端 (Client) ——创建并组装—— 接收者 (Receiver) action()关系解读调用者与命令接口是关联关系。调用者只知道有一个可以执行的东西命令但不知道其具体细节。具体命令继承自命令接口并关联着一个接收者。它把对接收者的调用封装在execute()里。客户端负责创建接收者和具体命令对象并将两者绑定最后把命令对象交给调用者。这种结构的最大好处在于解耦调用者与接收者之间没有直接依赖。调用者只需要面对一个统一的命令接口。你可以随时更换、组合或扩展新的命令而无需修改调用者的代码。注意在实际的C代码中我们需要特别注意对象生命周期和内存管理。如果命令对象是在堆上动态创建的调用者或客户端需要负责在适当的时候释放内存或者更现代的做法是使用智能指针如std::unique_ptrCommand来自动管理。3. 在C中实现命令模式的详细步骤理论说再多不如一行代码。让我们通过一个经典的“文本编辑器”例子来一步步实现一个支持复制、粘贴和撤销的命令模式系统。3.1 第一步定义命令接口这是所有命令的基石。我们使用一个抽象基类。// Command.h #ifndef COMMAND_H #define COMMAND_H class Command { public: virtual ~Command() default; // 基类虚析构函数确保派生类正确释放 virtual void execute() 0; // 执行命令 virtual void undo() 0; // 撤销命令为支持撤销功能 }; #endif // COMMAND_H这里我们增加了一个undo()纯虚函数这是支持撤销操作的关键。一个完整的命令对象应该知道如何执行自己也应该知道如何撤销自己。3.2 第二步实现接收者接收者是真正干活的类。在这个例子里我们模拟一个简单的文档。// Document.h / Document.cpp #include string #include iostream #include stack class Document { private: std::string content_; std::stackstd::string history_; // 用于简单演示撤销的历史栈 public: Document(const std::string initialContent ) : content_(initialContent) {} void copy(size_t pos, size_t len) { // 模拟复制操作这里简单打印 std::string copied content_.substr(pos, len); std::cout [Document] 复制了文本: \ copied \ 到剪贴板模拟\n; // 实际中这里会将文本存入某个全局或上下文相关的剪贴板 } void paste(const std::string text, size_t pos) { history_.push(content_); // 执行前保存状态用于撤销 content_.insert(pos, text); std::cout [Document] 在位置 pos 粘贴了文本: \ text \\n; std::cout [Document] 当前内容: \ content_ \\n; } void deleteText(size_t pos, size_t len) { history_.push(content_); // 执行前保存状态 std::string deleted content_.substr(pos, len); content_.erase(pos, len); std::cout [Document] 删除了文本: \ deleted \\n; std::cout [Document] 当前内容: \ content_ \\n; } // 一个简单的撤销实现恢复到上一次历史状态 void undoLastChange() { if (!history_.empty()) { content_ history_.top(); history_.pop(); std::cout [Document] 撤销操作内容恢复为: \ content_ \\n; } else { std::cout [Document] 没有更多历史可以撤销。\n; } } const std::string getContent() const { return content_; } };这个Document类提供了copy,paste,deleteText等具体操作并且用一个栈来简陋地记录历史状态以支持撤销。在实际项目中更精细的撤销/重做通常由命令对象本身来维护状态。3.3 第三步创建具体命令类现在我们将Document的每个操作封装成一个具体的命令类。CopyCommand.h/cpp:#ifndef COPY_COMMAND_H #define COPY_COMMAND_H #include “Command.h” #include “Document.h” #include memory class CopyCommand : public Command { private: std::shared_ptrDocument document_; // 使用智能指针管理接收者 size_t position_; size_t length_; public: CopyCommand(std::shared_ptrDocument doc, size_t pos, size_t len) : document_(std::move(doc)), position_(pos), length_(len) {} void execute() override { if (document_) { document_-copy(position_, length_); } } void undo() override { // 复制操作通常不需要撤销或者撤销意味着清空剪贴板 // 这里为了接口统一我们什么也不做或者可以输出一条信息。 std::cout “[CopyCommand] 复制操作不可撤销或无需撤销。\n”; } }; #endif // COPY_COMMAND_HPasteCommand.h/cpp:#ifndef PASTE_COMMAND_H #define PASTE_COMMAND_H #include “Command.h” #include “Document.h” #include memory #include string class PasteCommand : public Command { private: std::shared_ptrDocument document_; std::string textToPaste_; size_t position_; std::string previousState_; // 用于撤销保存粘贴前的内容 public: PasteCommand(std::shared_ptrDocument doc, const std::string text, size_t pos) : document_(std::move(doc)), textToPaste_(text), position_(pos) {} void execute() override { if (document_) { previousState_ document_-getContent(); // 执行前保存状态 document_-paste(textToPaste_, position_); } } void undo() override { if (document_ !previousState_.empty()) { // 简单的撤销将文档内容恢复为执行前的状态 // 注意这种全局恢复可能不是最高效的对于复杂操作需要更精细的状态管理 std::cout “[PasteCommand] 执行撤销...\n”; // 这里我们需要一个方法来设置文档内容。为了示例我们假设Document有setContent方法。 // 实际中undo逻辑可能更复杂比如记录被插入文本的位置和长度然后删除它。 // 我们修改一下Document类增加一个“恢复状态”的方法或者命令自己记录足够的信息。 // 为了简化我们这里调用document的撤销方法如果存在。 document_-undoLastChange(); // 利用Document简陋的历史栈 } } }; #endif // PASTE_COMMAND_H在PasteCommand中我们演示了两种撤销思路一是命令自己保存受影响的状态previousState_二是委托给接收者document_-undoLastChange()。在大型应用中前者更常见因为命令对象可以精确记录最小化的变更信息如“在位置X插入了字符串S”撤销时只需删除该字符串效率更高。3.4 第四步构建调用者调用者通常是UI控件、菜单项、快捷键管理器或一个全局的命令管理器。// Invoker.h / Invoker.cpp #include “Command.h” #include memory #include vector class Invoker { private: std::vectorstd::shared_ptrCommand commandHistory_; // 命令历史用于宏命令或高级撤销 std::shared_ptrCommand currentCommand_; // 当前设置的命令 public: void setCommand(std::shared_ptrCommand cmd) { currentCommand_ std::move(cmd); } void executeCommand() { if (currentCommand_) { currentCommand_-execute(); commandHistory_.push_back(currentCommand_); // 记录到历史 } else { std::cout “[Invoker] 没有设置任何命令。\n”; } } void undoLastCommand() { if (!commandHistory_.empty()) { auto lastCmd commandHistory_.back(); lastCmd-undo(); commandHistory_.pop_back(); // 撤销后从历史中移除简单模型 } else { std::cout “[Invoker] 没有命令可以撤销。\n”; } } // 可以添加重做(redo)、执行宏命令等方法 };这个调用者不仅可以执行命令还维护了一个简单的命令历史栈从而实现了跨命令的撤销功能。这是一个比在Document内部维护状态更清晰、更符合命令模式哲学的做法因为撤销的逻辑被提升到了命令调度层。3.5 第五步客户端组装一切最后在main函数或应用程序的初始化代码中我们将所有部分组装起来。// main.cpp #include “Document.h” #include “CopyCommand.h” #include “PasteCommand.h” #include “Invoker.h” #include iostream #include memory int main() { // 1. 创建接收者 auto myDoc std::make_sharedDocument(“Hello, World!”); // 2. 创建调用者例如一个工具栏按钮或菜单项 Invoker copyButton, pasteButton; // 3. 客户端创建具体命令并设置给调用者 // 假设我们有一个选中区域位置7长度5 (“World”) auto copyCmd std::make_sharedCopyCommand(myDoc, 7, 5); copyButton.setCommand(copyCmd); // 粘贴命令准备粘贴“C” auto pasteCmd std::make_sharedPasteCommand(myDoc, “C ”, 7); // 在”World”前粘贴 pasteButton.setCommand(pasteCmd); std::cout “初始文档: \”” myDoc-getContent() “\”\n\n”; // 4. 用户交互模拟点击按钮 std::cout “用户点击‘复制’按钮:\n”; copyButton.executeCommand(); std::cout “\n用户点击‘粘贴’按钮:\n”; pasteButton.executeCommand(); std::cout “\n用户点击‘撤销’按钮:\n”; pasteButton.undoLastCommand(); // 撤销刚才的粘贴操作 // 5. 动态改变命令展示灵活性 std::cout “\n--- 动态命令替换示例 ---\n”; // 假设粘贴按钮现在要执行一个删除命令 auto deleteCmd std::make_shared/*假设的DeleteCommand*/(myDoc, 7, 3); // pasteButton.setCommand(deleteCmd); // 同样的调用者可以执行完全不同的操作 // pasteButton.executeCommand(); // 现在执行的是删除 return 0; }通过这个完整的例子你可以清晰地看到命令模式如何将“发起请求的对象”按钮、“执行的请求”命令对象和“执行操作的对象”文档分离开来。增加一个新的操作如“加粗”你只需要创建一个新的BoldCommand类并在客户端组装它完全不需要修改现有的Invoker或Document类。4. 命令模式的高级应用与变体掌握了基础实现后我们来看看命令模式在C项目中的一些高级用法和常见变体这些才是体现其威力的地方。4.1 支持撤销与重做Undo/Redo我们前面的例子已经初步实现了撤销。一个健壮的撤销/重做系统通常需要精确的状态记录命令对象应记录执行时改变的最小数据集而不是整个文档的快照。例如PasteCommand记录插入的位置和内容撤销时只需删除那段内容。命令历史栈使用两个栈——undoStack和redoStack。执行命令时将其压入undoStack并清空redoStack。撤销时从undoStack弹出命令执行其undo()然后压入redoStack。重做时从redoStack弹出命令执行其execute()然后压入undoStack。复合命令宏命令将多个命令组合成一个命令使其可以作为一个单元被撤销/重做。4.2 复合命令宏命令复合命令是命令模式的一个强大变体它本身也是一个命令但内部包含了一个子命令列表。class MacroCommand : public Command { private: std::vectorstd::shared_ptrCommand commands_; public: void addCommand(std::shared_ptrCommand cmd) { commands_.push_back(std::move(cmd)); } void execute() override { for (auto cmd : commands_) { cmd-execute(); } } void undo() override { // 注意撤销顺序应与执行顺序相反 for (auto it commands_.rbegin(); it ! commands_.rend(); it) { (*it)-undo(); } } };这样你可以将一系列操作如“全选”-“复制”-“移动到文末”-“粘贴”录制为一个宏命令然后一次性执行或撤销。这在自动化脚本、批量操作中非常有用。4.3 命令队列与线程池命令对象是天然的任务单元。你可以将命令放入一个队列中然后由另一个线程或线程池按顺序或优先级取出并执行。这是实现异步操作、任务调度、日志回放等功能的经典方式。class CommandQueue { std::queuestd::shared_ptrCommand queue_; std::mutex mtx_; std::condition_variable cv_; bool stop_ false; public: void push(std::shared_ptrCommand cmd) { std::lock_guardstd::mutex lock(mtx_); queue_.push(std::move(cmd)); cv_.notify_one(); } std::shared_ptrCommand pop() { std::unique_lockstd::mutex lock(mtx_); cv_.wait(lock, [this](){ return stop_ || !queue_.empty(); }); if (stop_ queue_.empty()) return nullptr; auto cmd std::move(queue_.front()); queue_.pop(); return cmd; } void stop() { { std::lock_guardstd::mutex lock(mtx_); stop_ true; } cv_.notify_all(); } }; // 工作线程 void workerThread(CommandQueue queue) { while (auto cmd queue.pop()) { cmd-execute(); } }在这个模型中UI线程快速地将用户操作封装成命令并推入队列然后立即返回保持界面响应。后台的工作线程则不断从队列中取出命令并执行繁重的操作如文件I/O、网络请求、复杂计算。4.4 空对象与默认行为有时调用者可能没有设置命令。为了避免空指针检查可以引入一个“空命令”Null Command对象其execute()和undo()方法为空操作。这遵循了“空对象模式”简化了调用者的代码。class NullCommand : public Command { public: void execute() override { /* 什么都不做 */ } void undo() override { /* 什么都不做 */ } }; // 在Invoker中可以将currentCommand_默认初始化为一个NullCommand的共享实例。5. 在C中应用命令模式的实战心得与避坑指南经过多年在C项目中使用命令模式我积累了一些实战经验和需要特别注意的“坑”。5.1 内存管理与对象生命周期这是C实现命令模式最需要小心的地方。优先使用智能指针std::shared_ptrCommand是管理命令对象生命周期的安全选择特别是当命令可能在多个地方被引用时如历史栈、重做栈、队列。std::unique_ptrCommand则适用于所有权单一的场景。注意接收者的生命周期命令对象通常持有接收者的引用或指针。必须确保在命令执行时接收者对象仍然有效。如果接收者可能先于命令被销毁考虑使用std::weak_ptr来持有接收者并在执行前检查其有效性。命令的复制与移动命令对象可能包含状态如执行参数、用于撤销的备份。如果需要将命令存入容器或传递请确保你的命令类定义了正确的拷贝构造函数/移动构造函数和赋值运算符或者禁用拷贝只允许移动。5.2 性能考量命令对象的创建开销如果命令执行得非常频繁如游戏中的每一帧输入频繁的动态内存分配new命令对象可能成为性能瓶颈。可以考虑使用对象池或自定义分配器来复用命令对象。状态存储开销为了支持撤销命令可能需要保存大量数据。评估是否需要全状态保存如整个文档序列化还是增量保存如只记录差异。对于大型数据增量保存通常是更好的选择。5.3 设计权衡何时用何时不用非常适合命令模式的场景需要撤销/重做功能这是命令模式的杀手级应用。需要将操作参数化、队列化或日志化如任务调度系统、事务处理、操作记录与回放。需要支持宏命令或脚本将一系列操作组合执行。GUI系统菜单项、工具栏按钮、快捷键绑定每个都可以对应一个命令对象。网络请求将请求封装成命令便于排队、重试、取消。可能不太适合或需要简化的场景命令极其简单且唯一如果操作只有一个固定的接收者和一个固定的方法直接调用可能更简单。对性能有极端要求命令对象的抽象会带来一定的间接性开销。在性能关键的循环中可能需要更直接的方式。命令不需要状态如果操作没有参数也不需要撤销那么一个简单的函数指针或std::function可能就足够了。5.4 一个常见的陷阱过度设计命令模式是一个强大的工具但不要为了用模式而用模式。如果你的系统只有两三个简单的、不需要撤销的操作直接调用可能使代码更清晰。判断的标准是变化点如果你的系统未来很可能需要支持撤销、队列、日志或动态改变操作那么提前引入命令模式是值得的否则可以等到需要时再重构。5.5 与现代C特性的结合std::function与Lambda表达式对于非常简单的命令你可以使用std::functionvoid()作为命令接口并用Lambda来创建命令。这省去了定义具体命令类的麻烦但牺牲了类型安全性和可能的状态管理能力复杂的撤销逻辑难以用Lambda表达。class SimpleInvoker { std::functionvoid() command_; public: void setCommand(std::functionvoid() cmd) { command_ std::move(cmd); } void execute() { if (command_) command_(); } }; // 使用 Document doc; SimpleInvoker invoker; invoker.setCommand([doc]() { doc.paste(“text”, 0); });模板化命令如果你希望命令接口能处理不同类型的接收者和参数可以使用模板。但这会增加复杂性通常只在框架或库的开发中考虑。命令模式在C中是一种将“行为请求”对象化的优雅方法它通过解耦请求发送者和接收者极大地提升了代码的灵活性和可维护性。从简单的GUI事件处理到复杂的交易系统其思想贯穿其中。理解其核心掌握其C实现的细节特别是内存和生命周期管理再结合项目实际需求灵活运用和变通你就能真正驾驭这个模式写出更干净、更强大的C代码。