1. 命令模式在C++中的核心价值
命令模式(Command Pattern)是我在大型C++项目中频繁使用的设计模式之一。它的本质是将请求封装为对象,使你可以参数化客户端请求,将请求排队或记录请求日志,以及支持可撤销的操作。这种解耦方式特别适合需要实现"操作-执行"分离的场景。
在游戏开发中,我们常用命令模式处理用户输入。比如当玩家按下WASD键时,不是直接调用角色移动函数,而是创建对应的移动命令对象。这样做的好处是:
- 可以轻松实现撤销/重做功能
- 命令可以序列化用于网络传输
- 支持宏命令(组合多个命令)
- 方便实现命令队列和延迟执行
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 命令模式的标准实现
2.1 基础类结构
标准的命令模式包含以下核心组件:
cpp复制// 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// 具体命令
class MoveCommand : public Command {
public:
MoveCommand(Character& receiver, int x, int y)
: receiver_(receiver), x_(x), y_(y), prevX_(0), prevY_(0) {}
void execute() override {
prevX_ = receiver_.getX();
prevY_ = receiver_.getY();
receiver_.moveTo(x_, y_);
}
void undo() override {
receiver_.moveTo(prevX_, prevY_);
}
private:
Character& receiver_;
int x_, y_;
int prevX_, prevY_;
};
// 调用者
class InputHandler {
public:
void setCommand(std::unique_ptr<Command> cmd) {
command_ = std::move(cmd);
}
void pressButton() {
if(command_) {
command_->execute();
history_.push(command_->clone());
}
}
private:
std::unique_ptr<Command> command_;
std::stack<std::unique_ptr<Command>> history_;
};
2.2 实现要点解析
-
命令对象应保持轻量:命令对象通常会被频繁创建和销毁,建议避免在命令中存储大量数据。对于需要复杂参数的命令,可以考虑使用参数对象。
-
支持撤销操作:undo()实现的关键是在execute()时保存对象的前置状态。对于不可逆操作(如删除文件),可以采用补偿操作的方式实现撤销。
-
命令克隆:要实现完善的命令历史,命令类需要支持克隆操作。可以使用原型模式或提供clone()接口。
3. 高级应用场景
3.1 宏命令实现
宏命令是命令模式的进阶用法,允许将多个命令组合成一个命令:
cpp复制class MacroCommand : public Command {
public:
void addCommand(std::unique_ptr<Command> 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();
}
}
private:
std::vector<std::unique_ptr<Command>> commands_;
};
3.2 异步命令执行
在现代C++中,我们可以结合std::async实现异步命令:
cpp复制class AsyncCommand : public Command {
public:
void execute() override {
future_ = std::async(std::launch::async, [this]{
// 长时间运行的操作
doWork();
});
}
void undo() override {
if(future_.valid()) {
future_.wait();
undoWork();
}
}
private:
std::future<void> future_;
};
4. 性能优化实践
4.1 对象池优化
频繁创建命令对象可能导致内存碎片。我们可以使用对象池模式优化:
cpp复制class CommandPool {
public:
template<typename T, typename... Args>
std::unique_ptr<T, std::function<void(T*)>> acquire(Args&&... args) {
if(pool_.empty()) {
return std::unique_ptr<T, std::function<void(T*)>>(
new T(std::forward<Args>(args)...),
[this](T* p) { pool_.push_back(std::unique_ptr<T>(p)); });
}
auto ptr = std::move(pool_.back());
pool_.pop_back();
return std::unique_ptr<T, std::function<void(T*)>>(
ptr.release(),
[this](T* p) { pool_.push_back(std::unique_ptr<T>(p)); });
}
private:
std::vector<std::unique_ptr<Command>> pool_;
};
4.2 命令批处理
对于需要处理大量相似命令的场景,可以合并命令:
cpp复制class BatchMoveCommand : public Command {
public:
void addMove(Character& c, int x, int y) {
moves_.emplace_back(&c, x, y, c.getX(), c.getY());
}
void execute() override {
for(auto& m : moves_) {
m.character->moveTo(m.x, m.y);
}
}
void undo() override {
for(auto& m : moves_) {
m.character->moveTo(m.prevX, m.prevY);
}
}
private:
struct Move {
Character* character;
int x, y;
int prevX, prevY;
};
std::vector<Move> moves_;
};
5. 实际项目中的经验教训
5.1 线程安全注意事项
在多线程环境下使用命令模式时:
- 确保命令对象是线程安全的或无状态的
- 命令队列需要使用互斥锁保护
- 避免在命令中持有共享资源的裸指针
cpp复制class ThreadSafeCommandQueue {
public:
void push(std::unique_ptr<Command> cmd) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::move(cmd));
}
std::unique_ptr<Command> pop() {
std::lock_guard<std::mutex> lock(mutex_);
if(queue_.empty()) return nullptr;
auto cmd = std::move(queue_.front());
queue_.pop();
return cmd;
}
private:
std::queue<std::unique_ptr<Command>> queue_;
std::mutex mutex_;
};
5.2 内存管理陷阱
- 避免在命令中持有裸指针,改用智能指针
- 注意命令对象生命周期可能比调用者长的情况
- 对于需要共享的数据,考虑使用shared_ptr
cpp复制class SafeResourceCommand : public Command {
public:
SafeResourceCommand(std::shared_ptr<Resource> res)
: resource_(res) {}
// ... execute和undo实现
private:
std::shared_ptr<Resource> resource_;
};
6. 现代C++特性应用
6.1 使用lambda简化命令
C++11后,可以使用lambda快速创建命令对象:
cpp复制auto cmd = std::make_unique<LambdaCommand>(
[&obj](){ obj.doSomething(); }, // execute
[&obj](){ obj.undoSomething(); } // undo
);
LambdaCommand的实现:
cpp复制class LambdaCommand : public Command {
public:
using Func = std::function<void()>;
LambdaCommand(Func execute, Func undo)
: execute_(std::move(execute))
, undo_(std::move(undo)) {}
void execute() override { execute_(); }
void undo() override { undo_(); }
private:
Func execute_;
Func undo_;
};
6.2 可变参数模板命令
利用模板实现灵活的命令参数:
cpp复制template<typename Receiver, typename... Args>
class GenericCommand : public Command {
public:
using Action = void (Receiver::*)(Args...);
using UndoAction = void (Receiver::*)(Args...);
GenericCommand(Receiver& r, Action a, UndoAction ua, Args... args)
: receiver_(r), action_(a), undoAction_(ua),
args_(std::forward<Args>(args)...) {}
void execute() override {
std::apply([this](auto&&... args) {
(receiver_.*action_)(std::forward<decltype(args)>(args)...);
}, args_);
}
void undo() override {
std::apply([this](auto&&... args) {
(receiver_.*undoAction_)(std::forward<decltype(args)>(args)...);
}, args_);
}
private:
Receiver& receiver_;
Action action_;
UndoAction undoAction_;
std::tuple<Args...> args_;
};
7. 测试策略
7.1 单元测试要点
- 测试命令执行后的状态变化
- 测试undo操作能否正确恢复状态
- 测试命令组合的正确性
- 测试异常安全性
cpp复制TEST(CommandTest, MoveCommandExecution) {
Character player;
MoveCommand cmd(player, 10, 20);
cmd.execute();
EXPECT_EQ(player.getX(), 10);
EXPECT_EQ(player.getY(), 20);
cmd.undo();
EXPECT_EQ(player.getX(), 0);
EXPECT_EQ(player.getY(), 0);
}
7.2 性能测试建议
- 测量命令对象的创建/销毁开销
- 测试命令队列的处理能力
- 评估内存使用情况
- 检查多线程竞争条件下的表现
8. 与其他模式的结合
8.1 命令+备忘录模式
结合备忘录模式可以实现更强大的撤销功能:
cpp复制class MementoCommand : public Command {
public:
void execute() override {
memento_ = receiver_.createMemento();
receiver_.doAction();
}
void undo() override {
receiver_.restoreFromMemento(*memento_);
}
private:
Receiver receiver_;
std::unique_ptr<Memento> memento_;
};
8.2 命令+责任链模式
将命令组织成责任链,实现复杂操作流程:
cpp复制class ChainableCommand : public Command {
public:
void setNext(std::unique_ptr<ChainableCommand> next) {
next_ = std::move(next);
}
void execute() override {
if(!onExecute()) return;
if(next_) {
next_->execute();
}
}
void undo() override {
if(next_) {
next_->undo();
}
onUndo();
}
protected:
virtual bool onExecute() = 0;
virtual void onUndo() = 0;
private:
std::unique_ptr<ChainableCommand> next_;
};
9. 典型应用案例
9.1 游戏开发中的应用
在游戏引擎中,命令模式常用于:
- 用户输入处理
- 游戏回放系统
- 网络同步
- AI行为队列
cpp复制// 游戏中的典型应用
class Game {
public:
void handleInput(InputEvent event) {
auto cmd = inputHandler_.handleInput(event);
if(cmd) {
cmd->execute();
commandHistory_.push(std::move(cmd));
}
}
void undoLastCommand() {
if(!commandHistory_.empty()) {
commandHistory_.top()->undo();
commandHistory_.pop();
}
}
private:
InputHandler inputHandler_;
std::stack<std::unique_ptr<Command>> commandHistory_;
};
9.2 GUI框架中的应用
在GUI框架中,命令模式用于:
- 菜单项操作
- 按钮点击事件
- 文本编辑操作
- 事务管理
cpp复制// 文本编辑器中的撤销/重做实现
class TextEditor {
public:
void insertText(size_t pos, const std::string& text) {
auto cmd = std::make_unique<InsertCommand>(*this, pos, text);
cmd->execute();
undoStack_.push(std::move(cmd));
redoStack_ = std::stack<std::unique_ptr<Command>>(); // 清空重做栈
}
void undo() {
if(!undoStack_.empty()) {
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
cmd->undo();
redoStack_.push(std::move(cmd));
}
}
void redo() {
if(!redoStack_.empty()) {
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
cmd->execute();
undoStack_.push(std::move(cmd));
}
}
private:
std::string content_;
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
10. 设计权衡与替代方案
10.1 何时不使用命令模式
- 当操作极其简单且不需要撤销/重做功能时
- 性能敏感场景中命令对象创建开销过大时
- 当操作逻辑很少变化时
10.2 替代方案比较
- 直接函数调用:简单但缺乏灵活性
- 策略模式:关注算法替换而非操作封装
- 观察者模式:适合事件通知而非操作封装
在实际项目中,我经常将命令模式与这些模式结合使用,根据具体需求选择最合适的方案。
