15 命令模式

原文转载:https://blog.csdn.net/sinat_21107433/article/details/102810123

还记得Jungle曾经设计的Qt图片浏览器吗?鼠标点击“上一张”,浏览上一张图片;点击“下一张”,浏览下一张图片;点击“自动播放”,则自动从上到下播放每一张图片。是不是很有趣的一个小程序?

鼠标点击某个键,就好像用户在向图片浏览器发送指令,图片浏览器内部接收到指令后开始调用相应的函数,最终结果是播放上一张或下一张图片,即执行或响应了用户发出的命令。客户并不知道发出的命令是什么形式,也不知道图片浏览器内部命令是如何执行的;同样,浏览器内部也不知道是谁发送了命令。命令的发送方和接收方(执行方)没有任何关联。在软件设计模式中,有一种将命令的发送者与执行者解耦的设计模式——命令模式。

1.命令模式简介

命令模式可以将请求(命令)的发送者与接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道请求是如何完成的。下面是比较晦涩难懂的命令模式的定义:

命令模式:

将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。

 命令模式的定义比较复杂,也提到一些术语。这些将在下面的阐述和举例中做进一步说明。

2.命令模式结构

命令模式的UML结构如上图,命令模式一共有以下几种角色:

  • Command(抽象命令类):是一个抽象类,声明了用于执行命令的接口execute()。
  • ConcreteCommand(具体命令类):具体的命令类,实现了执行命令的接口execute(),它对应具体的接收者对象,将接收者(Receiver)的动作action()绑定其中。在execu()方法中将调用接收者的动作action()。(这就是定义中的“将请求封装成一个对象”的体现)
  • Invoker(调用者):请求的发送者,通过命令对象来执行请求。一个调用者不需要在设计时确定其接收者,所以调用者通过聚合,与命令类产生关联。具体实现中,可以将一个具体命令对象注入到调用者中,再通过调用具体命令对象的execute()方法,实现简介请求命令执行者(接收者)的操作
  • Receiver(接收者): 实现处理请求的具体操作(action)。

3.命令模式代码实例

房间中的开关(Button)就是命令模式的一个实现,本例使用命令模式来模拟开关功能,可控制的对象包括电灯(Lamp)风扇(Fan)。用户每次触摸(touch)开关,都可以打开或者关闭电灯或者电扇。

本实例的UML图如上所示。抽象命令类仅声明execute()接口。有两个具体命令类,分别是控制灯的LampCommand和控制风扇的FanCommand类,两个具体类中实现了execute()接口,即执行开关灯/风扇请求。本例中的调用者是按钮Button,每次用户触摸touch())开关按钮,即是在发送请求。本例具体设计实现过程如下。

3.1.接收者类:电灯和风扇

 
  1. // 接收者:电灯类

  2. class Lamp

  3. {

  4. public :

  5. Lamp(){

  6. this->lampState = false;

  7. }

  8. void on(){

  9. lampState = true;

  10. printf("Lamp is on\n");

  11. }

  12. void off(){

  13. lampState = false;

  14. printf("Lamp is off\n");

  15. }

  16. bool getLampState(){

  17. return lampState;

  18. }

  19. private:

  20. bool lampState;

  21. };

  22.  
  23. // 接收者:风扇类

  24. class Fan

  25. {

  26. public:

  27. Fan(){

  28. this->fanState = false;

  29. }

  30. void on(){

  31. fanState = true;

  32. printf("Fan is on\n");

  33. }

  34. void off(){

  35. fanState = false;

  36. printf("Fan is off\n");

  37. }

  38. bool getFanState(){

  39. return fanState;

  40. }

  41. private:

  42. bool fanState;

  43. };

3.2.抽象命令类

 
  1. // 抽象命令类 Command

  2. class Command

  3. {

  4. public:

  5. Command(){}

  6. // 声明抽象接口:发送命令

  7. virtual void execute() = 0;

  8. private:

  9. Command *command;

  10. };

3.3.具体命令类 

 
  1. // 具体命令类 LampCommand

  2. class LampCommand :public Command

  3. {

  4. public:

  5. LampCommand(){

  6. printf("开关控制电灯\n");

  7. lamp = new Lamp();

  8. }

  9. // 实现execute()

  10. void execute(){

  11. if (lamp->getLampState()){

  12. lamp->off();

  13. }

  14. else{

  15. lamp->on();

  16. }

  17. }

  18. private:

  19. Lamp *lamp;

  20. };

  21.  
  22. // 具体命令类 FanCommand

  23. class FanCommand :public Command

  24. {

  25. public:

  26. FanCommand(){

  27. printf("开关控制风扇\n");

  28. fan = new Fan();

  29. }

  30. // 实现execute()

  31. void execute(){

  32. if (fan->getFanState()){

  33. fan->off();

  34. }

  35. else{

  36. fan->on();

  37. }

  38. }

  39. private:

  40. Fan *fan;

  41. };

