什么是命令模式?
命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
引出:
在市中心逛了很久的街后, 你找到了一家不错的餐厅, 坐在了临窗的座位上。 一名友善的服务员走近你, 迅速记下你点的食物, 写在一张纸上。 服务员来到厨房, 把订单贴在墙上。 过了一段时间, 厨师拿到了订单, 他根据订单来准备食物。 厨师将做好的食物和订单一起放在托盘上。 服务员看到托盘后对订单进行检查, 确保所有食物都是你要的, 然后将食物放到了你的桌上。
那张纸就是一个命令, 它在厨师开始烹饪前一直位于队列中。 命令中包含与烹饪这些食物相关的所有信息。 厨师能够根据它马上开始烹饪, 而无需跑来直接和你确认订单详情。
介绍
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→命令→接受者。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口。
命令模式的基本结构
命令模式的核心思想是将“请求”封装为对象,使得可以用不同的请求对客户进行参数化。这样做的好处是能够轻松地扩展和更改系统中的命令,而不影响调用者。
命令模式主要包括以下几个组件:
- 命令接口(Command Interface):定义执行操作的接口。
- 具体命令(Concrete Command):实现命令接口,执行具体的操作。
- 调用者(Invoker):调用命令对象执行请求。
- 接收者(Receiver):执行请求的具体操作。
示例代码:
1. 命令接口
首先,我们定义一个命令接口,所有的具体命令都将实现这个接口:
#include <iostream>
#include <memory>
// Command接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
2. 具体命令
然后,我们实现几个具体的命令,比如打开和关闭灯光的命令:
// 接收者类:灯
class Light {
public:
void on() {
std::cout << "Light is ON" << std::endl;
}
void off() {
std::cout << "Light is OFF" << std::endl;
}
};
// 具体命令类:打开灯
class LightOnCommand : public Command {
public:
explicit LightOnCommand(std::shared_ptr<Light> light) : light_(light) {}
void execute() override {
light_->on();
}
void undo() override {
light_->off();
}
private:
std::shared_ptr<Light> light_;
};
// 具体命令类:关闭灯
class LightOffCommand : public Command {
public:
explicit LightOffCommand(std::shared_ptr<Light> light) : light_(light) {}
void execute() override {
light_->off();
}
void undo() override {
light_->on();
}
private:
std::shared_ptr<Light> light_;
};
3. 调用者
接下来,我们实现一个调用者类,用于执行命令:
// 调用者类:遥控器
class RemoteControl {
public:
void setCommand(std::shared_ptr<Command> command) {
command_ = command;
}
void pressButton() {
if (command_) {
command_->execute();
}
}
void pressUndo() {
if (command_) {
command_->undo();
}
}
private:
std::shared_ptr<Command> command_;
};
4. 客户端代码
最后,我们编写客户端代码来测试我们的命令模式实现:
int main() {
auto light = std::make_shared<Light>();
auto lightOnCommand = std::make_shared<LightOnCommand>(light);
auto lightOffCommand = std::make_shared<LightOffCommand>(light);
RemoteControl remote;
// 打开灯
remote.setCommand(lightOnCommand);
remote.pressButton(); // 输出: Light is ON
// 撤销打开灯操作
remote.pressUndo(); // 输出: Light is OFF
// 关闭灯
remote.setCommand(lightOffCommand);
remote.pressButton(); // 输出: Light is OFF
// 撤销关闭灯操作
remote.pressUndo(); // 输出: Light is ON
return 0;
}
优点
1. 解耦调用者和接收者
命令模式将请求的发出者和执行者解耦,使得调用者只需要知道如何发出请求,而不需要知道请求是如何被执行的。这种解耦提高了代码的灵活性和可维护性。
2. 增强可扩展性
新的命令可以很容易地添加到系统中,而不需要修改现有代码。这使得系统具有良好的扩展性。例如,可以轻松地添加新的家电控制命令,而不影响现有的遥控器实现。
3. 支持撤销和重做操作
通过在命令对象中实现 undo
方法,命令模式可以方便地支持撤销和重做操作。这对于需要复杂操作的应用(如编辑器、事务性系统)非常有用。
4. 支持组合命令
命令模式可以将多个命令组合成一个宏命令,从而实现一系列操作的封装。这使得批处理操作变得简单。
5. 支持日志记录和回放
可以将命令对象记录下来,以支持日志记录和回放操作。通过这种方式,可以实现操作的重放和审计。
缺点
1. 增加类的数量
引入命令模式通常会增加系统中类的数量,因为每个具体操作都需要一个对应的命令类。这可能会使系统变得复杂,尤其是在操作非常多的情况下。
2. 引用开销
每个命令对象都需要保存接收者的引用,这可能会增加内存开销,特别是在命令对象数量较多时。
3. 简单命令的过度设计
对于一些简单操作,使用命令模式可能显得过于复杂和冗余。例如,对于仅仅调用一个方法的简单操作,命令模式可能增加了不必要的复杂性。
4. 需要额外的逆操作逻辑
为了支持撤销操作,每个命令都需要实现 undo
方法,这可能需要维护额外的状态和逻辑,增加了实现的复杂性。
示例扩展
为了更好地理解这些优缺点,我们可以考虑扩展前面的遥控器示例,增加一个组合命令(宏命令)和撤销功能。
组合命令(宏命令)
组合命令可以将多个命令组合在一起,使得它们可以被一起执行或撤销:
#include <vector>
// 宏命令类
class MacroCommand : public Command {
public:
void addCommand(std::shared_ptr<Command> command) {
commands_.push_back(command);
}
void execute() override {
for (const auto& command : commands_) {
command->execute();
}
}
void undo() override {
for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
(*it)->undo();
}
}
private:
std::vector<std::shared_ptr<Command>> commands_;
};
示例客户端代码
int main() {
auto light = std::make_shared<Light>();
auto lightOnCommand = std::make_shared<LightOnCommand>(light);
auto lightOffCommand = std::make_shared<LightOffCommand>(light);
RemoteControl remote;
// 创建宏命令
auto macroCommand = std::make_shared<MacroCommand>();
macroCommand->addCommand(lightOnCommand);
macroCommand->addCommand(lightOffCommand);
// 执行宏命令
remote.setCommand(macroCommand);
remote.pressButton(); // 输出: Light is ON, Light is OFF
// 撤销宏命令
remote.pressUndo(); // 输出: Light is ON, Light is OFF
return 0;
}
通过这种扩展,我们可以看到命令模式的灵活性和扩展性如何在实际应用中体现。同时,我们也可以理解在更复杂的场景中,命令模式如何增加系统的复杂性和类的数量。
总结
通过以上示例,我们展示了命令模式如何将请求封装为对象,并通过这种方式实现了灵活的请求处理。命令模式使得请求的发出者与执行者解耦,增强了系统的扩展性和可维护性。
命令模式非常适用于以下场景:
- 需要对请求排队并执行。
- 需要支持撤销操作。
- 需要将一组操作封装为对象,并能进行参数化处理。
在实际开发中,命令模式可以用来实现诸如事务性操作、日志记录、宏命令等功能,是一种非常实用的设计模式。