作爲程序員的你,必須要知道命令模式!

還記得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筆記

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章