1. 命令模式基础概念解析
命令模式是GoF 23种设计模式中行为型模式的一种,它将请求封装成对象,从而允许用户使用不同的请求、队列或日志来参数化其他对象。这种解耦方式在实际工程中非常实用,特别是在需要实现撤销/重做、事务处理等场景时。
我第一次在真实项目中应用命令模式是在开发一个图形编辑器时。当时需要实现一个支持多级撤销的操作历史记录功能,命令模式完美解决了这个问题。通过将每个编辑操作封装成命令对象,我们能够轻松实现操作序列的存储和回放。
命令模式的核心参与者包括:
- Invoker(调用者):触发命令的对象
- Command(命令):声明执行操作的接口
- ConcreteCommand(具体命令):将接收者与动作绑定
- Receiver(接收者):知道如何执行请求的具体对象
- Client(客户端):创建具体命令并设置接收者
2. 命令模式的典型应用场景
2.1 GUI操作与撤销功能
在图形界面应用中,命令模式几乎是实现撤销/重做功能的标准方案。每个用户操作(如添加形状、修改属性)都被封装为一个命令对象,这些对象被存储在历史列表中。当用户触发撤销时,只需调用最近命令的undo()方法。
cpp复制// 示例:图形编辑器的移动命令
class MoveCommand : public Command {
public:
MoveCommand(Shape* shape, const Point& delta)
: shape(shape), delta(delta) {}
void execute() override {
shape->move(delta);
}
void undo() override {
shape->move(Point(-delta.x, -delta.y));
}
private:
Shape* shape;
Point delta;
};
2.2 任务队列与线程池
命令模式非常适合实现异步任务处理系统。我们可以将各种任务封装为命令对象,然后放入队列中由工作线程依次执行。这种设计在游戏开发中特别常见,比如将渲染命令、物理计算等不同任务统一封装。
cpp复制// 示例:线程池任务处理
class ThreadPool {
public:
void addTask(std::unique_ptr<Command> task) {
std::lock_guard<std::mutex> lock(queueMutex);
taskQueue.push(std::move(task));
condition.notify_one();
}
void workerThread() {
while (running) {
std::unique_ptr<Command> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this]{ return !taskQueue.empty() || !running; });
if (!running) break;
task = std::move(taskQueue.front());
taskQueue.pop();
}
task->execute();
}
}
private:
std::queue<std::unique_ptr<Command>> taskQueue;
std::mutex queueMutex;
std::condition_variable condition;
bool running = true;
};
2.3 游戏开发中的输入处理
在游戏引擎中,命令模式常被用来处理玩家输入。不同的按键可以绑定到不同的命令对象,使得按键配置可以在运行时灵活改变。这种设计也便于实现"回放"功能,只需记录执行过的命令序列。
cpp复制// 示例:游戏输入处理
class InputHandler {
public:
void handleInput() {
if (isPressed(BUTTON_X)) buttonX->execute();
if (isPressed(BUTTON_Y)) buttonY->execute();
// ...
}
void bindCommand(Button button, std::unique_ptr<Command> command) {
switch (button) {
case BUTTON_X: buttonX = std::move(command); break;
case BUTTON_Y: buttonY = std::move(command); break;
// ...
}
}
private:
std::unique_ptr<Command> buttonX;
std::unique_ptr<Command> buttonY;
// ...
};
3. C++实现命令模式的最佳实践
3.1 接口设计与内存管理
在C++中实现命令模式时,接口设计需要考虑资源管理和性能问题。我推荐使用智能指针来管理命令对象的生命周期,特别是在需要长期保存命令对象(如实现撤销栈)的场景中。
cpp复制class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
virtual bool isReversible() const { return true; }
};
// 使用示例
std::vector<std::unique_ptr<Command>> commandHistory;
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
if (cmd->isReversible()) {
commandHistory.push_back(std::move(cmd));
}
}
3.2 性能优化技巧
命令对象可能被频繁创建和销毁,特别是在游戏循环等性能敏感场景中。这时可以考虑使用对象池模式来重用命令对象,减少内存分配开销。
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();
*ptr = T(std::forward<Args>(args)...);
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;
};
3.3 现代C++特性应用
C++11及以后版本提供的lambda表达式可以简化命令模式的实现,特别是在只需要简单操作的场景中,可以避免创建单独的Command子类。
cpp复制// 使用lambda创建临时命令
auto cmd = make_command(
[=]() { target->doSomething(params); }, // execute
[=]() { target->undoSomething(params); } // undo
);
// make_command实现示例
template<typename Execute, typename Undo>
auto make_command(Execute&& execute, Undo&& undo) {
class LambdaCommand : public Command {
public:
LambdaCommand(Execute&& e, Undo&& u)
: execute(std::forward<Execute>(e))
, undo(std::forward<Undo>(u)) {}
void execute() override { execute(); }
void undo() override { undo(); }
private:
Execute execute;
Undo undo;
};
return std::make_unique<LambdaCommand>(
std::forward<Execute>(execute),
std::forward<Undo>(undo));
}
4. 命令模式的高级应用与变体
4.1 组合命令(宏命令)
组合命令允许将多个命令组合成一个复合命令,这在实现复杂操作或批量操作时非常有用。我在一个CAD软件项目中就使用这种技术来实现"组合操作"功能。
cpp复制class MacroCommand : public Command {
public:
void addCommand(std::unique_ptr<Command> cmd) {
commands.push_back(std::move(cmd));
}
void execute() override {
for (auto it = commands.begin(); it != commands.end(); ++it) {
(*it)->execute();
}
}
void undo() override {
for (auto it = commands.rbegin(); it != commands.rend(); ++it) {
(*it)->undo();
}
}
private:
std::vector<std::unique_ptr<Command>> commands;
};
4.2 事务处理系统
命令模式可以扩展实现简单的事务处理系统。每个命令除了执行和撤销方法外,还可以加入验证逻辑,只有所有命令都验证通过才会执行。
cpp复制class Transaction {
public:
void addCommand(std::unique_ptr<Command> cmd) {
commands.push_back(std::move(cmd));
}
bool execute() {
// 验证阶段
for (const auto& cmd : commands) {
if (!cmd->validate()) {
return false;
}
}
// 执行阶段
executedCommands.clear();
for (auto& cmd : commands) {
cmd->execute();
executedCommands.push_back(std::move(cmd));
}
commands.clear();
return true;
}
void rollback() {
for (auto it = executedCommands.rbegin(); it != executedCommands.rend(); ++it) {
(*it)->undo();
}
}
private:
std::vector<std::unique_ptr<Command>> commands;
std::vector<std::unique_ptr<Command>> executedCommands;
};
4.3 命令模式的扩展应用
在分布式系统中,命令模式可以扩展为"命令消息",将命令序列化后通过网络传输,在远程节点上执行。我在一个物联网项目中就使用这种技术来实现远程设备控制。
cpp复制class RemoteCommand : public Command {
public:
virtual std::string serialize() const = 0;
static std::unique_ptr<RemoteCommand> deserialize(const std::string& data);
};
class LightOnCommand : public RemoteCommand {
public:
LightOnCommand(int deviceId) : deviceId(deviceId) {}
void execute() override {
// 实际项目中这里会是网络通信代码
std::cout << "Turning on light " << deviceId << std::endl;
}
std::string serialize() const override {
return "light:on:" + std::to_string(deviceId);
}
static std::unique_ptr<LightOnCommand> deserialize(const std::string& data) {
// 解析data字符串...
return std::make_unique<LightOnCommand>(parsedDeviceId);
}
private:
int deviceId;
};
5. 命令模式的优缺点与替代方案
5.1 命令模式的优势
- 解耦调用者与执行者:调用者不需要知道具体的执行细节
- 支持撤销/重做:通过维护命令历史轻松实现
- 支持事务:可以构建原子操作序列
- 灵活性:可以轻松组合、排队、记录命令
- 可扩展性:新命令可以随时添加而不影响现有代码
5.2 命令模式的局限性
- 性能开销:每个操作都需要创建命令对象
- 内存占用:实现撤销功能需要保存所有命令对象
- 复杂性增加:简单操作也需要创建多个类
- 可能导致过度设计:不是所有场景都需要这种灵活性
5.3 替代方案比较
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接调用 | 简单操作 | 直接高效 | 耦合度高 |
| 函数指针/Callback | 简单回调 | 轻量级 | 功能有限 |
| 策略模式 | 算法替换 | 灵活 | 不支持撤销 |
| 观察者模式 | 事件通知 | 松耦合 | 不适合命令控制 |
在实际项目中,我通常会根据以下标准决定是否使用命令模式:
- 是否需要撤销/重做功能
- 操作是否需要排队或记录
- 调用者与执行者是否需要完全解耦
- 操作是否足够复杂,值得引入额外抽象层
6. 实战案例:文本编辑器的命令模式实现
让我们通过一个完整的文本编辑器示例来展示命令模式的实战应用。这个编辑器将支持基本的文本编辑操作,并实现撤销/重做功能。
6.1 基础架构设计
cpp复制// 接收者 - 文档类
class Document {
public:
void insert(size_t pos, const std::string& text) {
content.insert(pos, text);
}
void erase(size_t pos, size_t len) {
content.erase(pos, len);
}
const std::string& getContent() const { return content; }
private:
std::string content;
};
// 命令基类
class EditorCommand {
public:
virtual ~EditorCommand() = default;
virtual void execute() = 0;
virtual void undo() = 0;
virtual std::string getDescription() const = 0;
};
// 具体命令 - 插入文本
class InsertCommand : public EditorCommand {
public:
InsertCommand(Document& doc, size_t pos, const std::string& text)
: doc(doc), pos(pos), text(text) {}
void execute() override { doc.insert(pos, text); }
void undo() override { doc.erase(pos, text.length()); }
std::string getDescription() const override {
return "Insert \"" + text + "\" at position " + std::to_string(pos);
}
private:
Document& doc;
size_t pos;
std::string text;
};
// 具体命令 - 删除文本
class EraseCommand : public EditorCommand {
public:
EraseCommand(Document& doc, size_t pos, size_t len)
: doc(doc), pos(pos), len(len), deletedText(doc.getContent().substr(pos, len)) {}
void execute() override { doc.erase(pos, len); }
void undo() override { doc.insert(pos, deletedText); }
std::string getDescription() const override {
return "Erase " + std::to_string(len) + " chars at position " + std::to_string(pos);
}
private:
Document& doc;
size_t pos;
size_t len;
std::string deletedText;
};
6.2 撤销系统实现
cpp复制class Editor {
public:
void executeCommand(std::unique_ptr<EditorCommand> cmd) {
cmd->execute();
undoStack.push(std::move(cmd));
redoStack = std::stack<std::unique_ptr<EditorCommand>>(); // 清空重做栈
}
bool undo() {
if (undoStack.empty()) return false;
auto cmd = std::move(undoStack.top());
undoStack.pop();
cmd->undo();
redoStack.push(std::move(cmd));
return true;
}
bool redo() {
if (redoStack.empty()) return false;
auto cmd = std::move(redoStack.top());
redoStack.pop();
cmd->execute();
undoStack.push(std::move(cmd));
return true;
}
void printHistory() const {
std::cout << "Undo stack:\n";
printStack(undoStack);
std::cout << "\nRedo stack:\n";
printStack(redoStack);
}
private:
Document doc;
std::stack<std::unique_ptr<EditorCommand>> undoStack;
std::stack<std::unique_ptr<EditorCommand>> redoStack;
static void printStack(const std::stack<std::unique_ptr<EditorCommand>>& stack) {
auto temp = stack;
std::vector<std::string> items;
while (!temp.empty()) {
items.push_back(temp.top()->getDescription());
temp.pop();
}
for (auto it = items.rbegin(); it != items.rend(); ++it) {
std::cout << " - " << *it << "\n";
}
}
};
6.3 使用示例
cpp复制int main() {
Editor editor;
// 执行一系列编辑操作
editor.executeCommand(std::make_unique<InsertCommand>(editor.getDocument(), 0, "Hello"));
editor.executeCommand(std::make_unique<InsertCommand>(editor.getDocument(), 5, " World"));
editor.executeCommand(std::make_unique<EraseCommand>(editor.getDocument(), 5, 1));
std::cout << "Current document: " << editor.getDocument().getContent() << "\n\n";
// 查看操作历史
editor.printHistory();
// 执行撤销
editor.undo();
std::cout << "\nAfter undo: " << editor.getDocument().getContent() << "\n";
// 执行重做
editor.redo();
std::cout << "After redo: " << editor.getDocument().getContent() << "\n";
return 0;
}
这个案例展示了命令模式在实现编辑器撤销系统时的典型应用。通过将每个编辑操作封装为命令对象,我们可以轻松实现多级撤销/重做功能,同时保持代码的清晰和可维护性。
7. 性能考量与优化策略
在实际项目中应用命令模式时,性能是需要特别关注的问题。以下是我在多年实践中总结的一些优化经验:
7.1 对象创建优化
命令对象的频繁创建和销毁可能导致性能瓶颈。对于性能敏感的场景,可以考虑以下优化:
- 对象池技术:预先分配一批命令对象,循环使用
- 轻量级命令:将大对象存储在外部,命令只保存引用
- 命令合并:将多个连续小操作合并为一个复合命令
cpp复制// 对象池实现示例
class CommandPool {
public:
template<typename T, typename... Args>
std::unique_ptr<T, std::function<void(T*)>> create(Args&&... args) {
if (freeList.empty()) {
return std::unique_ptr<T, std::function<void(T*)>>(
new T(std::forward<Args>(args)...),
[this](T* p) { freeList.push_back(p); });
}
T* ptr = freeList.back();
freeList.pop_back();
*ptr = T(std::forward<Args>(args)...);
return std::unique_ptr<T, std::function<void(T*)>>(
ptr,
[this](T* p) { freeList.push_back(p); });
}
private:
std::vector<Command*> freeList;
};
7.2 内存管理策略
长期维护命令历史(如实现无限撤销)可能导致内存占用过高。可以考虑:
- 限制历史深度:只保留最近的N个命令
- 检查点机制:定期保存完整状态,只记录后续增量命令
- 懒加载:将不常用的命令序列化到磁盘
cpp复制// 有限历史实现
class LimitedHistory {
public:
void push(std::unique_ptr<Command> cmd) {
if (history.size() >= maxSize) {
history.pop_front();
}
history.push_back(std::move(cmd));
}
std::unique_ptr<Command> pop() {
if (history.empty()) return nullptr;
auto cmd = std::move(history.back());
history.pop_back();
return cmd;
}
private:
std::deque<std::unique_ptr<Command>> history;
size_t maxSize = 100; // 最多保存100个命令
};
7.3 多线程环境下的命令模式
在多线程应用中使用命令模式需要特别注意线程安全问题:
- 命令不可变性:执行后命令状态不应改变
- 线程安全队列:使用锁或原子操作保护命令队列
- 执行隔离:确保命令执行不影响共享状态
cpp复制// 线程安全命令队列
class ConcurrentCommandQueue {
public:
void push(std::unique_ptr<Command> cmd) {
std::lock_guard<std::mutex> lock(mutex);
queue.push(std::move(cmd));
cv.notify_one();
}
std::unique_ptr<Command> pop() {
std::unique_lock<std::mutex> 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;
};
在实际项目中,我通常会根据具体场景混合使用这些优化策略。例如,在一个图形编辑器中,我们可能同时使用对象池来管理绘图命令,限制撤销历史深度,并使用线程安全队列来处理后台渲染命令。