哈喽,各位好!今天咱们来聊聊一个挺有意思的设计模式,叫Command模式。这玩意儿听起来高大上,但其实用起来很顺手,尤其是在需要实现撤销/重做功能的时候,简直就是神器。
Command模式是啥? 简单来说就是把一个请求或者操作封装成一个对象。
想象一下,你在玩一个游戏,你按了一下“跳跃”按钮。在Command模式的世界里,这个“跳跃”不是直接执行,而是被封装成一个“跳跃命令”的对象。这个对象知道谁要跳跃(接收者),以及怎么跳跃(执行方法)。 这样一来,我们就可以把这个命令对象存储起来,稍后执行,甚至撤销。
Command模式的组成部分
Command模式主要包含以下几个角色:
- Command(命令): 这是一个接口或者抽象类,定义了执行命令的接口
execute()
。 所有的具体命令类都要实现这个接口。 - ConcreteCommand(具体命令): 这是实现了Command接口的具体类。它关联一个接收者对象,并调用接收者的相应方法来执行命令。
- Receiver(接收者): 这是真正执行命令的对象。它知道如何完成请求所需的具体操作。
- Invoker(调用者): 这是负责调用命令的对象。它不关心命令是如何执行的,只负责在合适的时机调用命令的
execute()
方法。 - Client(客户端): 负责创建具体的命令对象,并设置其接收者。
举个例子:遥控器
我们用一个遥控器来理解一下。遥控器就是一个Invoker,每个按钮就是一个Command,电视机就是Receiver。你按下“开机”按钮,遥控器就调用了“开机命令”的 execute()
方法,然后“开机命令”就调用了电视机的开机功能。
Command模式的优点
- 解耦: Invoker和Receiver之间完全解耦,Invoker不需要知道Receiver是谁,以及如何执行命令。
- 可扩展性: 可以很容易地添加新的命令,而不需要修改现有的代码。
- 支持撤销/重做: 因为命令被封装成对象,所以可以很方便地实现撤销和重做功能。
- 支持队列化: 可以将多个命令放入队列中,然后依次执行。
- 日志记录: 可以记录执行过的命令,方便调试和分析。
Command模式的缺点
- 类数量增加: 每个命令都需要创建一个类,可能会导致类的数量增加。
- 复杂性增加: 对于简单的操作,使用Command模式可能会增加代码的复杂性。
代码实现:一个简单的计算器
咱们用一个简单的计算器来演示Command模式。这个计算器支持加法、减法操作,并且可以撤销和重做。
#include <iostream>
#include <vector>
// Receiver: 计算器
class Calculator {
public:
int currentValue = 0;
void add(int operand) {
currentValue += operand;
std::cout << "加法: " << operand << ", 当前值: " << currentValue << std::endl;
}
void subtract(int operand) {
currentValue -= operand;
std::cout << "减法: " << operand << ", 当前值: " << currentValue << std::endl;
}
int getValue() const {
return currentValue;
}
};
// Command: 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void unexecute() = 0; // 撤销操作
virtual int getOperand() const { return 0; } // 用于记录操作数,方便撤销
};
// ConcreteCommand: 加法命令
class AddCommand : public Command {
private:
Calculator* calculator;
int operand;
public:
AddCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}
void execute() override {
calculator->add(operand);
}
void unexecute() override {
calculator->subtract(operand); // 撤销加法就是减法
}
int getOperand() const override {
return operand;
}
};
// ConcreteCommand: 减法命令
class SubtractCommand : public Command {
private:
Calculator* calculator;
int operand;
public:
SubtractCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}
void execute() override {
calculator->subtract(operand);
}
void unexecute() override {
calculator->add(operand); // 撤销减法就是加法
}
int getOperand() const override {
return operand;
}
};
// Invoker: 命令调用者
class CommandManager {
private:
std::vector<Command*> history; // 命令历史记录
int currentIndex = -1; // 当前命令索引
Calculator* calculator;
public:
CommandManager(Calculator* calculator) : calculator(calculator) {}
~CommandManager() {
for (Command* command : history) {
delete command;
}
}
void executeCommand(Command* command) {
// 删除当前索引之后的所有命令,相当于清空redo栈
while (history.size() > currentIndex + 1) {
delete history.back();
history.pop_back();
}
command->execute();
history.push_back(command);
currentIndex++;
}
void undo() {
if (currentIndex >= 0) {
history[currentIndex]->unexecute();
currentIndex--;
} else {
std::cout << "无法撤销" << std::endl;
}
}
void redo() {
if (currentIndex < history.size() - 1) {
currentIndex++;
history[currentIndex]->execute();
} else {
std::cout << "无法重做" << std::endl;
}
}
int getCurrentValue() const {
return calculator->getValue();
}
};
int main() {
Calculator calculator;
CommandManager commandManager(&calculator);
// 执行加法命令
commandManager.executeCommand(new AddCommand(&calculator, 5)); // 当前值: 5
commandManager.executeCommand(new AddCommand(&calculator, 3)); // 当前值: 8
commandManager.executeCommand(new SubtractCommand(&calculator, 2)); // 当前值: 6
std::cout << "当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 6
// 撤销操作
commandManager.undo(); // 撤销减法, 当前值: 8
std::cout << "撤销后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 8
commandManager.undo(); // 撤销加法, 当前值: 5
std::cout << "再次撤销后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 5
// 重做操作
commandManager.redo(); // 重做加法, 当前值: 8
std::cout << "重做后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 8
commandManager.redo(); // 重做减法, 当前值: 6
std::cout << "再次重做后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 6
commandManager.redo(); // 无法重做
commandManager.undo();
commandManager.executeCommand(new AddCommand(&calculator, 10)); // 当前值: 16 之前的redo操作全部失效
return 0;
}
代码解释
- Calculator (Receiver): 这是真正的计算器类,负责执行加法和减法操作。
currentValue
存储当前的值。 - Command (Command): 这是一个抽象类,定义了
execute()
和unexecute()
方法。所有具体的命令类都要继承它。getOperand()
用于获取操作数,方便撤销。 - AddCommand & SubtractCommand (ConcreteCommand): 这两个类分别实现了加法和减法命令。它们关联一个
Calculator
对象,并在execute()
方法中调用Calculator
相应的加法或减法方法。unexecute()
方法用于撤销操作,加法的撤销是减法,减法的撤销是加法。 - CommandManager (Invoker): 这个类负责管理命令的历史记录。
executeCommand()
方法执行命令,并将命令添加到历史记录中。undo()
方法撤销上一个命令,redo()
方法重做下一个命令。history
是一个std::vector<Command*>
,用于存储命令的历史记录。currentIndex
记录当前命令的索引。
撤销/重做的实现
撤销/重做的关键在于 CommandManager
类中的 history
向量和 currentIndex
变量。
history
: 存储了所有执行过的命令对象。currentIndex
: 指向当前可以撤销的命令的索引。
当执行一个新命令时,命令会被添加到 history
向量中,并且 currentIndex
会增加。当执行 undo()
操作时,currentIndex
会减少,并且 history[currentIndex]
对应的命令的 unexecute()
方法会被调用。当执行 redo()
操作时,currentIndex
会增加,并且 history[currentIndex]
对应的命令的 execute()
方法会被调用。
更复杂的情况:状态保存
上面的例子很简单,但是如果 Receiver 的状态很复杂,直接通过 unexecute()
来恢复状态可能会很困难。 比如,一个文本编辑器,如果每次撤销都要重新计算整个文本的排版,效率会很低。
这时候,可以考虑在 Command
对象中保存执行命令之前的 Receiver 的状态。这样,撤销的时候,直接把 Receiver 的状态恢复到之前保存的状态就可以了。
// 假设 Calculator 类更复杂,需要保存状态
class Calculator {
public:
int currentValue = 0;
// ... 其他状态 ...
// 保存当前状态
struct Memento {
int currentValue;
// ... 其他状态 ...
};
Memento saveState() {
Memento m;
m.currentValue = currentValue;
// ... 保存其他状态 ...
return m;
}
// 恢复到指定状态
void restoreState(const Memento& m) {
currentValue = m.currentValue;
// ... 恢复其他状态 ...
}
void add(int operand) {
currentValue += operand;
std::cout << "加法: " << operand << ", 当前值: " << currentValue << std::endl;
}
void subtract(int operand) {
currentValue -= operand;
std::cout << "减法: " << operand << ", 当前值: " << currentValue << std::endl;
}
int getValue() const {
return currentValue;
}
};
// 修改后的 Command
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void unexecute() = 0;
};
class AddCommand : public Command {
private:
Calculator* calculator;
int operand;
Calculator::Memento previousState; // 保存之前的状态
public:
AddCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}
void execute() override {
previousState = calculator->saveState(); // 保存状态
calculator->add(operand);
}
void unexecute() override {
calculator->restoreState(previousState); // 恢复状态
}
};
class SubtractCommand : public Command {
private:
Calculator* calculator;
int operand;
Calculator::Memento previousState; // 保存之前的状态
public:
SubtractCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}
void execute() override {
previousState = calculator->saveState(); // 保存状态
calculator->subtract(operand);
}
void unexecute() override {
calculator->restoreState(previousState); // 恢复状态
}
};
// CommandManager 不需要修改,因为 Command 已经负责状态管理
在这个例子中,Calculator
类增加了一个 Memento
结构体,用于保存和恢复状态。AddCommand
和 SubtractCommand
类在 execute()
方法中保存了 Calculator
的状态,在 unexecute()
方法中恢复了状态。 这种方式更加通用,适用于复杂的 Receiver 对象。
Command模式的应用场景
- GUI应用程序: 菜单项、按钮等操作都可以封装成命令,方便实现撤销/重做功能。
- 事务处理: 数据库事务可以看作是一系列命令的集合,可以进行提交和回滚操作。
- 游戏开发: 玩家的操作可以封装成命令,方便实现录像和回放功能。
- 工作流引擎: 工作流中的每个步骤可以封装成命令,方便实现流程的控制和管理.
- 日志系统: 记录操作日志,方便问题排查和审计。
总结
Command模式是一种非常实用的设计模式,它可以将请求封装成对象,从而实现解耦、可扩展性、撤销/重做等功能。虽然它可能会增加类的数量,但对于需要灵活控制和管理操作的场景来说,Command模式绝对是一个值得考虑的选择。
希望今天的讲解对大家有所帮助!记住,设计模式不是银弹,要根据实际情况选择合适的模式。 编码愉快!