🌟 核心思想
命令模式将请求封装成对象,使你可以:
-
参数化客户端的不同请求
-
将请求排队或记录日志
-
支持可撤销的操作
就像现实中的遥控器,每个按钮对应一个封装好的命令,不需要知道具体实现。
🎛 现实世界类比:餐厅点餐
-
顾客(Client):创建命令对象(点单)
-
服务员(Invoker):接收并传递命令(订单)
-
厨师(Receiver):知道如何执行请求(做菜)
-
订单(Command):封装了具体的请求
🏗 四大核心角色
角色 | 作用 | 餐厅示例 |
---|---|---|
Command(命令) | 声明执行操作的接口 | 订单接口 |
ConcreteCommand(具体命令) | 绑定Receiver和动作 | 具体菜品订单 |
Receiver(接收者) | 知道如何执行操作 | 厨师 |
Invoker(调用者) | 触发命令执行 | 服务员 |
Client(客户端) | 创建具体命令 | 顾客 |
📜 代码示例(Java实现)
1. 定义命令接口和具体命令
java
复制
// 命令接口 interface Command { void execute(); void undo(); // 支持撤销 } // 具体命令:做披萨 class PizzaOrder implements Command { private Chef receiver; private String pizzaType; public PizzaOrder(Chef receiver, String pizzaType) { this.receiver = receiver; this.pizzaType = pizzaType; } public void execute() { receiver.makePizza(pizzaType); } public void undo() { receiver.cancelPizza(pizzaType); } } // 具体命令:做意面 class PastaOrder implements Command { private Chef receiver; private String pastaType; public PastaOrder(Chef receiver, String pastaType) { this.receiver = receiver; this.pastaType = pastaType; } public void execute() { receiver.makePasta(pastaType); } public void undo() { receiver.cancelPasta(pastaType); } }
2. 定义接收者(厨师)
java
复制
// 接收者:知道如何执行操作 class Chef { public void makePizza(String type) { System.out.println("制作" + type + "披萨"); } public void cancelPizza(String type) { System.out.println("取消" + type + "披萨订单"); } public void makePasta(String type) { System.out.println("制作" + type + "意面"); } public void cancelPasta(String type) { System.out.println("取消" + type + "意面订单"); } }
3. 定义调用者(服务员)
java
复制
import java.util.Stack; // 调用者:触发命令执行 class Waiter { private Stack<Command> history = new Stack<>(); public void takeOrder(Command command) { command.execute(); history.push(command); // 记录历史用于撤销 } public void cancelLastOrder() { if (!history.isEmpty()) { Command last = history.pop(); last.undo(); } } }
4. 客户端使用
java
复制
public class Restaurant { public static void main(String[] args) { Chef chef = new Chef(); // 接收者 // 创建具体命令 Command pizza = new PizzaOrder(chef, "海鲜"); Command pasta = new PastaOrder(chef, "番茄肉酱"); Waiter waiter = new Waiter(); // 调用者 // 点餐执行命令 waiter.takeOrder(pizza); waiter.takeOrder(pasta); System.out.println("\n--- 取消最后一个订单 ---"); waiter.cancelLastOrder(); } }
输出结果:
复制
制作海鲜披萨 制作番茄肉酱意面 --- 取消最后一个订单 --- 取消番茄肉酱意面订单
✅ 优点
✔ 解耦调用者和接收者:调用者无需知道具体实现
✔ 可扩展性强:新增命令不影响现有代码
✔ 支持撤销/重做:维护命令历史记录
✔ 支持宏命令:组合多个命令为一个
❌ 缺点
✖ 可能产生大量命令类:每个操作都需要一个类
✖ 增加系统复杂度:多层间接调用
🎯 适用场景
-
需要回调机制的系统
-
需要实现撤销/重做功能
-
需要将操作排队、记录或远程执行
-
需要支持事务的系统
典型应用:
-
图形界面菜单/按钮
-
游戏控制指令
-
数据库事务
-
智能家居遥控系统
🔄 对比其他模式
模式 | 目的 | 关键区别 |
---|---|---|
命令模式 | 封装请求为对象 | 强调请求的封装与执行分离 |
策略模式 | 封装算法 | 关注算法替换,通常无接收者 |
职责链模式 | 处理请求链 | 多个对象尝试处理同一请求 |
模式进阶技巧
-
组合命令:创建宏命令(一次执行多个命令)
-
命令队列:实现异步命令执行
-
日志记录:持久化命令序列用于恢复
-
事务处理:实现原子操作(全部成功或全部回滚)
总结
命令模式就像万能遥控器,将各种操作封装成统一接口的命令对象,带来三大超能力:
-
一键操作:隐藏复杂实现细节
-
时光倒流:轻松实现撤销功能
-
批处理:支持命令组合和队列
💡 设计箴言:"把请求变成对象,让控制更加灵活" —— 命令模式精髓
Invoker(调用者)与 Client(客户端)的关系解析
在命令模式中,Invoker 和 Client 是两个关键但角色不同的参与者,它们的关系可以用"餐厅后厨系统"来形象理解:
🍽️ 形象比喻
-
Client(顾客):点餐的人(知道想吃什么,但不知道怎么做)
-
Invoker(服务员):传菜员(不关心菜怎么做,只负责触发命令传递)
-
Command(订单):记录客户需求的单据
-
Receiver(厨师):实际执行操作的人
🔍 核心关系特点
维度 | Invoker | Client | 二者关系 |
---|---|---|---|
职责 | 触发命令执行 | 创建并配置命令对象 | Client组装命令,Invoker执行命令 |
知情权 | 不知道命令具体实现 | 知道Receiver和具体命令 | Client是"知情者",Invoker是"盲执行者" |
交互方式 | 持有命令接口引用 | 创建具体命令实例 | Client将具体命令注入Invoker |
类比 | 遥控器按钮 | 编程遥控器的人 | 就像给遥控器编程的人(Client)和实际按按钮的人(Invoker)的关系 |
💻 代码层面关系
java
复制
// Client的典型操作: Receiver chef = new Chef(); // 1. 创建接收者 Command cmd = new PizzaOrder(chef, "海鲜"); // 2. 创建具体命令 Invoker waiter = new Waiter(); // 3. 创建调用者 waiter.setCommand(cmd); // 4. Client将命令装配给Invoker waiter.executeCommand(); // 5. Invoker执行命令(不知道具体是什么命令)
⚡ 交互流程图
复制
Client → 创建Receiver → 创建ConcreteCommand → 绑定到Invoker → Invoker.execute() (知情者) (组装命令) (注入依赖) (盲执行)
🌟 关键设计原则体现
-
单一职责原则:
-
Client专注命令装配
-
Invoker专注命令触发
-
-
松耦合:
-
Invoker只依赖Command抽象接口
-
二者通过命令对象间接通信
-
-
控制反转:
-
Client控制流程(什么时候创建/绑定命令)
-
Invoker被注入命令后执行
-
🛠️ 实际应用场景
-
GUI按钮系统:
-
Client:界面构建代码
-
Invoker:按钮组件
-
Command:点击操作
-
-
游戏控制:
-
Client:游戏初始化代码
-
Invoker:输入处理器
-
Command:游戏指令
-
-
事务系统:
-
Client:事务组装代码
-
Invoker:事务执行引擎
-
Command:子操作命令
-
⚠️ 常见误区
-
错误认知:认为Invoker需要知道具体命令类型
-
实际上Invoker应该只依赖Command接口
-
-
过度设计:简单场景可能不需要严格区分二者
-
当Client本身就能直接调用Receiver时,可能不需要完整命令模式
-
-
生命周期混淆:
-
Client通常只在初始化阶段活跃
-
Invoker在运行时持续接收触发
-
这种精妙的职责分离正是命令模式强大灵活性的核心所在!