命令模式:让请求与执行优雅解耦
说实话,命令模式是我在项目中用得最频繁的设计模式之一。为什么?因为它解决了一个很实际的问题——如何把「发起请求的对象」和「执行请求的对象」彻底分开。
我记得有一次接手一个老项目,里面有个菜单系统,每个按钮点击后直接调用业务逻辑。后来需求变了,要加撤销功能、要记录操作日志、还要支持宏录制……改得我头皮发麻。那时候我就想,要是当初用了命令模式,这些需求加进来简直不要太轻松。
模式动机与定义
先说说动机。你想想看,在传统设计中,调用者直接持有接收者的引用,调用者说「你干这个」,接收者就执行。这看起来没问题,但一旦出现下面这些场景,问题就来了:
- 需要记录操作历史,支持撤销/重做
- 需要把操作排队,异步执行
- 需要把多个操作组合成一个宏命令
- 需要在不同时间、不同地点执行同一个请求
说白了,调用者和接收者之间耦合太紧,导致扩展困难。
命令模式的定义其实很简洁:将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
核心思想:请求本身变成一个对象,这个对象包含了执行操作所需的所有信息。调用者只需要发出命令对象,至于谁来执行、怎么执行,调用者不关心。
角色与结构
命令模式涉及四个角色,我习惯把它们分成两组来理解:
| 角色 | 英文名 | 职责 |
|---|---|---|
| 命令接口 | Command | 声明执行操作的接口,通常包含 execute() 和 undo() |
| 具体命令 | ConcreteCommand | 实现命令接口,持有接收者引用,在 execute 中调用接收者的业务方法 |
| 调用者 | Invoker | 持有命令对象,在需要时调用命令的 execute 方法 |
| 接收者 | Receiver | 真正执行业务逻辑的对象 |
嗯,这里要注意:调用者只认识命令接口,不认识具体命令,更不认识接收者。这就是解耦的关键所在。
下面这张图能帮你理清它们之间的关系:
C++ 代码实现
光说不练假把式。我们直接看代码。假设我们要做一个文本编辑器,支持「插入文本」和「删除文本」两个操作。
先定义命令接口:
// Command.h
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
然后是接收者——文本编辑器:
// TextEditor.h
#include <string>
#include <iostream>
class TextEditor {
private:
std::string text_;
public:
void insert(const std::string& str, size_t pos) {
text_.insert(pos, str);
std::cout << "插入后: " << text_ << std::endl;
}
void remove(size_t pos, size_t len) {
text_.erase(pos, len);
std::cout << "删除后: " << text_ << std::endl;
}
const std::string& getText() const { return text_; }
};
接下来是具体命令。这里有个细节——命令对象需要保存执行时的上下文信息,这样才能支持撤销:
// InsertCommand.h
class InsertCommand : public Command {
private:
TextEditor* editor_;
std::string text_;
size_t position_;
public:
InsertCommand(TextEditor* editor, const std::string& text, size_t pos)
: editor_(editor), text_(text), position_(pos) {}
void execute() override {
editor_->insert(text_, position_);
}
void undo() override {
// 撤销插入 = 删除刚插入的文本
editor_->remove(position_, text_.length());
}
};
// DeleteCommand.h
class DeleteCommand : public Command {
private:
TextEditor* editor_;
std::string deletedText_;
size_t position_;
public:
DeleteCommand(TextEditor* editor, size_t pos, size_t len)
: editor_(editor), position_(pos) {
// 执行前先保存被删除的内容
deletedText_ = editor_->getText().substr(pos, len);
}
void execute() override {
editor_->remove(position_, deletedText_.length());
}
void undo() override {
// 撤销删除 = 重新插入被删的文本
editor_->insert(deletedText_, position_);
}
};
个人经验:DeleteCommand 的构造函数里就保存了被删除的内容。为什么不在 execute 里保存?因为如果 execute 被调用多次,每次保存的内容可能不一样。我习惯在构造时就把「快照」拍好,这样 undo 时能精确恢复。
最后是调用者——按钮或者菜单项:
// Button.h
#include <memory>
class Button {
private:
std::unique_ptr<Command> command_;
public:
void setCommand(std::unique_ptr<Command> cmd) {
command_ = std::move(cmd);
}
void click() {
if (command_) {
command_->execute();
}
}
};
使用起来是这样的:
int main() {
TextEditor editor;
Button btn;
// 插入 "Hello"
btn.setCommand(std::make_unique<InsertCommand>(&editor, "Hello", 0));
btn.click(); // 输出: 插入后: Hello
// 插入 " World"
btn.setCommand(std::make_unique<InsertCommand>(&editor, " World", 5));
btn.click(); // 输出: 插入后: Hello World
return 0;
}
撤销操作实现
撤销是命令模式的经典应用场景。实现思路其实很简单:维护一个命令历史栈,每次执行命令时压栈,撤销时从栈顶弹出并调用 undo。
// CommandHistory.h
#include <vector>
#include <memory>
class CommandHistory {
private:
std::vector<std::unique_ptr<Command>> history_;
public:
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
history_.push_back(std::move(cmd));
}
void undo() {
if (history_.empty()) {
std::cout << "没有可撤销的操作" << std::endl;
return;
}
auto& lastCmd = history_.back();
lastCmd->undo();
history_.pop_back();
}
bool canUndo() const {
return !history_.empty();
}
};
我曾经踩过的坑:撤销操作一定要考虑「幂等性」。什么意思?就是同一个命令的 undo 被多次调用时,不能把系统搞崩。我见过一个项目,undo 里直接 delete 了资源,结果第二次 undo 时又 delete 了一次……嗯,double free 了。
使用 CommandHistory 改造后的调用方式:
int main() {
TextEditor editor;
CommandHistory history;
// 插入 "Hello"
history.executeCommand(
std::make_unique<InsertCommand>(&editor, "Hello", 0));
// 输出: 插入后: Hello
// 插入 " World"
history.executeCommand(
std::make_unique<InsertCommand>(&editor, " World", 5));
// 输出: 插入后: Hello World
// 撤销一次
history.undo(); // 输出: 删除后: Hello
// 再撤销一次
history.undo(); // 输出: 删除后: (空字符串)
return 0;
}
你看,有了命令模式,撤销功能加进来几乎不需要改动现有代码。这就是设计模式的价值——不是让你写更少的代码,而是让你写更容易扩展的代码。
总结一下:命令模式把请求封装成对象,让调用者和接收者彻底解耦。它的核心价值在于:支持撤销/重做、支持操作队列、支持宏命令、支持日志记录。我个人觉得,凡是涉及到「操作历史」的场景,命令模式都是首选。
最后说一句,命令模式虽然好,但也不要滥用。如果你的系统只有两三个操作,而且永远不需要撤销,那直接用函数回调反而更简单。设计模式是工具,不是目的。