3.3.调用者:Button

 
  1. // 调用者 Button

  2. class Button

  3. {

  4. public:

  5. Button(){}

  6. // 注入具体命令类对象

  7. void setCommand(Command *cmd){

  8. this->command = cmd;

  9. }

  10. // 发送命令:触摸按钮

  11. void touch(){

  12. printf("触摸开关:");

  13. command->execute();

  14. }

  15. private:

  16. Command *command;

  17. };

 3.4.客户端代码示例

 
  1. #include <iostream>

  2. #include "CommandPattern.h"

  3.  
  4. int main()

  5. {

  6. // 实例化调用者:按钮

  7. Button *button = new Button();

  8. Command *lampCmd, *fanCmd;

  9.  
  10. // 按钮控制电灯

  11. lampCmd = new LampCommand();

  12. button->setCommand(lampCmd);

  13. button->touch();

  14. button->touch();

  15. button->touch();

  16.  
  17. printf("\n\n");

  18.  
  19. // 按钮控制风扇

  20. fanCmd = new FanCommand();

  21. button->setCommand(fanCmd);

  22. button->touch();

  23. button->touch();

  24. button->touch();

  25.  
  26. printf("\n\n");

  27. system("pause");

  28. return 0;

  29. }

3.5.效果

 可以看到,客户端只需要有一个调用者和抽象命令类,在给调用者注入命令时,再将命令类具体化(这也就是定义中“可用不同的请求对客户进行参数化”的体现)。客户端并不知道命令是如何传递和响应,只需发送命令touch()即可,由此实现命令发送者和接收者的解耦。

如果系统中增加了新的功能,功能键与新功能对应,只需增加对应的具体命令类,在新的具体命令类中调用新的功能类的action()方法,然后将该具体命令类通过注入的方式加入到调用者,无需修改原有代码,符合开闭原则。

4.命令队列

有时候,当请求发送者发送一个请求时,有不止一个请求接收者产生响应(Qt信号槽,一个信号可以连接多个槽),这些请求接收者将逐个执行业务方法,完成对请求的处理,此时可以用命令队列来实现。比如按钮开关同时控制电灯和风扇,这个例子中,请求发送者是按钮开关,有两个接收者产生响应,分别是电灯和风扇。

可以参考的命令队列的实现方式是增加一个命令队列类(CommandQueue)来存储多个命令对象,不同命令对象对应不同的命令接收者。调用者也将面对命令队列类编程,增加注入具体命令队列类对象的方法setCommandQueue(CommandQueue *cmdQueue)。

下面的例子展示了按钮开关请求时,电灯和风扇同时作为请求的接收者。代码如下所示:

 
  1. #ifdef COMMAND_QUEUE

  2. /*************************************/

  3. /* 命令队列 */

  4. #include <vector>

  5.  
  6. // 命令队列类

  7. class CommandQueue

  8. {

  9. public:

  10. CommandQueue(){

  11. }

  12. void addCommand(Command *cmd){

  13. commandQueue.push_back(cmd);

  14. }

  15. void execute(){

  16. for (int i = 0; i < commandQueue.size(); i++)

  17. {

  18. commandQueue[i]->execute();

  19. }

  20. }

  21. private:

  22. vector<Command*>commandQueue;

  23.  
  24. };

  25.  
  26. // 调用者

  27. class Button2

  28. {

  29. public:

  30. Button2(){}

  31. // 注入具体命令队列类对象

  32. void setCommandQueue(CommandQueue *cmdQueue){

  33. this->cmdQueue = cmdQueue;

  34. }

  35. // 发送命令:触摸按钮

  36. void touch(){

  37. printf("触摸开关:");

  38. cmdQueue->execute();

  39. }

  40. private:

  41. CommandQueue *cmdQueue;

  42. };

  43.  
  44. #endif

 客户端代码如下:

 
  1. #ifdef COMMAND_QUEUE

  2.  
  3. printf("\n\n***********************************\n");

  4. Button2 *button2 = new Button2();

  5. Command *lampCmd2, *fanCmd2;

  6. CommandQueue *cmdQueue = new CommandQueue();

  7.  
  8. // 按钮控制电灯

  9. lampCmd2 = new LampCommand();

  10. cmdQueue->addCommand(lampCmd2);

  11.  
  12. // 按钮控制风扇

  13. fanCmd2 = new FanCommand();

  14. cmdQueue->addCommand(fanCmd2);

  15.  
  16. button2->setCommandQueue(cmdQueue);

  17. button2->touch();

  18.  
  19. #endif

效果如下图:

5.命令模式其他应用

5.1.记录请求日志

将历史请求记录保存在日志里,即请求日志。很多软件系统都提供了日志文件,记录运行过程中的流程。一旦系统发生故障,日志成为了分析问题的关键。日志也可以保存命令队列中的所有命令对象,每执行完一个命令就从日志里删除一个对应的对象。

5.2.宏命令

宏命令又叫组合命令,是组合模式和命令模式的结合。宏命令是一个具体命令类,拥有一个命令集合,命令集合中包含了对其他命令对象的引用。宏命令通常不直接与请求者交互,而是通过它的成员来遍历调用接收者的方法。当调用宏命令的execute()方法时,就遍历执行每一个具体命令对象的execute()方法。(类似于前面的命令队列)

6.总结

优点:

  • 降低系统耦合度,将命令的请求者与接收者分离解耦,请求者和发送者不存在直接关联,各自独立互不影响。
  • 便于扩展:新的命令很容易加入到系统中,且符合开闭原则。
  • 较容易实现命令队列或宏命令。
  • 为请求的撤销和回复操作提供了一种设计实现方案。

缺点:

  • 命令模式可能导致系统中有过多的具体命令类,增加了系统中对象的数量。

适用环境:

  • 系统需要将请求发送者和接收者解耦,使得发送者和接收者互不影响。
  • 系统需要在不同时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销和恢复操作。
  • 系统需要将一组操作组合在一起形成宏命令。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值