趣谈设计模式 | 命令模式(Command):将命令封装为对象
文章目錄
- 案例:智能遙控
- 命令模式
- 應用場景
- 隊列請求
- 日志系統
- 總結
- 完整代碼與文檔
命令模式的應用場景較少,且不易理解,因此我也不好舉例,所以下面的描述可能會存在一些問題,請見諒
案例:智能遙控
小明所在的公司正在研發一個智能遙控APP,可以通過將家電的命令配對到APP上,通過APP我們就能夠遠程的啟動家中的家電,并讓其執行任務。
如果我們直接讓遙控器來要求家電做出某些命令,由于家電的品牌、種類不同,其功能的接口以及實現也各不相同,為我們的設計帶來了巨大的挑戰。
由于不同的產品的命令不一樣,接口也不一樣,如果讓控制器全權負責家電命令的請求、下達、執行,這就要求控制器必須要清楚家電的所有細節,并且需要針對對象編程,一旦我們需要進行拓展,就會需要讓我們的遙控器來適應新的家電。
這種針對細節編程的設計存在大量的缺陷,例如下圖,當存在大量的類時,就會導致控制器的邏輯十分繁雜,并且拓展性、維護性極低。
如果繼續根據品牌、型號進行劃分,又會產生一大堆類型,并且當有新型號產品出現時,我們的拓展也非常麻煩
所以我們不妨換個思路,將命令的執行與調用進行分離,控制器不需要知道這些家電的執行方式,他只需要發出命令的請求,而后讓專門的對象去處理這些命令。
采用這樣的設計,控制器就不再需要了解命令的執行細節,當發起請求命令時,控制器就會調用命令對象的執行方法,然后讓命令對象操縱接收者執行動作。這樣,就將動作的請求者從動作的執行者中進行解耦。
這就是命令模式的核心。
命令模式
命令模式將一個請求(行為)封裝成一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤銷的操作。
命令模式由以下部分組成
- Invoker(調用者):命令的調用者,接收客戶端的請求后通過調用命令對象中的執行方法來下達命令
- Command(命令接口):用來聲明執行命令的接口
- ConcreteCommand(命令對象):綁定了一個接收者,調用接收者的對應操作來完成命令
- Receiver(接收者):命令的接收者,即實際執行命令的人
類圖如下
了解完命令模式之后,我們繼續實現我們的控制器
為了能讓控制器能夠適配多種命令,我們創造出一個命令接口,并讓所有的具體命令都去實現它。并且為了防止我們可能會存在誤操作,我還加入了一個撤回操作的undo方法
class Command { public:virtual ~Command() = default;virtual void execute() = 0; //執行任務virtual void undo() = 0; //撤回任務 };接著為了方便舉例,我們還需要實現一個家電的實例
class SweepingRobot { public:void action(){std::cout << "掃地機器人開始執行清掃計劃!" << std::endl;}void undo(){std::cout << "撤銷掃地機器人的清掃計劃!" << std::endl;} };命令對象會保留一份執行者的實例,通過調用執行者的對應操作來完成命令
class SweepingRobotCommand : public Command { public:SweepingRobotCommand(SweepingRobot* recvive): _recvive(recvive){}void execute() override{_recvive->action();}void undo() override{_recvive->undo();} private:SweepingRobot* _recvive; };下面就實現我們具體的控制器吧,為了方便下達命令,我利用哈希表來將命令語句與具體命令建立映射,當輸入執行的語句后,我們就會自動的去查找哈希表中存在的命令,然后執行它
同時,利用一個棧來保存所有的執行命令,來方便我們進行誤操作的回滾
class Controller { public://保存命令,即我們通常說的“配對”void setCommand(Command* command, std::string type){_commands.insert(make_pair(type, command));}//查找命令是否存在,如果存在則執行void executeCommand(const std::string& type){auto res = _commands.find(type);if(res == _commands.end()){std::cout << "該命令不存在,請檢查輸入" << std::endl;}else{res->second->execute(); //執行命令}}//撤回上一條命令void undoCommand(){//從undo棧中取出上一條命令,并撤回if(!_undo.empty()){Command* command = _undo.top();_undo.pop();command->undo();_undo.push(res->second); //將執行過的命令放入undo棧中}} private:std::stack<Command*> _undo; //用棧來保存執行過的命令,用于進行回退std::unordered_map<std::string, Command*> _commands; //利用哈希來建立起具體命令的映射 };測試代碼
int main() {Controller controller;AirConditioner equipment1;SweepingRobot equipment2;Command* command1 = new AirConditionerCommand(&equipment1); Command* command2 = new SweepingRobotCommand(&equipment2);controller.setCommand(command1, "AirConditioner"); //配對controller.setCommand(command2, "SweepingRobot");controller.executeCommand("AirConditioner"); //啟動空調controller.executeCommand("SweepingRobot"); //啟動掃地機器人controller.undoCommand(); //撤回上一條命令controller.undoCommand(); //撤回上一條命令delete command1, command2;return 0; }應用場景
雖然命令模式在日常的應用、業務編寫中都不是很常見,但這并不意味著它離我們很遙遠,在一些較為底層的設計中還是會存在著它的身影。
隊列請求
上面我提到過,命令模式將發出請求的對象和執行請求的對象進行解耦,這也就意味著任務的發布者不需要知道任務的執行流程以及細節,只需要下達命令,而執行者只需要接收任務并進行執行,不需要了解任務的發布者是誰,兩者之間不存在耦合關系。
說到這里,我們馬上就想到了生產者消費者模型、線程池、工作隊列等應用。工作隊列來說,客戶將命令對象放入隊列中,而另一端的線程從隊列中取出命令對象,并調用命令對象中的execute函數來完成任務,它不在乎對象到底做些什么,它只知道取出命令并調用執行方法,兩者完全解耦。
日志系統
在上面的實現中,我利用一個棧將所有的執行命令保存下來,并提供了undo命令來進行回退,這不就是我們的日志系統嗎?這里就用數據庫的日志系統進行舉例,其會保存我們所有執行的sql語句,并將其寫入日志中,我們可以通過日志來進行反向的undo操作,進行版本的回退。
而我們通常所說的事務,也是這樣實現的。事務要么全都執行,要么全都不執行。當有某個事務執行失敗時,就會查詢日志,并執行其undo操作,來達到版本的回退效果
總結
要點
- 命令模式將發出請求的對象和執行請求的對象解耦,做法是將命令抽象為對象
- 命令支持撤銷,可以通過undo方法來進行回滾
- 調用者通過調用命令對象的執行方法來發出請求,使得接收者執行動作
- 可能會導致存在大量的命令類
應用場景
- 日志系統、事務等需要對命令記錄、撤回、重做等場景
- 工作隊列等需要將命令的發布者和執行者解耦的場景
完整代碼與文檔
如果有需要完整代碼或者markdown文檔的同學可以點擊下面的github鏈接
github
總結
以上是生活随笔為你收集整理的趣谈设计模式 | 命令模式(Command):将命令封装为对象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 趣谈设计模式 | 代理模式(Proxy)
- 下一篇: 趣谈设计模式 | 适配器模式(Adapt