0%

【设计模式】命令模式

本篇介绍了命令模式及相关的面向对象设计原则。命令模式将请求封装成对象,以便使用不同的请求来参数化其他对象,从而实现请求发出者和请求执行者之间的解耦。

1 引例

我们需要设计这样一个系统,有一个遥控器,遥控器有一个插槽和三个按钮,其中两个按钮对应“开”和“关”,一个按钮对应“撤销”操作,插槽中可以插入不同的控制模块来控制不同的电器,比如电灯、热水器、电动窗帘、空调等等,也就是说遥控器的功能取决于插槽内的控制模块。而不同电器的“开”和“关”操作都有各自的实现,他们是毫无关联的单独的类。

直观的方式是判断当前插槽内是什么模块,然后为按钮调用对应的电器的命令,但是当出现更多的电器时,需要不停的修改遥控器的代码。因为此时请求发出者(遥控器)和请求执行者(被操控的电器)是紧耦合的,如果能将它们解耦,那么系统将具有更好的弹性来应对各种变化。

2 命令模式

我们可以将遥控器的命令封装成一系列统一的命令对象,让命令对象和具体的命令执行者绑定,负责执行对应的命令,而命令发出者只需要调用命令对象的统一接口就可以实现对不同执行者的控制。

在上例中,遥控器的命令只有三种:开、关和撤销。接下来以电灯为例,按照上述思路来设计一个遥控器。电灯类非常简单,只具有开和关两种操作:

1
2
3
4
5
6
7
8
9
10
class Light {
public:
void on() {
// 开灯
}

void off() {
// 关灯
}
};

接下来是遥控器系统的实现,按照上面的思路,首先设计一个命令对象的接口,所有命令对象只有一个方法,那就是执行操作:

1
2
3
4
5
class Command {
public:
virtual void execute() = 0; // 执行命令
virtual void undo() = 0; // 撤销命令
};

接下来,开和关是两种命令,因此要定义为两类命令对象,命令对象需要包含命令的执行对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 开灯命令
class LightOnCommand : public Command {
public:
Light* light;
LightOnCommand(Light* lt) : light(lt) {}

virtual void execute() {
light->on();
}
// 撤销开灯即为关灯
virtual void undo() {
light->off();
}
};
// 关灯命令
class LightOffCommand : public Command {
public:
Light* light;
LightOffCommand(Light* lt) : light(lt) {}

virtual void execute() {
light->off();
}
// 撤销关灯即为开灯
virtual void undo() {
light->on();
}
};

最后是遥控器,遥控器需要包含命令对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class RemoteControl {
public:
Command* OnCommand; // 开按钮对应的命令对象
Command* OffCommand; // 关按钮对应的命令对象
Command* undoCommand; // 记录上一次按的按钮对应的命令,用于撤销按钮
// 初始为按钮绑定一个默认命令对象,该命令不做任何操作,这样可以避免对象是否为空的判断
RemoteControl() {
OnCommand = new noCommand();
OffCommand = new noCommand();
undoCommand = new noCommand();
}
// 为按钮绑定对应的命令对象
void setCommand(Command* on, Command* off) {
delete OnCommand;
OnCommand = on;
delete OffCommand;
OffCommand = off;
}

void PressOnButton() {
OnCommand->execute();
undoCommand = OnCommand;
}

void PressOffButton() {
OffCommand->execute();
undoCommand = OffCommand;
}

void PressUndoButton() {
undoCommand->undo();
}
};

然后就可以使用这个遥控器了:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
RemoteControl* rc = new RemoteControl();
Light* lt = new light();
Command* OnCommand = new LightOnCommand(lt);
Command* OffCommand = new LightOffCommand(lt);

rc.setCommand(OnCommand, OffCommand);
rc.PressOnButton();
rc.PressOffButton();
rc.PressUndoButton();
// delete ...
return 0;
}

这样一来遥控器不需要知道命令的执行者是谁,只要为按钮绑定好对应的命令对象,然后发出命令,命令对象就会让对应的命令执行者执行对应的动作。同时,遥控器、命令和电器这三者之间是组合的关系,组合意味着可以随意替换,也就是说遥控器可以更换不同的命令对象,而命令对象同样可以更换不同的电器,只要这个电器符合该命令的接口就可以,比如客厅的电灯和卧室的电灯就可以分别绑定到两个不同的 LightXXXCommand 对象上,而无需重新编码。

命令模式将请求封装成对象,以便使用不同的请求来参数化其他对象。命令模式的类图如下所示:

image-20230305134334949

当然,具体命令对象的实现非常灵活,命令要实现什么功能完全取决于 execute() 方法的实现,比如要打开电视,同时将电视的音量设定为 10,那么命令对象可以设计成这样:

1
2
3
4
5
6
7
8
9
10
11
12
class TVOnCommand : public Command {
public:
TV* tv;
TVOnCommand(TV* t) : tv(t) {}
virtual void execute() {
tv->on();
tv->setVolume(10);
}
virtual void undo() {
tv->off();
}
};

此外,命令对象还可以实现复合命令,来执行一系列命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MacroCommand : public Command {
public:
vector<Command*> commands;
MacroCommand(const vector<Command*>& cs) : commands(cs) {}
virtual void execute() {
for(auto& c : commands) {
c->execute();
}
}
virtual void undo() {
for(auto& c : commands) {
c->undo();
}
}
};

命令模式应用非常广泛,比如可以实现命令队列,让多个线程从队列中获取命令对象并执行,这样一来线程无需知道命令执行的具体对象是谁,只要调用命令对象的执行方法即可;再比如软件中的执行日志,可以将软件运行时执行过的命令对象序列化到磁盘上,在软件异常中断恢复数据时,只需要按顺序再从磁盘读取该对象并执行命令就可以恢复到中断前的状态了。

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。