命令模式:把请求变成可以保存和撤销的对象
点击按钮、操作队列、撤销重做、任务调度,都要求“动作”本身能像数据一样被保存和传递。命令模式把动作对象化。
一、痛点:调用者直接绑死接收者
一个智能家居系统里,用户可能通过面板、语音、定时任务三种方式执行“开灯”“关灯”“调节温度”。如果每种入口都直接调用设备方法:
if("light-on".equals(action)){light.turnOn();}elseif("ac-up".equals(action)){ac.increaseTemperature();}调用者和设备实现强耦合,也没法支持撤销、排队、日志回放。
命令模式的回答:把每个动作封装成 Command 对象,调用者只向命令对象发出“执行”,不关心谁真正干活。
二、实现:命令接口 → 具体命令 → 调用者 → 接收者
第一步:定义命令接口
publicinterfaceCommand{voidexecute();voidundo();}第二步:定义接收者
publicclassLight{privatebooleanon;publicvoidturnOn(){this.on=true;System.out.println("[灯] 已打开");}publicvoidturnOff(){this.on=false;System.out.println("[灯] 已关闭");}}第三步:实现具体命令
publicclassLightOnCommandimplementsCommand{privatefinalLightlight;publicLightOnCommand(Lightlight){this.light=light;}@Overridepublicvoidexecute(){light.turnOn();}@Overridepublicvoidundo(){light.turnOff();}}publicclassLightOffCommandimplementsCommand{privatefinalLightlight;publicLightOffCommand(Lightlight){this.light=light;}@Overridepublicvoidexecute(){light.turnOff();}@Overridepublicvoidundo(){light.turnOn();}}第四步:调用者与历史记录
importjava.util.ArrayDeque;importjava.util.Deque;publicclassRemoteController{privatefinalDeque<Command>history=newArrayDeque<>();publicvoidpress(Commandcommand){command.execute();history.push(command);}publicvoidundo(){if(!history.isEmpty()){history.pop().undo();}}}第五步:不同入口复用同一套命令
Lightlight=newLight();RemoteControllercontroller=newRemoteController();controller.press(newLightOnCommand(light));controller.press(newLightOffCommand(light));controller.undo();// 重新开灯面板、语音、定时任务都只是创建同一个LightOnCommand的入口,设备逻辑只存在一处。
三、命令模式解决的四类问题
| 问题 | 命令模式的解法 |
|---|---|
| 调用者与执行者耦合 | 调用者只依赖 Command 接口 |
| 需要撤销/重做 | 命令对象保存历史,并可执行 undo |
| 需要延迟执行或定时执行 | 命令对象可以被排队、持久化、定时触发 |
| 需要记录操作日志 | 每个命令天然是日志条目 |
四、工程化提醒
- 只有动作需要被管理时才引入命令模式:普通业务方法直接调用即可,别把所有调用都包装成 Command;
- 命令粒度要合适:一个命令最好对应一个可撤销的业务动作,太细会类爆炸,太粗会难回滚;
- 与真实队列配合:生产上可将 Command 序列化到消息队列,实现任务解耦和重试。
小结
命令模式一句话:把动作封装成对象,让动作可保存、可排队、可撤销。它解耦调用者与接收者,也为复杂操作管理提供了统一入口。