1. 命令模式:从理论到实战的跨越
第一次接触命令模式是在重构一个老旧的C++游戏引擎时。那个项目里充斥着这样的代码:if (input == 'A') player.moveLeft(); else if (input == 'D')...。当我需要添加新操作时,不得不修改这段已经超过300行的输入处理函数——这简直就是维护者的噩梦。
命令模式的核心价值在于将"做什么"(请求内容)与"谁来做"(执行对象)解耦。想象餐厅点餐的场景:顾客(调用者)只需要告诉服务员(调用对象)想要什么菜(命令对象),而不需要关心厨师(接收者)具体在厨房如何烹饪。这种间接性带来的灵活性,在软件开发中尤为珍贵。
在C++中实现命令模式时,我们通常会遇到几个关键决策点:
- 是否需要支持撤销/重做?
- 命令的执行是否需要延迟或排队?
- 命令对象的生命周期如何管理?
这些问题的答案将直接影响我们的实现方式。接下来,我将通过一个游戏开发中的真实案例,展示如何用现代C++(C++17及以上)实现一个类型安全、高性能的命令系统。
2. 基础实现:从接口设计开始
2.1 定义命令接口
任何命令模式实现的核心都是一个抽象命令接口。在C++中,我们通常将其定义为纯虚类:
class Command { public: virtual ~Command() = default; virtual void execute() = 0; virtual void undo() = 0; // 可选 };这个基础版本已经能满足大多数需求,但我们可以做得更好。考虑到命令可能执行失败,可以扩展为:
class Command { public: enum class Status { Success, Failure }; virtual ~Command() = default; virtual Status execute() = 0; virtual Status undo() = 0; virtual std::string description() const { return ""; } };2.2 具体命令实现
假设我们正在开发一个文本编辑器,实现一个简单的插入文本命令:
class InsertTextCommand : public Command { public: InsertTextCommand(Document& doc, size_t pos, std::string text) : document(doc), position(pos), textToInsert(std::move(text)) {} Status execute() override { if (position > document.length()) return Status::Failure; oldState = document.state(); // 保存当前状态用于撤销 document.insert(position, textToInsert); return Status::Success; } Status undo() override { document.restore(oldState); return Status::Success; } private: Document& document; size_t position; std::string textToInsert; DocumentState oldState; };这里有几个值得注意的实现细节:
- 使用
std::move避免不必要的字符串拷贝 - 在执行前检查position有效性
- 采用备忘录模式保存文档状态实现撤销
2.3 调用者实现
调用者(Invoker)负责触发命令执行。在GUI应用中,这通常是菜单项、按钮等UI元素:
class Button { public: explicit Button(std::unique_ptr<Command> cmd) : command(std::move(cmd)) {} void onClick() { if (command->execute() == Command::Status::Success) { lastExecuted = std::move(command); // 可以在这里将命令加入历史记录 } } private: std::unique_ptr<Command> command; std::unique_ptr<Command> lastExecuted; };3. 高级技巧:现代C++的威力
3.1 使用std::function实现轻量命令
对于简单命令,我们可以利用C++11的std::function避免定义具体命令类:
using LightweightCommand = std::function<Status()>; class FunctionCommand { public: template<typename F> explicit FunctionCommand(F&& f) : func(std::forward<F>(f)) {} Status execute() { return func(); } private: LightweightCommand func; };这种方式的优势在于:
- 无需为每个命令创建新类
- 可以直接捕获lambda表达式
- 性能开销几乎为零
3.2 类型安全的命令分发
当需要根据不同类型执行不同操作时,可以使用std::variant和std::visit:
using CommandVariant = std::variant<InsertCommand, DeleteCommand, FormatCommand>; class CommandProcessor { public: void apply(CommandVariant cmd) { std::visit([this](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, InsertCommand>) { handleInsert(arg); } else if constexpr (...) { // 其他命令处理 } }, cmd); } };3.3 命令队列与异步执行
实现命令队列可以轻松支持宏录制、事务处理等高级功能:
class CommandQueue { public: void add(std::unique_ptr<Command> cmd) { queue.push(std::move(cmd)); } void processAll() { while (!queue.empty()) { auto cmd = std::move(queue.front()); queue.pop(); cmd->execute(); history.push(std::move(cmd)); } } private: std::queue<std::unique_ptr<Command>> queue; std::stack<std::unique_ptr<Command>> history; // 用于撤销 };4. 实战案例:游戏输入系统重构
4.1 问题场景
假设我们有一个传统的游戏输入处理系统:
void processInput(Input input) { switch (input.type) { case InputType::KeyPress: if (input.key == KEY_W) player.moveForward(); else if (input.key == KEY_S) player.moveBack(); // ...更多按键处理 break; case InputType::MouseClick: // 鼠标处理逻辑 break; } }这种实现方式的问题在于:
- 新增输入类型需要修改核心函数
- 难以实现输入重映射
- 无法支持回放和撤销
4.2 命令模式改造
首先定义游戏命令接口:
class GameCommand { public: virtual ~GameCommand() = default; virtual void execute(Player& player) = 0; virtual void undo(Player& player) = 0; };然后实现具体命令:
class MoveCommand : public GameCommand { public: enum class Direction { Forward, Back, Left, Right }; explicit MoveCommand(Direction dir) : direction(dir) {} void execute(Player& player) override { lastPosition = player.position(); switch (direction) { case Direction::Forward: player.moveForward(); break; // ...其他方向 } } void undo(Player& player) override { player.setPosition(lastPosition); } private: Direction direction; Vector3 lastPosition; };4.3 输入映射系统
使用命令模式后,我们可以轻松实现输入重映射:
class InputMapper { public: void bind(InputType type, InputCode code, std::unique_ptr<GameCommand> cmd) { mappings[type][code] = std::move(cmd); } void processInput(const InputEvent& event) { if (auto it = mappings[event.type].find(event.code); it != mappings.end()) { it->second->execute(currentPlayer); commandHistory.push(it->second.get()); } } private: std::unordered_map<InputType, std::unordered_map<InputCode, std::unique_ptr<GameCommand>>> mappings; std::vector<GameCommand*> commandHistory; Player& currentPlayer; };5. 性能优化与陷阱规避
5.1 内存管理策略
命令对象的生命周期管理是关键考量。常见策略包括:
- 预分配池:对于固定大小的命令,使用对象池避免频繁分配
template<typename T> class CommandPool { public: template<typename... Args> T* create(Args&&... args) { if (pool.empty()) { pool.push_back(std::make_unique<T>(std::forward<Args>(args)...)); } auto ptr = pool.back().get(); pool.pop_back(); return ptr; } void reclaim(std::unique_ptr<T> cmd) { pool.push_back(std::move(cmd)); } private: std::vector<std::unique_ptr<T>> pool; };- 小对象优化:利用std::variant和SBO(Small Buffer Optimization)避免堆分配
5.2 多线程考量
当命令需要在多线程环境下执行时:
class ThreadSafeCommandQueue { public: void push(std::unique_ptr<Command> cmd) { std::lock_guard lock(mutex); queue.push(std::move(cmd)); cv.notify_one(); } std::unique_ptr<Command> pop() { std::unique_lock lock(mutex); cv.wait(lock, [this]{ return !queue.empty(); }); auto cmd = std::move(queue.front()); queue.pop(); return cmd; } private: std::queue<std::unique_ptr<Command>> queue; std::mutex mutex; std::condition_variable cv; };5.3 常见陷阱与解决方案
命令状态污染:确保命令对象是可重复使用的,或者在每次执行前重置状态
撤销栈爆炸:对于频繁执行的命令(如鼠标移动),考虑合并为单个复合命令
循环依赖:避免命令对象持有对调用者的引用,导致生命周期问题
类型擦除成本:对于性能敏感场景,考虑使用CRTP模式避免虚函数开销
template<typename Derived> class CommandCRTP { public: void execute() { static_cast<Derived*>(this)->executeImpl(); } }; class ConcreteCommand : public CommandCRTP<ConcreteCommand> { public: void executeImpl() { /* 具体实现 */ } };6. 扩展应用:超越基础命令模式
6.1 复合命令模式
组合多个命令形成宏命令:
class MacroCommand : public Command { public: void add(std::unique_ptr<Command> cmd) { commands.push_back(std::move(cmd)); } Status execute() override { for (auto& cmd : commands) { if (cmd->execute() != Status::Success) { return Status::Failure; } } return Status::Success; } Status undo() override { for (auto it = commands.rbegin(); it != commands.rend(); ++it) { if ((*it)->undo() != Status::Success) { return Status::Failure; } } return Status::Success; } private: std::vector<std::unique_ptr<Command>> commands; };6.2 事务处理系统
将命令模式与数据库事务结合:
class Transaction { public: void add(std::unique_ptr<Command> cmd) { commands.push_back(std::move(cmd)); } bool commit() { for (auto& cmd : commands) { if (cmd->execute() != Status::Success) { rollback(); return false; } } return true; } private: void rollback() { for (auto it = commands.rbegin(); it != commands.rend(); ++it) { (*it)->undo(); } } std::vector<std::unique_ptr<Command>> commands; };6.3 命令模式与事件溯源
将命令执行记录用于系统状态重建:
class EventSourcingSystem { public: void apply(std::unique_ptr<Command> cmd) { if (cmd->execute() == Status::Success) { auto event = cmd->toEvent(); eventLog.push_back(std::move(event)); currentState = applyEvent(currentState, event); } } State rebuildFromScratch() const { State state; for (const auto& event : eventLog) { state = applyEvent(state, event); } return state; } private: std::vector<Event> eventLog; State currentState; };在实现这些扩展模式时,我发现一个关键点:命令对象应该是无副作用的纯函数(除了对接收者的影响)。这意味着命令的执行结果应该只依赖于接收者状态和命令参数,而不依赖于外部状态。这种设计使得命令更容易测试、调试和回放。