還記得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.接收者類:電燈和風扇
// 接收者:電燈類
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;
};
3.2.抽象命令類
// 抽象命令類 Command
class Command
{
public:
Command(){}
// 聲明抽象接口:發送命令
virtual void execute() = 0;
private:
Command *command;
};
3.3.具體命令類
// 具體命令類 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;
};
3.3.調用者:Button
// 調用者 Button
class Button
{
public:
Button(){}
// 注入具體命令類對象
void setCommand(Command *cmd){
this->command = cmd;
}
// 發送命令:觸摸按鈕
void touch(){
printf("觸摸開關:");
command->execute();
}
private:
Command *command;
};
3.4.客戶端代碼示例
#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;
}
3.5.效果
可以看到,客戶端只需要有一個調用者和抽象命令類,在給調用者注入命令時,再將命令類具體化(這也就是定義中“可用不同的請求對客戶進行參數化”的體現)。客戶端並不知道命令是如何傳遞和響應,只需發送命令touch()即可,由此實現命令發送者和接收者的解耦。
如果系統中增加了新的功能,功能鍵與新功能對應,只需增加對應的具體命令類,在新的具體命令類中調用新的功能類的action()方法,然後將該具體命令類通過注入的方式加入到調用者,無需修改原有代碼,符合開閉原則。
4.命令隊列
有時候,當請求發送者發送一個請求時,有不止一個請求接收者產生響應(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
效果如下圖:
5.命令模式其他應用
5.1.記錄請求日誌
將歷史請求記錄保存在日誌裏,即請求日誌。很多軟件系統都提供了日誌文件,記錄運行過程中的流程。一旦系統發生故障,日誌成爲了分析問題的關鍵。日誌也可以保存命令隊列中的所有命令對象,每執行完一個命令就從日誌裏刪除一個對應的對象。
5.2.宏命令
宏命令又叫組合命令,是組合模式和命令模式的結合。宏命令是一個具體命令類,擁有一個命令集合,命令集合中包含了對其他命令對象的引用。宏命令通常不直接與請求者交互,而是通過它的成員來遍歷調用接收者的方法。當調用宏命令的execute()方法時,就遍歷執行每一個具體命令對象的execute()方法。(類似於前面的命令隊列)
6.總結
優點:
- 降低系統耦合度,將命令的請求者與接收者分離解耦,請求者和發送者不存在直接關聯,各自獨立互不影響。
- 便於擴展:新的命令很容易加入到系統中,且符合開閉原則。
- 較容易實現命令隊列或宏命令。
- 爲請求的撤銷和回覆操作提供了一種設計實現方案。
缺點:
- 命令模式可能導致系統中有過多的具體命令類,增加了系統中對象的數量。
適用環境:
- 系統需要將請求發送者和接收者解耦,使得發送者和接收者互不影響。
- 系統需要在不同時間指定請求、將請求排隊和執行請求。
- 系統需要支持命令的撤銷和恢復操作。
- 系統需要將一組操作組合在一起形成宏命令。
歡迎關注知乎專欄:Jungle是一個用Qt的工業Robot
歡迎關注Jungle的微信公衆號:Jungle筆記