本篇介绍了命令模式及相关的面向对象设计原则。命令模式将请求封装成对象,以便使用不同的请求来参数化其他对象,从而实现请求发出者和请求执行者之间的解耦。
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(); return 0; }
|
这样一来遥控器不需要知道命令的执行者是谁,只要为按钮绑定好对应的命令对象,然后发出命令,命令对象就会让对应的命令执行者执行对应的动作。同时,遥控器、命令和电器这三者之间是组合的关系,组合意味着可以随意替换,也就是说遥控器可以更换不同的命令对象,而命令对象同样可以更换不同的电器,只要这个电器符合该命令的接口就可以,比如客厅的电灯和卧室的电灯就可以分别绑定到两个不同的 LightXXXCommand
对象上,而无需重新编码。
命令模式将请求封装成对象,以便使用不同的请求来参数化其他对象。命令模式的类图如下所示:
当然,具体命令对象的实现非常灵活,命令要实现什么功能完全取决于 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(); } } };
|
命令模式应用非常广泛,比如可以实现命令队列,让多个线程从队列中获取命令对象并执行,这样一来线程无需知道命令执行的具体对象是谁,只要调用命令对象的执行方法即可;再比如软件中的执行日志,可以将软件运行时执行过的命令对象序列化到磁盘上,在软件异常中断恢复数据时,只需要按顺序再从磁盘读取该对象并执行命令就可以恢复到中断前的状态了。