1. 命令模式的核心价值与应用场景
在C++开发中,命令模式(Command Pattern)是一种行为型设计模式,它将请求封装为独立的对象,使你可以参数化客户端请求、将请求排队或记录请求日志,以及支持可撤销的操作。这种解耦方式在实际工程中有着广泛的应用价值。
我曾在开发一个图形编辑器时深刻体会到命令模式的威力。当时需要实现一个支持多级撤销/重做的绘图系统,命令模式完美解决了这个问题。每个绘图操作(如画线、填充、移动)都被封装成命令对象,执行后压入历史栈。当用户点击撤销时,只需从栈顶取出命令对象执行其undo()方法即可。
命令模式特别适合以下场景:
- 需要将操作请求者与执行者解耦时
- 需要支持事务、撤销/重做功能时
- 需要将操作延迟执行或放入队列时
- 需要支持宏命令(组合多个命令)时
提示:在游戏开发中,命令模式常用于实现输入处理系统。每个按键绑定到一个命令对象,使得按键配置可以动态改变,也为回放功能提供了基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++命令模式的经典实现
2.1 基础类结构设计
标准的命令模式包含以下几个关键组件:
- Command(抽象命令类):声明执行操作的接口
- ConcreteCommand(具体命令):实现Execute方法,调用接收者的操作
- Receiver(接收者):知道如何执行请求的具体操作
- Invoker(调用者):要求命令执行请求
- Client(客户端):创建具体命令并设置接收者
以下是一个最简实现示例:
cpp复制// 接收者 - 知道如何执行操作
class Receiver {
public:
void Action() {
cout << "Receiver执行操作" << endl;
}
};
// 抽象命令接口
class Command {
public:
virtual ~Command() {}
virtual void Execute() = 0;
};
// 具体命令
class ConcreteCommand : public Command {
public:
ConcreteCommand(Receiver* receiver) : receiver_(receiver) {}
void Execute() override {
receiver_->Action();
}
private:
Receiver* receiver_;
};
// 调用者
class Invoker {
public:
void SetCommand(Command* command) {
command_ = command;
}
void ExecuteCommand() {
command_->Execute();
}
private:
Command* command_;
};
2.2 实际使用示例
cpp复制int main() {
Receiver receiver;
Command* command = new ConcreteCommand(&receiver);
Invoker invoker;
invoker.SetCommand(command);
invoker.ExecuteCommand(); // 输出:Receiver执行操作
delete command;
return 0;
}
这种基础实现虽然简单,但已经展现了命令模式的核心思想:将请求封装为对象,使得可以用不同的请求对客户进行参数化。
3. 高级应用:支持撤销与重做
命令模式真正的威力在于支持撤销操作。我们需要扩展Command接口,添加Undo方法,并维护一个命令历史栈。
3.1 可撤销命令的实现
cpp复制class UndoableCommand : public Command {
public:
virtual void Undo() = 0;
};
class DrawCommand : public UndoableCommand {
public:
DrawCommand(Canvas* canvas, const Point& from, const Point& to)
: canvas_(canvas), from_(from), to_(to) {}
void Execute() override {
canvas_->DrawLine(from_, to_);
}
void Undo() override {
canvas_->EraseLine(from_, to_);
}
private:
Canvas* canvas_;
Point from_;
Point to_;
};
3.2 命令历史管理
cpp复制class CommandHistory {
public:
void Push(UndoableCommand* cmd) {
history_.push(cmd);
}
UndoableCommand* Pop() {
if (history_.empty()) return nullptr;
auto cmd = history_.top();
history_.pop();
return cmd;
}
private:
stack<UndoableCommand*> history_;
};
// 使用示例
CommandHistory history;
Canvas canvas;
// 执行绘制命令
auto cmd1 = new DrawCommand(&canvas, Point(0,0), Point(10,10));
cmd1->Execute();
history.Push(cmd1);
// 撤销上一步
auto lastCmd = history.Pop();
if (lastCmd) {
lastCmd->Undo();
delete lastCmd;
}
这种实现方式支持无限级撤销,只需在Undo()方法中正确实现反向操作即可。在实际项目中,还需要考虑内存管理、命令合并等高级功能。
4. 性能优化与工程实践
4.1 命令对象池化
频繁创建销毁命令对象可能导致性能问题,特别是在需要处理大量快速操作的场景(如实时图形编辑)。我们可以使用对象池技术优化:
cpp复制class CommandPool {
public:
template<typename T, typename... Args>
T* Acquire(Args&&... args) {
if (free_list_.empty()) {
return new T(std::forward<Args>(args)...);
}
auto cmd = static_cast<T*>(free_list_.top());
free_list_.pop();
new (cmd) T(std::forward<Args>(args)...); // 原地构造
return cmd;
}
template<typename T>
void Release(T* cmd) {
cmd->~T(); // 显式析构
free_list_.push(cmd);
}
private:
stack<Command*> free_list_;
};
4.2 宏命令的实现
宏命令(Macro Command)是命令模式的扩展,它允许将多个命令组合成一个命令:
cpp复制class MacroCommand : public Command {
public:
void Add(Command* cmd) {
commands_.push_back(cmd);
}
void Execute() override {
for (auto cmd : commands_) {
cmd->Execute();
}
}
~MacroCommand() {
for (auto cmd : commands_) {
delete cmd;
}
}
private:
vector<Command*> commands_;
};
// 使用示例
MacroCommand macro;
macro.Add(new ConcreteCommand(&receiver1));
macro.Add(new ConcreteCommand(&receiver2));
macro.Execute();
宏命令在批处理操作中非常有用,比如实现"一键优化"功能,或者录制和回放用户操作序列。
5. 现代C++的改进实现
5.1 使用智能指针管理命令生命周期
原始指针管理容易导致内存泄漏,现代C++可以使用unique_ptr:
cpp复制class Invoker {
public:
void SetCommand(unique_ptr<Command> command) {
command_ = move(command);
}
void ExecuteCommand() {
if (command_) {
command_->Execute();
}
}
private:
unique_ptr<Command> command_;
};
// 使用示例
auto invoker = Invoker();
invoker.SetCommand(make_unique<ConcreteCommand>(&receiver));
invoker.ExecuteCommand();
5.2 使用lambda表达式简化命令创建
对于简单命令,可以使用std::function避免创建具体命令类:
cpp复制class FunctionCommand : public Command {
public:
FunctionCommand(function<void()> f) : f_(f) {}
void Execute() override {
f_();
}
private:
function<void()> f_;
};
// 使用示例
auto cmd = make_unique<FunctionCommand>([]() {
cout << "Lambda命令执行" << endl;
});
invoker.SetCommand(move(cmd));
invoker.ExecuteCommand();
这种方式大大简化了简单命令的创建,但牺牲了部分类型安全性和可扩展性。
5.3 使用可变模板参数实现通用命令
C++11的可变模板参数允许我们创建更灵活的命令:
cpp复制template<typename R, typename... Args>
class GenericCommand {
public:
using FuncType = function<R(Args...)>;
GenericCommand(FuncType f) : f_(f) {}
R Execute(Args... args) {
return f_(forward<Args>(args)...);
}
private:
FuncType f_;
};
// 使用示例
GenericCommand<int, int, int> addCmd([](int a, int b) {
return a + b;
});
int result = addCmd.Execute(3, 4); // 返回7
这种实现方式提供了极大的灵活性,但需要权衡类型安全和代码清晰度。
6. 实际项目中的经验教训
6.1 命令粒度的选择
命令的粒度设计是实际项目中最容易出错的地方。过细的粒度会导致命令爆炸,过粗的粒度又会影响撤销/重做的精确性。根据我的经验:
- 对于图形编辑器,每个原子操作(如画一笔、移动一个元素)应该是一个独立命令
- 对于文本编辑器,通常以单词或行为单位更合适
- 对于游戏,可以将连续输入(如长按按键)合并为一个命令
6.2 序列化与持久化
要实现保存/加载功能,命令需要支持序列化。一种常见做法是为命令添加Serialize方法:
cpp复制class SerializableCommand : public Command {
public:
virtual string Serialize() const = 0;
virtual void Deserialize(const string& data) = 0;
};
class MoveCommand : public SerializableCommand {
public:
MoveCommand(Shape* shape, int dx, int dy)
: shape_(shape), dx_(dx), dy_(dy) {}
string Serialize() const override {
ostringstream oss;
oss << "MOVE " << shape_->GetId() << " " << dx_ << " " << dy_;
return oss.str();
}
void Deserialize(const string& data) override {
istringstream iss(data);
string cmd;
int id, dx, dy;
iss >> cmd >> id >> dx >> dy;
shape_ = FindShapeById(id);
dx_ = dx;
dy_ = dy;
}
void Execute() override { shape_->Move(dx_, dy_); }
void Undo() override { shape_->Move(-dx_, -dy_); }
private:
Shape* shape_;
int dx_, dy_;
};
6.3 多线程环境下的命令模式
在多线程环境中使用命令模式需要特别注意线程安全问题:
- 命令对象应该是线程不可变的(immutable),所有状态在构造时确定
- 命令执行应该是无副作用的,或者副作用被严格控制
- 命令队列需要适当的同步机制
cpp复制class ThreadSafeInvoker {
public:
void PostCommand(unique_ptr<Command> cmd) {
lock_guard<mutex> lock(mutex_);
queue_.push(move(cmd));
cond_.notify_one();
}
unique_ptr<Command> GetCommand() {
unique_lock<mutex> lock(mutex_);
cond_.wait(lock, [this] { return !queue_.empty(); });
auto cmd = move(queue_.front());
queue_.pop();
return cmd;
}
private:
queue<unique_ptr<Command>> queue_;
mutex mutex_;
condition_variable cond_;
};
7. 命令模式与其他模式的结合
7.1 命令模式与备忘录模式
备忘录模式(Memento)可以辅助命令模式实现更复杂的撤销机制,特别是当命令的撤销需要恢复对象的大量状态时:
cpp复制// 备忘录类
class CanvasMemento {
friend class Canvas;
CanvasMemento(const Canvas& canvas) : state_(canvas.SaveState()) {}
string GetState() const { return state_; }
private:
string state_;
};
// 在Canvas类中添加
class Canvas {
public:
unique_ptr<CanvasMemento> CreateMemento() {
return make_unique<CanvasMemento>(*this);
}
void RestoreFromMemento(const CanvasMemento* memento) {
LoadState(memento->GetState());
}
};
// 修改后的命令
class ComplexDrawCommand : public UndoableCommand {
public:
ComplexDrawCommand(Canvas* canvas) : canvas_(canvas) {
memento_ = canvas->CreateMemento();
}
void Execute() override { /* 复杂绘制逻辑 */ }
void Undo() override {
canvas_->RestoreFromMemento(memento_.get());
}
private:
Canvas* canvas_;
unique_ptr<CanvasMemento> memento_;
};
7.2 命令模式与组合模式
组合模式(Composite)可以让我们创建宏命令,将多个命令组合成一个命令树:
cpp复制class CompositeCommand : public Command {
public:
void Add(unique_ptr<Command> cmd) {
children_.push_back(move(cmd));
}
void Execute() override {
for (auto& cmd : children_) {
cmd->Execute();
}
}
private:
vector<unique_ptr<Command>> children_;
};
这种组合方式在实现复杂操作时非常有用,比如"全选并删除"这样的组合操作。
7.3 命令模式与观察者模式
观察者模式(Observer)可以用于通知命令的执行状态:
cpp复制class CommandNotifier {
public:
virtual ~CommandNotifier() = default;
virtual void OnCommandExecuted(const Command& cmd) = 0;
virtual void OnCommandUndone(const Command& cmd) = 0;
};
class ObservableCommand : public UndoableCommand {
public:
void AddObserver(CommandNotifier* observer) {
observers_.push_back(observer);
}
void Execute() override {
// ...执行逻辑
NotifyExecuted();
}
void Undo() override {
// ...撤销逻辑
NotifyUndone();
}
private:
void NotifyExecuted() {
for (auto obs : observers_) {
obs->OnCommandExecuted(*this);
}
}
void NotifyUndone() {
for (auto obs : observers_) {
obs->OnCommandUndone(*this);
}
}
vector<CommandNotifier*> observers_;
};
这种结合在需要实现进度通知或日志记录的场景下特别有用。
