從C++編程入手設計模式——命令模式
? 命令模式是一個用指令封裝請求的優雅方法。換而言之,對于一個復雜的系統,當我們發現,使用一系列的指令(Command)來操作對象的時候,這個設計模式就會顯得非常的實用。我們經常遇到這樣的場景:用戶點擊一個按鈕,希望執行某個操作,比如保存文件、刪除一條記錄、或撤銷上一步操作。我們通常會寫一段代碼來直接響應這個按鈕事件。然而,如果我們希望這個操作是可以記錄、撤銷、重做,甚至延遲執行的,傳統的方式就變得笨拙而混亂。
? 這個時候,命令模式(Command Pattern)就派上用場了。它的核心思想很簡單:把每一個操作封裝成一個對象。這樣,我們就可以把操作當作數據一樣存儲、傳遞、撤銷甚至組合。命令模式中通常有幾個角色:命令對象(Command)、接收者(Receiver)、調用者(Invoker)和客戶端。命令對象負責封裝“做什么”;接收者是實際干活的人;調用者是觸發命令的人;客戶端負責把它們組合在一起。
? 我們可以想象一個遙控器控制家電的例子。電燈是接收者,它有開和關的功能;開燈命令和關燈命令是命令對象;遙控器就是調用者,它保存著按鈕和命令的映射;用戶就是客戶端,負責把命令裝進遙控器并按下按鈕。
#include <iostream>
#include <memory>
#include <vector>// 命令接口
class Command {
public:virtual void execute() = 0;virtual ~Command() = default;
};// 接收者
class Receiver {
public:void action() {std::cout << "Receiver: Executing action.\n";}
};// 具體命令
class ConcreteCommand : public Command {
private:std::shared_ptr<Receiver> receiver;
public:explicit ConcreteCommand(std::shared_ptr<Receiver> r) : receiver(std::move(r)) {}void execute() override {receiver->action();}
};// 調用者
class Invoker {
private:std::vector<std::shared_ptr<Command>> commandQueue;
public:void addCommand(std::shared_ptr<Command> cmd) {commandQueue.push_back(std::move(cmd));}void run() {for (const auto& cmd : commandQueue) {cmd->execute();}commandQueue.clear();}
};int main() {auto receiver = std::make_shared<Receiver>();auto command = std::make_shared<ConcreteCommand>(receiver);Invoker invoker;invoker.addCommand(command);invoker.run();return 0;
}
? 如你所見,這個就是常見的命令模式的模板代碼。
優缺點討論
命令模式的一個好處是,它實現了調用者和接收者之間的徹底解耦。調用者并不知道命令怎么執行,它只知道有個命令對象可以被觸發。這使得我們可以在不改變調用者的前提下,輕松替換命令,甚至在運行時動態改變行為。
此外,命令模式還為我們帶來了其他附加功能。比如,可以將命令記錄到列表中,支持撤銷和重做;可以組合多個命令形成宏命令,實現批量操作;可以把命令序列化之后發到遠程服務器執行,實現遠程控制。
不過命令模式也有它的代價。每個操作都要定義一個命令類,當操作很多時,類的數量會迅速增加。此外,為了支持撤銷,命令對象還需要記錄足夠的信息,這可能會帶來額外的內存負擔。
練習題:通過命令控制文本編輯器
描述:
模擬一個文本編輯器,用戶可以執行“添加文字”、“刪除文字”等操作,每個操作為一個命令。
要求:
- 接收者
TextDocument
,支持添加和刪除字符 - 命令:
AddTextCommand
,DeleteTextCommand
- 支持撤銷操作
- Invoker 為
EditorInvoker
,負責管理執行歷史
modern-cpp-patterns-playground/Command/TextEditor at main · Charliechen114514/modern-cpp-patterns-playground