精读C++20设计模式——行为型设计模式:命令模式
精读C++20设计模式——行为型设计模式:命令模式
前言:Lets Command!
Command设计模式实际上不太Command。这个比较反直觉。因为Command设计模式压根就不是直接死命令对象到底怎么做事情。而是发送命令,接收对象根据发送者发送的命令执行代码。
命令模式干什么
命令模式将我们对API的操作封装成命令,命令的调用就是对对象的操作。看起来好像没什么问题,比如说:
class BankAccount {int balance = 0;constexpr int overdraft_limit = -500;
public:void deposit(int amount) { balance += amount; }void withdraw(int amount) { if(balance - amount < overdraft_limit) return;blance -= amount;}
};
我们把类完成了!现在我们可以这样做了。
struct Command { virtual void call() const = 0; };struct BankAccountCmd : Command
{BankAccount& ba;enum class AcceptableAction { deposit, withdraw } action;int amount;// BankAccountCmd init omittedvoid call() const override {switch(action){case deposit:// process all the deposit relativebreak;case withdraw:// process all the withdraw relativebreak;} };
};
你看到了嘛?我们现在立马就可以无任何侵入的做比直接调用显然更多的事情了:
BankAccount ba;
BankAccountCmd bacmd {ba, AcceptableAction::deposit, 500};
bacmd.call();
完事。你发现我们完全可以对Command做额外的提交约束啊等一系列的事情,完全不用动BankAccount的任何代码——只要他自己相关的接口是稳定的!
我们甚至还可以滚动回来!比如说我们小小的翻新一下Command(假设我们真的笃定Command是要支持撤回的)
struct Command { virtual void call() = 0; virtual void undo() = 0;
};
现在我们自然可以根据我们实现的逻辑的撤回操作依次的完成Command的接口,最后我们就会组合成一个非常具备代码整洁的命令链条——还是支持撤回操作的那种!
组合我们的命令:结合组合模式+命令模式
一堆Command的有机组合是不是也是一个Command,或者说Command的正交组合显然还是一个Command。那么,我们就有理由编写出一个更好的组合Command
struct ComposedBankCommand : Command
{// register commandsvoid call() override {for(auto& cmd : composers) cmd->call();};void undo() override {for(auto& cmd : composers) cmd->call();}private:vector<Command*> composers;
};
但好像不对?如果我们中间的一个command失败了,其他的干脆就不应该调用——当然对于并行式的Command蔟完全没问题。这个是逻辑设计的差异。解决这个的办法也很好说——Command内部维护一个是否成功的操作就好了嘛!
总结:
解决什么问题
命令模式要解决的核心问题是把“动作”从调用者中剥离出来,使动作成为可传递、可存储、可组合、可撤销的对象。当程序中出现需要延迟执行、排队执行、撤销/重做、日志回放、网络传输或组合多个操作等需求时,直接在调用处硬编码调用逻辑会导致耦合、难以扩展与难以控制。命令模式把对某个接口/对象的“操作”封装成单独的对象(Command),从而把请求者与执行者解耦,同时把操作本身作为一等公民来处理(队列、日志、回滚、组合……都可以做)。
如何解决
命令模式通过定义一个统一的命令接口(比如 call()
/ execute()
,必要时还加上 undo()
/ redo()
)来表示“要做的事”。每个具体命令包含执行该操作所需的接收者引用和参数。调用方只负责生成或提交命令对象,不直接操作接收者;命令可以被放入队列、写入日志、传到远端、组合成宏命令或在稍后执行。为支持撤销/补偿,命令可以维护执行前的状态(或借助 Memento),或者提供一个 undo()
方法做反向操作。为了支持异步、可靠性或回放,还可以给命令增加序列化/日志化能力。
各个变种的优劣对比
变种 | 描述 | 优点 | 缺点 | 典型适用场景 |
---|---|---|---|---|
基本命令(Basic Command) | 最简形式:命令对象封装接收者和参数,实现 execute() 。 | 实现简单,耦合低,便于扩展与测试。 | 只适合同步、一次性调用,缺少撤销/持久化支持。 | 简单的解耦、延迟执行需求。 |
可撤销命令(Undoable Command) | 命令实现 execute() 与 undo() 或保存 Memento 做回滚。 | 支持撤销/重做,用户体验好(编辑器、事务界面)。 | 需要额外保存状态或实现反向操作,设计与边界条件复杂。 | 文本编辑、图形编辑、事务局部回退。 |
组合命令 / 宏命令(Composite / Macro) | 把多个命令组合成一个命令;支持原子或顺序执行。 | 易于复用、批量操作、一次执行多个子操作。 | 失败处理复杂(部分成功如何回滚);错误传播要设计清楚。 | 批处理、事务化工作流、复合操作。 |