命令模式-Command Pattern
序言
相信大家都有过点外卖的经历:你只需要在 App 上选择菜品、下单,后厨就会收到一份"订单",厨师照着订单做菜,外卖小哥把做好的餐送到你手上。在整个过程中,你(调用方)并不知道后厨有多少位厨师、厨师是怎么做菜的、菜品用了什么食材。你发出的"订单"就是一个被封装起来的请求。
命令模式(Command Pattern)的思想与此类似:把一次"请求"或"动作"封装成一个独立的对象,调用方只需要"发出命令",至于命令由谁执行、如何执行,调用方一概不知。
这种模式最常见的现实类比是遥控器:每个按钮就是一个"命令",按下按钮后,电视、空调、灯光各自执行自己的动作,而遥控器本身完全不需要知道这些设备内部的工作原理。
定义
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
将一个请求封装成一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
把请求封装成对象之后,我们可以:
- 将请求作为方法的参数传递(参数化客户端);
- 将请求放入队列,按顺序依次执行(请求排队);
- 记录请求日志,便于回溯(请求日志);
- 记录已执行的命令,从而支持撤销(undoable operations)。
这就是命令模式的核心价值:请求的发出者(Invoker)和请求的执行者(Receiver)被彻底解耦。
结构
让我们从一个问题开始
假设我们正在开发一个智能家居控制系统,需要控制家里的灯光和电视。最直觉的写法是把所有控制逻辑堆在遥控器类中:
public class RemoteControl {
private Light light;
private TV tv;
// 按下按钮,通过大量的 if 判断来执行不同的动作
public void onButtonPressed(int button) {
if (button == 1) {
light.on();
} else if (button == 2) {
tv.on();
} else if (button == 3) {
tv.changeChannel(5);
}
// 每增加一个新设备,就要在这里增加一个 if 分支
// 每增加一个新的功能,同样要在这里增加一个 if 分支
// ...
}
}
这样写有几个明显的问题:
RemoteControl与Light、TV等具体类强耦合,而且认识所有设备的内部方法;- 每增加一个设备或功能,就要修改
RemoteControl,违反开闭原则; - 无法实现"撤销":当按下"打开灯光"后,要撤销成什么状态?
RemoteControl根本无从得知; - 无法将"请求"排队、记录日志,或者延迟执行。
那有没有一种方式,让 RemoteControl 只负责"按下按钮",而不用关心按钮背后到底做了什么?这就是命令模式要解决的问题。
命令模式的结构
命令模式由以下几个角色组成:
各个角色的职责如下:
- Command(命令):声明了执行操作的接口,通常包含
execute()方法,我们这里还增加了undo()用于撤销。 - ConcreteCommand(具体命令):绑定了一个接收者对象和动作,调用接收者的方法来完成具体的操作。例如
LightOnCommand绑定Light,执行时调用light.on()。 - Receiver(接收者):真正执行业务逻辑的对象,如
Light、TV。 - Invoker(调用者):持有命令对象并在某个时机触发命令的执行,如
RemoteControl。它不需要知道命令的具体实现。 - Client(客户端):创建具体命令对象并设置其接收者,然后把命令交给调用者。
代码实现
我们来实现上面提到的智能家居遥控器。
定义命令接口
public interface Command {
void execute();
// 支持撤销操作
void undo();
}
定义接收者
public class Light {
private boolean on = false;
public void on() {
this.on = true;
System.out.println("灯光已打开");
}
public void off() {
this.on = false;
System.out.println("灯光已关闭");
}
public boolean isOn() {
return on;
}
}
public class TV {
private int channel = 1;
public void on() {
System.out.println("电视已打开,当前频道:" + channel);
}
public void off() {
System.out.println("电视已关闭");
}
public void changeChannel(int channel) {
System.out.println("电视切换到频道:" + channel);
this.channel = channel;
}
public int getChannel() {
return channel;
}
}
定义具体命令
每个命令都绑定一个接收者,execute() 调用接收者的方法,undo() 执行反向操作:
public class LightOnCommand implements Command {
private final Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
private final Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
对于"切换频道"这种需要记住历史状态的操作,我们可以在命令内部保存上一个状态,从而实现撤销:
public class TVChangeChannelCommand implements Command {
private final TV tv;
private final int channel;
private int lastChannel;
public TVChangeChannelCommand(TV tv, int channel) {
this.tv = tv;
this.channel = channel;
}
@Override
public void execute() {
// 记录执行前的频道,用于撤销
this.lastChannel = tv.getChannel();
tv.changeChannel(channel);
}
@Override
public void undo() {
tv.changeChannel(lastChannel);
}
}
电视的开与关也是一对命令,逻辑与灯光命令一致:
public class TVOnCommand implements Command {
private final TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.on();
}
@Override
public void undo() {
tv.off();
}
}
public class TVOffCommand implements Command {
private final TV tv;
public TVOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.off();
}
@Override
public void undo() {
tv.on();
}
}
定义调用者(遥控器)
RemoteControl 持有命令对象,用户按下按钮时触发命令执行,同时记录上一条命令以便撤销:
public class RemoteControl {
private final Command[] onCommands;
private final Command[] offCommands;
private Command lastCommand;
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
// 用"空命令"填充所有插槽,避免空指针判断
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
lastCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void pressOn(int slot) {
onCommands[slot].execute();
lastCommand = onCommands[slot];
}
public void pressOff(int slot) {
offCommands[slot].execute();
lastCommand = offCommands[slot];
}
public void pressUndo() {
lastCommand.undo();
}
}
这里我们用到了一个 NoCommand,它是一个"空对象",什么都不做。这样可以避免在遥控器初始化时对每个插槽做空值判断:
public class NoCommand implements Command {
@Override
public void execute() {
// 什么都不做
}
@Override
public void undo() {
// 什么都不做
}
}
NoCommand 正是**空对象模式(Null Object Pattern)**的典型应用,在行为型模式中我们会单独介绍它。
客户端使用
public class Main {
public static void main(String[] args) {
// 接收者:家里的设备
Light livingRoomLight = new Light();
TV tv = new TV();
// 具体命令:把设备动作封装成命令
Command lightOn = new LightOnCommand(livingRoomLight);
Command lightOff = new LightOffCommand(livingRoomLight);
Command tvOn = new TVOnCommand(tv);
Command tvOff = new TVOffCommand(tv);
Command tvToChannel5 = new TVChangeChannelCommand(tv, 5);
// 调用者:遥控器,绑定命令到插槽
RemoteControl remote = new RemoteControl();
remote.setCommand(0, lightOn, lightOff);
remote.setCommand(1, tvOn, tvOff);
// 使用遥控器
System.out.println("=== 按下插槽0的开按钮 ===");
remote.pressOn(0);
System.out.println("=== 按下插槽1的开按钮 ===");
remote.pressOn(1);
System.out.println("=== 按下插槽0的开按钮(再开一次) ===");
remote.pressOn(0);
System.out.println("=== 执行一次性命令:电视切到5频道 ===");
tvToChannel5.execute();
System.out.println("=== 撤销上一次操作 ===");
remote.pressUndo();
System.out.println("=== 再次撤销 ===");
remote.pressUndo();
}
}
输出结果:
=== 按下插槽0的开按钮 ===
灯光已打开
=== 按下插槽1的开按钮 ===
电视已打开,当前频道:1
=== 按下插槽0的开按钮(再开一次) ===
灯光已打开
=== 执行一次性命令:电视切到5频道 ===
电视切换到频道:5
=== 撤销上一次操作 ===
电视切换到频道:1
=== 再次撤销 ===
灯光已关闭
可以看到,RemoteControl 自始至终都没有依赖 Light、TV 这些具体的类,它只和 Command 接口打交道。新增加一个设备(比如空调),只需要新增一个接收者类和几个具体命令类,RemoteControl 一行代码都不用改——这就是命令模式带来的解耦与扩展性。
在开源框架中的应用
Java Swing:Action 接口
Java Swing 中,javax.swing.Action 就是命令模式的典型应用。Action 继承了 ActionListener,把"动作"封装成对象,同时携带图标、文本、是否可用 等属性:
public interface Action extends ActionListener {
// 一些属性名常量,如 NAME、SHORT_DESCRIPTION、SMALL_ICON 等
public static final String DEFAULT = "Default";
public Object getValue(String key);
public void putValue(String key, Object value);
public void setEnabled(boolean b);
public boolean isEnabled();
...
}
AbstractAction 提供了默认实现,我们自定义命令时只需要继承它:
public class SaveAction extends AbstractAction {
public SaveAction() {
// 通过 putValue 设置按钮的显示文本、图标、提示信息
putValue(NAME, "保存");
putValue(SMALL_ICON, new ImageIcon("save.png"));
putValue(SHORT_DESCRIPTION, "保存当前文件");
}
@Override
public void actionPerformed(ActionEvent e) {
// 执行真正的保存逻辑
saveFile();
}
private void saveFile() {
System.out.println("文件已保存");
}
}
一个 Action 对象可以被多个按钮、菜单项共用,当 setEnabled(false) 时,所有关联的按钮和菜单项会同步变灰——这正是命令对象被"参数化"与"复用"的体现。
Java UndoManager:源码视角的命令栈管理
Swing 的 javax.swing.undo.UndoManager 是命令模式 + 备忘录模式在 JDK 源码中的标准结合。整个 javax.swing.undo 包以"命令"为核心建模:UndoableEdit 就是命令接口,AbstractUndoableEdit 是命令的默认实现,CompoundEdit 是复合命令,UndoManager 是命令的管理者(调用者)。
命令接口:UndoableEdit
// javax.swing.undo.UndoableEdit(源码)
public interface UndoableEdit {
// 撤销这个编辑
public void undo() throws CannotUndoException;
// 重做这个编辑
public void redo() throws CannotRedoException;
// 该命令是否可以撤销/重做
public boolean canUndo();
public boolean canRedo();
// 尝试把新编辑与当前编辑合并(如连续输入字符合并为一个编辑)
public boolean addEdit(UndoableEdit anEdit);
// 用新编辑替换当前编辑
public boolean replaceEdit(UndoableEdit anEdit);
...
}
命令管理者:UndoManager 的命令栈
UndoManager 内部用一个 Vector 保存已执行的命令,用 indexOfNextAdd 记录"下一次添加命令的位置"(也是撤销/重做的游标):
// javax.swing.undo.UndoManager(源码骨架)
public class UndoManager extends AbstractUndoableEdit implements UndoableEditListener {
// 命令栈:保存所有已执行的命令(UndoableEdit)
protected Vector<UndoableEdit> edits = new Vector<>();
// 下一次添加命令的位置;也是撤销游标
protected int indexOfNextAdd = 0;
// 添加一条命令:如果存在 redo 分支则先丢弃,再插入命令栈
public synchronized boolean addEdit(UndoableEdit anEdit) {
// 丢弃游标之后的所有命令(新命令会让 redo 分支失效)
trimEdits(indexOfNextAdd, edits.size() - 1);
// 尝试与上一条命令合并(例如连续键入合并为一次"键入"命令)
if (indexOfNextAdd > 0) {
UndoableEdit lastEdit = edits.elementAt(indexOfNextAdd - 1);
if (lastEdit.addEdit(anEdit)) {
return true; // 合并成功,不新增命令
}
anEdit.replaceEdit(lastEdit);
}
edits.add(anEdit); // 入栈
indexOfNextAdd++;
return true;
}
// 撤销:游标前移,调用对应命令的 undo()
public synchronized void undo() {
UndoableEdit edit = edits.elementAt(--indexOfNextAdd);
edit.undo();
}
// 重做:调用游标处命令的 redo(),游标后移
public synchronized void redo() {
UndoableEdit edit = edits.elementAt(indexOfAdd++);
edit.redo();
}
public synchronized boolean canUndo() {
return indexOfNextAdd > 0;
}
...
}
这段源码清楚地展示了命令模式的三大价值:
- 操作被封装成命令对象:每次编辑都是
UndoableEdit实例,被存进edits栈——没有命令对象就无法"把操作排进队列"; - 调用者统一管理:
UndoManager作为调用者,只知道对UndoableEdit调undo()/redo(),完全不关心命令内部的业务(是"插入文本"还是"删除文件"); - 命令支持合并与替换:
addEdit/replaceEdit让相邻命令可以合并,这正是"把请求参数化为对象"后带来的额外能力。
复合命令:CompoundEdit
CompoundEdit 本身也是一个 UndoableEdit(命令),它内部聚合一组子命令,end() 之后对外表现为一条可撤销的命令,undo() 时按逆序撤销全部子命令:
// javax.swing.undo.CompoundEdit(源码骨架)
public class CompoundEdit extends AbstractUndoableEdit {
protected Vector<UndoableEdit> edits = new Vector<>();
private boolean inProgress = true; // 收集子命令期间为 true
// 收集阶段:把子命令加入复合命令
public void addEdit(UndoableEdit anEdit) {
if (inProgress) {
edits.addElement(anEdit);
}
}
// 结束收集:复合命令"定型"
public void end() {
if (inProgress) {
inProgress = false;
// 尝试合并子命令……
}
}
// 撤销复合命令:逆序遍历子命令 逐个撤销
public void undo() {
super.undo();
int i = edits.size();
while (i-- > 0) {
UndoableEdit edit = edits.elementAt(i);
edit.undo();
}
}
}
这是**宏命令(Macro Command)**的标准实现:"一次选中多行删除"这种复合操作,在编辑器内部被拆成多个原子命令收集进 CompoundEdit,用户按一次撤销,全部原子操作一起撤销。
命令模式为 UndoManager 提供了基础:只有把"操作"封装成对象,才能把操作放进栈中、记录状态、批量撤销/重做。
Flowable:把命令模式用到极致的工作流引擎
如果说哪个开源框架把命令模式贯彻得最彻底,那一定是工作流引擎 Flowable(以及它的前身 Activiti)。Flowable 的官方架构文档开篇就写道:
Every API method is transformed into a Command instance. This Command instance is passed into the CommandExecutor which, amongst others, makes it pass a stack of CommandInterceptors. These interceptors have various responsibilities, including handling transactions.
(每个 API 方法都会被转换为一个 Command 实例。该 Command 实例被传递给 CommandExecutor,后者会让它通过一系列 CommandInterceptors。这些拦截器有着各种职责,包括处理事务。)
也就是说,我们在业务代码里调用的每一个引擎 API——runtimeService.startProcessInstanceByKey(...)、taskService.complete(...)、repositoryService.deploy(...)——在引擎内部都会被包装成一个具 体的 Command 对象,然后交给 CommandExecutor 执行。
Flowable 命令模式的结构
Command<T>:命令接口,只声明一个execute(CommandContext): T方法,所有引擎操作都实现该接口;StartProcessInstanceCmd、CompleteTaskCmd等:引擎内置的具体命令类,位于org.flowable.engine.impl.cmd包下,分别对应"发起流程实例""完成任务"等操作;CommandExecutor:命令执行器,负责把命令对象交给拦截器链执行;CommandInterceptor:命令拦截器接口,多个拦截器首尾相连组成一条拦截器链——这正是命令模式与责任链模式结合的最佳范例;CommandInvoker:拦截器链的末端节点,真正调用command.execute(commandContext);CommandContext:命令上下文,承载本次命令执行的数据库会话、变量作用域等运行期信息,由链上的拦截器创建并在命令间传递。
自定义命令:把业务包装进引擎
引擎不仅自己使用命令模式,还允许我们把自定义业务逻辑包装成命令,通过 ManagementService#executeCommand 提交给引擎执行,从而让自定义代码自动获得拦截器链提供的横切能力(事务、日志等):
import org.flowable.common.engine.api.management.ManagementService;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
public class CustomQueryCommand implements Command<List<CustomTask>> {
@Override
public List<CustomTask> execute(CommandContext commandContext) {
// 在命令上下文中执行数据库查询,共享引擎事务
return commandContext.getDbSqlSession()
.selectList("selectCustomTasks", new HashMap<>());
}
}
客户端提交命令:
ManagementService managementService = processEngine.getManagementService();
// 把自定义命令交给引擎执行,自动获得事务、日志等拦截器能力
List<CustomTask> tasks = managementService.executeCommand(new CustomQueryCommand());
拦截器链:命令模式 × 责任链
Flowable 的 CommandExecutor 内部维护一条拦截器链,典型的链形如下:
LogInterceptor -> TransactionInterceptor -> ... -> CommandInvoker
- LogInterceptor(日志拦截器):负责记录命令的执行,方便追踪引擎行为;
- TransactionInterceptor(事务拦截器):负责开启、提交、回滚事务,保证命令的原子性;
- CommandInvoker(命令调用器):链条末端,真正调用命令的
execute(); - 此外还有命令计数器等拦截器,用于引擎的监控与统计。
这样设计的好处:命令(业务逻辑)与执行环境(事务、日志、监控)彻底解耦。新增一种横切能力,只需要在链上插入一个拦截器,所有引擎命令都会自动获得该能力;而新增一种业务操作,只需要新增一个命令类。这完美印证了命令模式的核心价值——把"请求"封装成对象之后,请求的发出、排队、事务化、日志化就都成为可能。