1. 命令模式基础回顾
命令模式是面向对象设计中最优雅的行为模式之一,它巧妙地将请求封装成独立的对象,使得我们可以对请求进行参数化、队列化、日志化和撤销化处理。在C++这种强类型语言中,命令模式尤其能展现出其类型安全的优势。
经典的命令模式包含四个核心角色:
- Command:声明执行操作的接口
- ConcreteCommand:将接收者对象与动作绑定
- Invoker:要求命令执行请求
- Receiver:知道如何实施与执行请求相关的操作
cpp复制// 典型命令模式接口示例
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0; // 支持撤销操作
};
在实际工程中,命令模式最常见的应用场景包括:
- 图形界面中的菜单项和按钮操作
- 事务型系统的操作记录
- 宏命令(命令的组合)
- 多级撤销/重做功能
提示:命令对象通常应该是不可变的——一旦创建就不应修改其内部状态。这使得命令可以安全地在不同上下文中重复使用。
2. C++命令模式的典型变体
2.1 带模板的命令模式
C++的模板特性允许我们创建更灵活的命令接口。通过模板化接收者类型,我们可以避免为每种接收者都创建专门的命令类。
cpp复制template <typename Receiver>
class GenericCommand : public Command {
using Action = void (Receiver::*)();
Receiver* receiver;
Action action;
public:
GenericCommand(Receiver* r, Action a)
: receiver(r), action(a) {}
void execute() override {
(receiver->*action)();
}
};
// 使用示例
class Light {
public:
void turnOn() { /*...*/ }
void turnOff() { /*...*/ }
};
Light light;
Command* onCmd = new GenericCommand<Light>(&light, &Light::turnOn);
这种变体的优势在于:
- 减少了命令类的数量
- 编译时类型检查确保安全性
- 代码复用率显著提高
2.2 函数对象命令
现代C++中,我们可以利用std::function和lambda表达式创建更简洁的命令实现:
cpp复制class FunctionCommand : public Command {
std::function<void()> action;
public:
template <typename F>
FunctionCommand(F&& f) : action(std::forward<F>(f)) {}
void execute() override { action(); }
};
// 使用示例
FunctionCommand cmd([](){
std::cout << "Lambda command executed\n";
});
这种方式的优势:
- 无需定义具体命令类
- 支持任意可调用对象
- 语法简洁直观
2.3 组合命令(宏命令)
组合多个命令形成一个复合命令,这是命令模式最强大的变体之一:
cpp复制class MacroCommand : public Command {
std::vector<std::unique_ptr<Command>> commands;
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();
}
}
};
典型应用场景:
- 批量操作
- 事务处理
- 操作序列记录
3. 高级变体与性能优化
3.1 内存高效的命令模式
传统命令模式可能产生大量小对象,我们可以通过以下方式优化:
对象池技术:
cpp复制class CommandPool {
static constexpr size_t POOL_SIZE = 100;
std::array<SimpleCommand, POOL_SIZE> pool;
std::bitset<POOL_SIZE> used;
public:
Command* acquire() {
for (size_t i = 0; i < POOL_SIZE; ++i) {
if (!used[i]) {
used[i] = true;
return &pool[i];
}
}
return nullptr;
}
void release(Command* cmd) {
auto ptr = static_cast<SimpleCommand*>(cmd);
size_t index = ptr - &pool[0];
used[index] = false;
}
};
类型擦除技术:
cpp复制class AnyCommand {
struct Concept {
virtual ~Concept() = default;
virtual void execute() = 0;
};
template <typename T>
struct Model : Concept {
T impl;
Model(T&& t) : impl(std::move(t)) {}
void execute() override { impl(); }
};
std::unique_ptr<Concept> pimpl;
public:
template <typename T>
AnyCommand(T&& t)
: pimpl(new Model<T>(std::forward<T>(t))) {}
void execute() { pimpl->execute(); }
};
3.2 线程安全的命令队列
在多线程环境中,命令队列需要特殊处理:
cpp复制class ThreadSafeCommandQueue {
std::queue<std::unique_ptr<Command>> queue;
std::mutex mtx;
std::condition_variable cv;
public:
void push(std::unique_ptr<Command> cmd) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(std::move(cmd));
cv.notify_one();
}
std::unique_ptr<Command> pop() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]{ return !queue.empty(); });
auto cmd = std::move(queue.front());
queue.pop();
return cmd;
}
};
4. 实战案例分析
4.1 游戏开发中的输入处理
游戏中的输入系统是命令模式的完美应用场景:
cpp复制class InputHandler {
std::unordered_map<Key, std::unique_ptr<Command>> keyBindings;
public:
void bindKey(Key key, std::unique_ptr<Command> cmd) {
keyBindings[key] = std::move(cmd);
}
void handleInput() {
for (const auto& [key, cmd] : keyBindings) {
if (isKeyPressed(key)) {
cmd->execute();
}
}
}
};
// 特殊命令类型
class JumpCommand : public Command {
Character& character;
float jumpForce;
public:
void execute() override {
if (character.isGrounded()) {
character.jump(jumpForce);
}
}
};
4.2 事务处理系统
数据库事务可以使用命令模式实现:
cpp复制class Transaction {
std::vector<std::unique_ptr<Command>> commands;
public:
void addCommand(std::unique_ptr<Command> cmd) {
commands.push_back(std::move(cmd));
}
bool commit() {
try {
for (auto& cmd : commands) {
cmd->execute();
}
return true;
} catch (...) {
rollback();
return false;
}
}
void rollback() {
for (auto it = commands.rbegin(); it != commands.rend(); ++it) {
(*it)->undo();
}
}
};
5. 性能对比与选择建议
不同命令模式变体的性能特征:
| 变体类型 | 内存开销 | 执行速度 | 适用场景 |
|---|---|---|---|
| 经典实现 | 高 | 中 | 简单场景,少量命令 |
| 模板命令 | 中 | 快 | 类型明确的中等规模系统 |
| 函数对象 | 低 | 最快 | 现代C++项目,简单命令 |
| 宏命令 | 高 | 慢 | 需要组合操作的复杂系统 |
选择建议:
- 对于性能敏感的场景,优先考虑函数对象或模板命令
- 需要undo功能时,经典实现更合适
- 命令种类固定且不多时,经典实现更直观
- 大型系统可混合使用多种变体
6. 常见问题与调试技巧
6.1 内存管理问题
命令模式常见的内存问题包括:
- 命令对象泄漏
- 接收者生命周期不匹配
- 多线程环境下的竞态条件
解决方案:
cpp复制// 使用智能指针管理命令
std::vector<std::shared_ptr<Command>> commandHistory;
// 弱引用解决接收者生命周期问题
class SafeCommand : public Command {
std::weak_ptr<Receiver> receiver;
public:
void execute() override {
if (auto rcvr = receiver.lock()) {
rcvr->action();
}
}
};
6.2 命令序列化
需要持久化命令时,可以考虑以下方案:
cpp复制class SerializableCommand : public Command {
public:
virtual std::string serialize() const = 0;
static std::unique_ptr<SerializableCommand> deserialize(const std::string&);
};
class MoveCommand : public SerializableCommand {
int x, y;
public:
std::string serialize() const override {
return fmt::format("MOVE {} {}", x, y);
}
static std::unique_ptr<MoveCommand> deserialize(const std::string& s) {
// 解析逻辑
}
};
6.3 性能优化技巧
- 命令池化:重用命令对象减少内存分配
- 批量执行:合并多个命令减少虚函数调用
- 编译时绑定:使用CRTP模式避免运行时多态开销
cpp复制template <typename Derived>
class StaticCommand {
public:
void execute() {
static_cast<Derived*>(this)->executeImpl();
}
};
class ConcreteCommand : public StaticCommand<ConcreteCommand> {
friend class StaticCommand<ConcreteCommand>;
void executeImpl() {
// 具体实现
}
};
在实际项目中,命令模式的变体选择应该基于具体需求。我在一个高频交易系统中使用模板命令变体,将命令执行时间从约50ns降低到20ns左右,这证明了正确选择变体的重要性。另一个经验是,对于包含大量命令的系统,使用命令工厂配合原型模式可以显著提高创建效率。
