新谈设计模式 Chapter 14 — 命令模式 Command
Chapter 14 — 命令模式 Command
灵魂速记:把请求打包成对象——想存就存,想撤就撤,想排队就排队。
秒懂类比
去餐厅吃饭:
- 你告诉服务员:“来一份宫保鸡丁”
- 服务员把你的要求写在小票上
- 小票传给厨师
- 厨师照着小票做菜
小票就是命令对象。它把"谁要什么"封装起来了。好处:
- 可以排队(先来后到)
- 可以撤销(划掉那道菜)
- 可以记录(账单)
问题引入
// 灾难现场:按钮直接调用业务逻辑classButton{voidonClick(){document.save();// 按钮和业务逻辑紧耦合}};// 问题:// 1. 同样的"保存",菜单栏也要调、快捷键也要调——到处重复// 2. 想加撤销?在每个调用点都加?// 3. 想做宏录制?记录哪些操作?模式结构
┌──────────┐ ┌──────────┐ ┌──────────┐ │ Invoker │─────→│ Command │────→│ Receiver │ │ (服务员) │ 持有 │ (小票) │ 知道│ (厨师) │ ├──────────┤ ├──────────┤ ├──────────┤ │ +invoke()│ │+execute()│ │+action() │ │ │ │+undo() │ │ │ └──────────┘ └──────────┘ └──────────┘四个角色:
- Command:命令接口(
execute+undo) - ConcreteCommand:具体命令,知道找谁干活
- Receiver:真正干活的
- Invoker:触发命令的(按钮、菜单、快捷键……)
C++ 实现
#include<iostream>#include<memory>#include<stack>#include<string>#include<vector>// ========== Receiver:文本编辑器 ==========classTextEditor{public:voidinsertText(size_t pos,conststd::string&text){content_.insert(pos,text);std::cout<<" [插入] \""<<text<<"\" at "<<pos<<"\n";}voiddeleteText(size_t pos,size_t length){autodeleted=content_.substr(pos,length);content_.erase(pos,length);std::cout<<" [删除] \""<<deleted<<"\" at "<<pos<<"\n";}conststd::string&content()const{returncontent_;}private:std::string content_;};// ========== Command 接口 ==========classCommand{public:virtual~Command()=default;virtualvoidexecute()=0;virtualvoidundo()=0;};// ========== 具体命令:插入文本 ==========classInsertCommand:publicCommand{public:InsertCommand(TextEditor&editor,size_t pos,std::string text):editor_(editor),pos_(pos),text_(std::move(text)){}voidexecute()override{editor_.insertText(pos_,text_);}voidundo()override{editor_.deleteText(pos_,text_.size());}private:TextEditor&editor_;size_t pos_;std::string text_;};// ========== 具体命令:删除文本 ==========classDeleteCommand:publicCommand{public:DeleteCommand(TextEditor&editor,size_t pos,size_t length):editor_(editor),pos_(pos),length_(length){}voidexecute()override{deletedText_=editor_.content().substr(pos_,length_);// 先备份editor_.deleteText(pos_,length_);}voidundo()override{editor_.insertText(pos_,deletedText_);// 还原}private:TextEditor&editor_;size_t pos_;size_t length_;std::string deletedText_;// 备份,用于撤销};// ========== Invoker:命令管理器(支持撤销/重做) ==========classCommandManager{public:voidexecute(std::unique_ptr<Command>cmd){cmd->execute();undoStack_.push(std::move(cmd));// 执行新命令后清空重做栈while(!redoStack_.empty())redoStack_.pop();}voidundo(){if(undoStack_.empty()){std::cout<<" (没有可撤销的操作)\n";return;}std::cout<<" ↩️ 撤销:\n";auto&cmd=undoStack_.top();cmd->undo();redoStack_.push(std::move(cmd));undoStack_.pop();}voidredo(){if(redoStack_.empty()){std::cout<<" (没有可重做的操作)\n";return;}std::cout<<" ↪️ 重做:\n";auto&cmd=redoStack_.top();cmd->execute();undoStack_.push(std::move(cmd));redoStack_.pop();}private:std::stack<std::unique_ptr<Command>>undoStack_;std::stack<std::unique_ptr<Command>>redoStack_;};intmain(){TextEditor editor;CommandManager manager;// 输入 "Hello World!"manager.execute(std::make_unique<InsertCommand>(editor,0,"Hello"));std::cout<<" 内容: \""<<editor.content()<<"\"\n\n";manager.execute(std::make_unique<InsertCommand>(editor,5," World!"));std::cout<<" 内容: \""<<editor.content()<<"\"\n\n";// 删除 "World"manager.execute(std::make_unique<DeleteCommand>(editor,6,5));std::cout<<" 内容: \""<<editor.content()<<"\"\n\n";// 撤销!manager.undo();std::cout<<" 内容: \""<<editor.content()<<"\"\n\n";// 再撤销!manager.undo();std::cout<<" 内容: \""<<editor.content()<<"\"\n\n";// 重做!manager.redo();std::cout<<" 内容: \""<<editor.content()<<"\"\n";}输出:
[插入] "Hello" at 0 内容: "Hello" [插入] " World!" at 5 内容: "Hello World!" [删除] "World" at 6 内容: "Hello !" ↩️ 撤销: [插入] "World" at 6 内容: "Hello World!" ↩️ 撤销: [删除] " World!" at 5 内容: "Hello" ↪️ 重做: [插入] " World!" at 5 内容: "Hello World!"什么时候用?
| ✅ 适合 | ❌ 别用 |
|---|---|
| 需要撤销/重做 | 操作简单且不需要撤销 |
| 需要排队/延迟执行操作 | 直接调用就行 |
| 需要记录操作日志 | 不需要审计追踪 |
| 需要宏命令(组合多个操作) | 操作之间无关联 |
| GUI 按钮/菜单/快捷键解耦 | 只有一个调用入口 |
防混淆
Command vs Strategy
| Command | Strategy | |
|---|---|---|
| 目的 | 封装请求/操作 | 封装算法 |
| 核心动词 | “做这件事” | “用这种方式做” |
| 可撤销? | ✅ 天然支持 undo | ❌ 一般不支持 |
| 存储 | 经常入队列/入栈 | 运行时替换 |
| 类比 | 小票(记录要做什么) | 导航策略(走哪条路) |
一句话分清:Command 是名词(一个请求),Strategy 是副词(用什么方式)。
Command vs Memento
| Command | Memento | |
|---|---|---|
| 撤销方式 | 反向操作(insert→delete) | 恢复快照(整体还原) |
| 存储什么 | 操作本身 | 对象的状态 |
| 适用 | 操作可逆时 | 操作不可逆、或状态复杂时 |
现代 C++ 小贴士
简单场景下,std::function就是天然的命令对象:
#include<functional>#include<vector>std::vector<std::function<void()>>commandQueue;// 入队commandQueue.push_back([&editor]{editor.insertText(0,"Hi");});commandQueue.push_back([&editor]{editor.deleteText(0,2);});// 批量执行for(auto&cmd:commandQueue)cmd();但如果需要 undo,还是老老实实用类。
