还记得Jungle曾经设计的Qt图片浏览器吗?鼠标点击“上一张”,浏览上一张图片;点击“下一张”,浏览下一张图片;点击“自动播放”,则自动从上到下播放每一张图片。是否是颇有趣的一个小程序?ios
鼠标点击某个键,就好像用户在向图片浏览器发送指令,图片浏览器内部接收到指令后开始调用相应的函数,最终结果是播放上一张或下一张图片,即执行或响应了用户发出的命令。客户并不知道发出的命令是什么形式,也不知道图片浏览器内部命令是如何执行的;一样,浏览器内部也不知道是谁发送了命令。命令的发送方和接收方(执行方)没有任何关联。在软件设计模式中,有一种将命令的发送者与执行者解耦的设计模式——命令模式。编程
命令模式能够将请求(命令)的发送者与接收者彻底解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只须要知道如何发送请求,而没必要知道请求是如何完成的。下面是比较晦涩难懂的命令模式的定义:小程序
命令模式:设计模式
将一个请求封装为一个对象,从而可用不一样的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操做。浏览器
命令模式的定义比较复杂,也提到一些术语。这些将在下面的阐述和举例中作进一步说明。微信
命令模式的UML结构如上图,命令模式一共有如下几种角色:函数
房间中的开关(Button)就是命令模式的一个实现,本例使用命令模式来模拟开关功能,可控制的对象包括电灯(Lamp)和风扇(Fan)。用户每次触摸(touch)开关,均可以打开或者关闭电灯或者电扇。this
本实例的UML图如上所示。抽象命令类仅声明execute()接口。有两个具体命令类,分别是控制灯的LampCommand和控制风扇的FanCommand类,两个具体类中实现了execute()接口,即执行开关灯/风扇请求。本例中的调用者是按钮Button,每次用户触摸touch())开关按钮,便是在发送请求。本例具体设计实现过程以下。spa
// 接收者:电灯类 class Lamp { public : Lamp(){ this->lampState = false; } void on(){ lampState = true; printf("Lamp is on\n"); } void off(){ lampState = false; printf("Lamp is off\n"); } bool getLampState(){ return lampState; } private: bool lampState; }; // 接收者:风扇类 class Fan { public: Fan(){ this->fanState = false; } void on(){ fanState = true; printf("Fan is on\n"); } void off(){ fanState = false; printf("Fan is off\n"); } bool getFanState(){ return fanState; } private: bool fanState; };
// 抽象命令类 Command class Command { public: Command(){} // 声明抽象接口:发送命令 virtual void execute() = 0; private: Command *command; };
// 具体命令类 LampCommand class LampCommand :public Command { public: LampCommand(){ printf("开关控制电灯\n"); lamp = new Lamp(); } // 实现execute() void execute(){ if (lamp->getLampState()){ lamp->off(); } else{ lamp->on(); } } private: Lamp *lamp; }; // 具体命令类 FanCommand class FanCommand :public Command { public: FanCommand(){ printf("开关控制风扇\n"); fan = new Fan(); } // 实现execute() void execute(){ if (fan->getFanState()){ fan->off(); } else{ fan->on(); } } private: Fan *fan; };
// 调用者 Button class Button { public: Button(){} // 注入具体命令类对象 void setCommand(Command *cmd){ this->command = cmd; } // 发送命令:触摸按钮 void touch(){ printf("触摸开关:"); command->execute(); } private: Command *command; };
#include <iostream> #include "CommandPattern.h" int main() { // 实例化调用者:按钮 Button *button = new Button(); Command *lampCmd, *fanCmd; // 按钮控制电灯 lampCmd = new LampCommand(); button->setCommand(lampCmd); button->touch(); button->touch(); button->touch(); printf("\n\n"); // 按钮控制风扇 fanCmd = new FanCommand(); button->setCommand(fanCmd); button->touch(); button->touch(); button->touch(); printf("\n\n"); system("pause"); return 0; }
能够看到,客户端只须要有一个调用者和抽象命令类,在给调用者注入命令时,再将命令类具体化(这也就是定义中“可用不一样的请求对客户进行参数化”的体现)。客户端并不知道命令是如何传递和响应,只需发送命令touch()便可,由此实现命令发送者和接收者的解耦。设计
若是系统中增长了新的功能,功能键与新功能对应,只需增长对应的具体命令类,在新的具体命令类中调用新的功能类的action()方法,而后将该具体命令类经过注入的方式加入到调用者,无需修改原有代码,符合开闭原则。
有时候,当请求发送者发送一个请求时,有不止一个请求接收者产生响应(Qt信号槽,一个信号能够链接多个槽),这些请求接收者将逐个执行业务方法,完成对请求的处理,此时能够用命令队列来实现。好比按钮开关同时控制电灯和风扇,这个例子中,请求发送者是按钮开关,有两个接收者产生响应,分别是电灯和风扇。
能够参考的命令队列的实现方式是增长一个命令队列类(CommandQueue)来存储多个命令对象,不一样命令对象对应不一样的命令接收者。调用者也将面对命令队列类编程,增长注入具体命令队列类对象的方法setCommandQueue(CommandQueue *cmdQueue)。
下面的例子展现了按钮开关请求时,电灯和风扇同时做为请求的接收者。代码以下所示:
#ifdef COMMAND_QUEUE /*************************************/ /* 命令队列 */ #include <vector> // 命令队列类 class CommandQueue { public: CommandQueue(){ } void addCommand(Command *cmd){ commandQueue.push_back(cmd); } void execute(){ for (int i = 0; i < commandQueue.size(); i++) { commandQueue[i]->execute(); } } private: vector<Command*>commandQueue; }; // 调用者 class Button2 { public: Button2(){} // 注入具体命令队列类对象 void setCommandQueue(CommandQueue *cmdQueue){ this->cmdQueue = cmdQueue; } // 发送命令:触摸按钮 void touch(){ printf("触摸开关:"); cmdQueue->execute(); } private: CommandQueue *cmdQueue; }; #endif
客户端代码以下:
#ifdef COMMAND_QUEUE printf("\n\n***********************************\n"); Button2 *button2 = new Button2(); Command *lampCmd2, *fanCmd2; CommandQueue *cmdQueue = new CommandQueue(); // 按钮控制电灯 lampCmd2 = new LampCommand(); cmdQueue->addCommand(lampCmd2); // 按钮控制风扇 fanCmd2 = new FanCommand(); cmdQueue->addCommand(fanCmd2); button2->setCommandQueue(cmdQueue); button2->touch(); #endif
效果以下图:
将历史请求记录保存在日志里,即请求日志。不少软件系统都提供了日志文件,记录运行过程当中的流程。一旦系统发生故障,日志成为了分析问题的关键。日志也能够保存命令队列中的全部命令对象,每执行完一个命令就从日志里删除一个对应的对象。
宏命令又叫组合命令,是组合模式和命令模式的结合。宏命令是一个具体命令类,拥有一个命令集合,命令集合中包含了对其余命令对象的引用。宏命令一般不直接与请求者交互,而是经过它的成员来遍历调用接收者的方法。当调用宏命令的execute()方法时,就遍历执行每个具体命令对象的execute()方法。(相似于前面的命令队列)
优势:
缺点:
适用环境:
欢迎关注知乎专栏:Jungle是一个用Qt的工业Robot
欢迎关注Jungle的微信公众号:Jungle笔记