1. 命令模式的核心概念与应用场景
命令模式(Command Pattern)是面向对象设计中最经典的行为型模式之一,它通过将请求封装为独立的对象,实现了请求发送者与接收者的解耦。在C++这种强类型静态语言中,命令模式的应用尤为广泛,特别是在需要实现撤销/重做、任务队列、事务处理等场景时。
命令模式的核心思想可以用餐厅点餐来类比:顾客(Client)向服务员(Invoker)下达订单(Command),服务员并不关心谁来烹饪(Receiver),只需将订单交给后厨。这里的订单对象就是具体命令,它包含了执行所需的所有信息。
在C++中实现命令模式通常包含以下角色:
- Command(抽象命令类):声明执行操作的接口
- ConcreteCommand(具体命令):实现Execute方法,调用接收者的操作
- Invoker(调用者):要求命令执行请求
- Receiver(接收者):知道如何实施请求的具体操作
- Client(客户端):创建具体命令并设置接收者
2. C++命令模式的经典实现
2.1 基础类结构设计
让我们从一个最简单的文本编辑器示例开始,实现一个支持撤销的文字处理命令:
cpp复制#include <iostream>
#include <string>
#include <vector>
#include <memory>
// 接收者类 - 实际执行操作的对象
class Document {
public:
void Insert(const std::string& text, size_t position) {
content_.insert(position, text);
std::cout << "插入文本: " << text << " 在位置 " << position << std::endl;
}
void Delete(size_t position, size_t length) {
std::string deleted = content_.substr(position, length);
content_.erase(position, length);
std::cout << "删除文本: " << deleted << " 从位置 " << position << std::endl;
}
const std::string& GetContent() const { return content_; }
private:
std::string content_;
};
// 抽象命令接口
class Command {
public:
virtual ~Command() = default;
virtual void Execute() = 0;
virtual void Undo() = 0;
};
// 具体命令 - 插入文本
class InsertCommand : public Command {
public:
InsertCommand(Document& doc, const std::string& text, size_t position)
: document_(doc), text_(text), position_(position) {}
void Execute() override {
document_.Insert(text_, position_);
}
void Undo() override {
document_.Delete(position_, text_.length());
}
private:
Document& document_;
std::string text_;
size_t position_;
};
// 具体命令 - 删除文本
class DeleteCommand : public Command {
public:
DeleteCommand(Document& doc, size_t position, size_t length)
: document_(doc), position_(position), length_(length) {
deletedText_ = document_.GetContent().substr(position, length);
}
void Execute() override {
document_.Delete(position_, length_);
}
void Undo() override {
document_.Insert(deletedText_, position_);
}
private:
Document& document_;
size_t position_;
size_t length_;
std::string deletedText_;
};
// 调用者 - 负责执行命令
class Editor {
public:
void ExecuteCommand(std::unique_ptr<Command> cmd) {
cmd->Execute();
commandHistory_.push_back(std::move(cmd));
}
void Undo() {
if (!commandHistory_.empty()) {
commandHistory_.back()->Undo();
commandHistory_.pop_back();
}
}
private:
std::vector<std::unique_ptr<Command>> commandHistory_;
};
2.2 客户端使用示例
cpp复制int main() {
Document doc;
Editor editor;
// 执行插入命令
editor.ExecuteCommand(std::make_unique<InsertCommand>(doc, "Hello", 0));
editor.ExecuteCommand(std::make_unique<InsertCommand>(doc, " World", 5));
std::cout << "当前文档: " << doc.GetContent() << std::endl;
// 执行删除命令
editor.ExecuteCommand(std::make_unique<DeleteCommand>(doc, 0, 5));
std::cout << "当前文档: " << doc.GetContent() << std::endl;
// 撤销操作
editor.Undo();
std::cout << "撤销后文档: " << doc.GetContent() << std::endl;
return 0;
}
这个基础实现展示了命令模式的核心价值:将操作封装为对象,使得我们可以轻松实现撤销功能。每个命令对象都保存了执行和撤销操作所需的全部状态。
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_;
};
// 使用示例
void DemoMacroCommand() {
Document doc;
Editor editor;
auto macro = std::make_unique<MacroCommand>();
macro->AddCommand(std::make_unique<InsertCommand>(doc, "Hello", 0));
macro->AddCommand(std::make_unique<InsertCommand>(doc, " World", 5));
macro->AddCommand(std::make_unique<InsertCommand>(doc, "!", 11));
editor.ExecuteCommand(std::move(macro));
std::cout << "宏命令执行后: " << doc.GetContent() << std::endl;
editor.Undo();
std::cout << "撤销宏命令后: " << doc.GetContent() << std::endl;
}
复合命令特别适合需要批量操作的场景,比如图形编辑器中的组合图形操作,或者数据库中的事务处理。
3.2 命令队列与异步执行
命令模式天然适合实现任务队列和异步执行。我们可以创建一个命令队列,由专门的线程来执行:
cpp复制#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
class CommandQueue {
public:
void AddCommand(std::unique_ptr<Command> cmd) {
std::lock_guard<std::mutex> lock(mutex_);
commands_.push(std::move(cmd));
condition_.notify_one();
}
void StartProcessing() {
processor_ = std::thread([this] {
while (true) {
std::unique_ptr<Command> cmd;
{
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return !commands_.empty(); });
cmd = std::move(commands_.front());
commands_.pop();
}
if (cmd) {
cmd->Execute();
}
}
});
}
~CommandQueue() {
if (processor_.joinable()) {
processor_.join();
}
}
private:
std::queue<std::unique_ptr<Command>> commands_;
std::mutex mutex_;
std::condition_variable condition_;
std::thread processor_;
};
这种实现方式在游戏开发中非常常见,用于处理渲染命令、物理计算等异步任务。
4. 命令模式在C++项目中的实战应用
4.1 游戏开发中的输入处理
命令模式非常适合处理游戏输入系统。不同按键可以绑定到不同的命令对象,使得按键配置可以动态改变:
cpp复制class InputHandler {
public:
void HandleInput() {
if (IsKeyPressed(KEY_W)) buttonW_->Execute();
if (IsKeyPressed(KEY_S)) buttonS_->Execute();
if (IsKeyPressed(KEY_A)) buttonA_->Execute();
if (IsKeyPressed(KEY_D)) buttonD_->Execute();
}
void BindKeyW(std::unique_ptr<Command> cmd) { buttonW_ = std::move(cmd); }
void BindKeyS(std::unique_ptr<Command> cmd) { buttonS_ = std::move(cmd); }
void BindKeyA(std::unique_ptr<Command> cmd) { buttonA_ = std::move(cmd); }
void BindKeyD(std::unique_ptr<Command> cmd) { buttonD_ = std::move(cmd); }
private:
std::unique_ptr<Command> buttonW_;
std::unique_ptr<Command> buttonS_;
std::unique_ptr<Command> buttonA_;
std::unique_ptr<Command> buttonD_;
bool IsKeyPressed(int key) {
// 模拟按键检测
return true;
}
};
class JumpCommand : public Command {
public:
void Execute() override { std::cout << "角色跳跃\n"; }
void Undo() override { std::cout << "取消跳跃\n"; }
};
class FireCommand : public Command {
public:
void Execute() override { std::cout << "开火\n"; }
void Undo() override { std::cout << "取消开火\n"; }
};
4.2 事务系统实现
命令模式也是实现事务系统的理想选择。每个事务可以表示为一个命令对象,支持提交和回滚:
cpp复制class Database {
public:
void ExecuteSQL(const std::string& sql) {
std::cout << "执行SQL: " << sql << std::endl;
// 实际执行数据库操作
}
};
class TransactionCommand : public Command {
public:
explicit TransactionCommand(Database& db) : db_(db) {}
void AddStatement(const std::string& sql) {
statements_.push_back(sql);
}
void Execute() override {
for (const auto& sql : statements_) {
db_.ExecuteSQL(sql);
}
}
void Undo() override {
// 实现回滚逻辑
std::cout << "回滚事务\n";
}
private:
Database& db_;
std::vector<std::string> statements_;
};
4.3 性能优化技巧
在性能敏感的场景中,命令模式的实现可以做一些优化:
- 对象池技术:对于频繁创建销毁的命令对象,可以使用对象池来复用
- 轻量级命令:对于简单命令,可以使用函数指针或std::function替代类层次结构
- 批量执行:合并多个小命令为一个大命令,减少虚函数调用开销
cpp复制// 使用std::function的轻量级命令实现
class LightweightCommand {
public:
using CommandFunc = std::function<void()>;
explicit LightweightCommand(CommandFunc execute, CommandFunc undo = {})
: execute_(std::move(execute)), undo_(std::move(undo)) {}
void Execute() { if (execute_) execute_(); }
void Undo() { if (undo_) undo_(); }
private:
CommandFunc execute_;
CommandFunc undo_;
};
// 使用示例
void DemoLightweightCommand() {
int value = 0;
LightweightCommand increment(
[&value] { ++value; std::cout << "增加: " << value << "\n"; },
[&value] { --value; std::cout << "撤销增加: " << value << "\n"; }
);
increment.Execute();
increment.Execute();
increment.Undo();
}
5. 命令模式与其他设计模式的结合
5.1 命令模式与备忘录模式
结合备忘录模式可以实现更强大的撤销/重做功能,特别是在命令对象本身状态复杂时:
cpp复制class Memento {
public:
virtual ~Memento() = default;
};
class DocumentMemento : public Memento {
public:
explicit DocumentMemento(std::string content) : content_(std::move(content)) {}
std::string GetContent() const { return content_; }
private:
std::string content_;
};
class DocumentWithMemento : public Document {
public:
std::unique_ptr<Memento> CreateMemento() const {
return std::make_unique<DocumentMemento>(content_);
}
void RestoreFromMemento(const Memento* memento) {
if (const auto* docMemento = dynamic_cast<const DocumentMemento*>(memento)) {
content_ = docMemento->GetContent();
}
}
};
class MementoCommand : public Command {
public:
MementoCommand(DocumentWithMemento& doc) : document_(doc) {}
void Execute() override {
memento_ = document_.CreateMemento();
// 执行实际命令操作
}
void Undo() override {
if (memento_) {
document_.RestoreFromMemento(memento_.get());
}
}
protected:
DocumentWithMemento& document_;
std::unique_ptr<Memento> memento_;
};
5.2 命令模式与责任链模式
将命令模式与责任链模式结合,可以实现灵活的命令处理管道:
cpp复制class CommandHandler {
public:
virtual ~CommandHandler() = default;
virtual bool Handle(Command& cmd) = 0;
void SetNext(std::unique_ptr<CommandHandler> next) { next_ = std::move(next); }
protected:
std::unique_ptr<CommandHandler> next_;
};
class LoggingHandler : public CommandHandler {
public:
bool Handle(Command& cmd) override {
std::cout << "开始执行命令\n";
bool result = next_ ? next_->Handle(cmd) : true;
std::cout << "命令执行完成\n";
return result;
}
};
class ValidationHandler : public CommandHandler {
public:
bool Handle(Command& cmd) override {
// 执行验证逻辑
if (/* 验证失败 */ false) {
return false;
}
return next_ ? next_->Handle(cmd) : true;
}
};
class CommandPipeline {
public:
void AddHandler(std::unique_ptr<CommandHandler> handler) {
if (!first_) {
first_ = std::move(handler);
last_ = first_.get();
} else {
last_->SetNext(std::move(handler));
last_ = last_->next_.get();
}
}
bool Execute(Command& cmd) {
return first_ ? first_->Handle(cmd) : false;
}
private:
std::unique_ptr<CommandHandler> first_;
CommandHandler* last_ = nullptr;
};
5.3 命令模式与原型模式
使用原型模式可以轻松实现命令的克隆,这在需要重复执行相似命令时非常有用:
cpp复制class CloneableCommand : public Command {
public:
virtual std::unique_ptr<CloneableCommand> Clone() const = 0;
};
template <typename T>
class ClonableCommandImpl : public CloneableCommand {
public:
std::unique_ptr<CloneableCommand> Clone() const override {
return std::make_unique<T>(static_cast<const T&>(*this));
}
};
class RepeatCommand : public ClonableCommandImpl<RepeatCommand> {
public:
explicit RepeatCommand(std::unique_ptr<CloneableCommand> cmd, int times)
: cmd_(std::move(cmd)), times_(times) {}
void Execute() override {
for (int i = 0; i < times_; ++i) {
auto cloned = cmd_->Clone();
cloned->Execute();
}
}
void Undo() override {
// 实现撤销逻辑
}
private:
std::unique_ptr<CloneableCommand> cmd_;
int times_;
};
6. C++命令模式的最佳实践与陷阱
6.1 内存管理注意事项
在C++中实现命令模式时,内存管理是一个需要特别注意的问题:
- 所有权明确:命令对象通常由创建者拥有,但可能被多个地方使用(如历史记录、队列等),使用智能指针(std::unique_ptr/std::shared_ptr)可以简化管理
- 生命周期控制:长时间运行的命令队列要注意命令对象的生命周期,避免悬挂引用
- 对象池:对于频繁创建的命令对象,考虑使用对象池减少内存分配开销
cpp复制class CommandPool {
public:
template <typename T, typename... Args>
std::unique_ptr<T, std::function<void(T*)>> Acquire(Args&&... args) {
if (auto it = pools_.find(typeid(T)); it != pools_.end() && !it->second.empty()) {
auto ptr = static_cast<T*>(it->second.back().release());
it->second.pop_back();
new(ptr) T(std::forward<Args>(args)...);
return {ptr, [this](T* p) { Release(p); }};
}
auto ptr = std::make_unique<T>(std::forward<Args>(args)...);
return {ptr.release(), [this](T* p) { Release(p); }};
}
private:
void Release(Command* cmd) {
auto& pool = pools_[typeid(*cmd)];
cmd->~Command();
pool.emplace_back(cmd);
}
std::unordered_map<std::type_index, std::vector<std::unique_ptr<Command>>> pools_;
};
6.2 线程安全实现
在多线程环境中使用命令模式时,需要考虑线程安全问题:
- 命令队列:使用互斥锁保护共享命令队列
- 命令状态:如果命令对象包含可变状态,需要适当同步
- 执行顺序:确保命令的执行顺序符合预期
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;
}
bool Empty() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.empty();
}
private:
mutable std::mutex mutex_;
std::queue<std::unique_ptr<Command>> queue_;
};
6.3 性能优化实践
- 避免虚函数开销:对于性能关键路径,考虑使用std::function或CRTP模式减少虚函数调用
- 命令合并:将多个小命令合并为一个大命令,减少调度开销
- 批处理:支持批处理接口,减少锁竞争
cpp复制// 使用CRTP减少虚函数开销
template <typename Derived>
class CRTPCommand {
public:
void Execute() { static_cast<Derived*>(this)->ExecuteImpl(); }
void Undo() { static_cast<Derived*>(this)->UndoImpl(); }
};
class ConcreteCRTPCommand : public CRTPCommand<ConcreteCRTPCommand> {
public:
void ExecuteImpl() { std::cout << "CRTP命令执行\n"; }
void UndoImpl() { std::cout << "CRTP命令撤销\n"; }
};
6.4 常见陷阱与解决方案
- 命令膨胀:避免创建过多细粒度命令类,可以使用参数化命令
- 状态管理:确保命令对象是无状态的或状态可序列化
- 异常安全:命令执行过程中要考虑异常安全性,确保系统状态一致
cpp复制class ParametricCommand : public Command {
public:
using Action = std::function<void()>;
using UndoAction = std::function<void()>;
ParametricCommand(Action action, UndoAction undo)
: action_(std::move(action)), undo_(std::move(undo)) {}
void Execute() override {
try {
action_();
} catch (...) {
// 记录异常信息
throw;
}
}
void Undo() override {
if (undo_) undo_();
}
private:
Action action_;
UndoAction undo_;
};
在实际项目中,命令模式的这些高级用法和注意事项可以帮助我们构建更灵活、更健壮的系统。特别是在需要支持复杂操作序列、撤销/重做、事务处理等场景时,命令模式提供了一种清晰、可扩展的解决方案。
