如果你写过撤销功能,就一定体会过这种感觉:刚开始只是一个按钮,后来变成两个、三个、十个,再后来每个按钮背后还要支持撤销、重做、宏录制……代码从几百行膨胀到几千行,最后连改一个参数都要小心翼翼。
先说清楚,我聊的是设计模式里的命令模式(Command Pattern),不是敲命令行,也不是网络设备里的模式切换命令。很多人在复习设计模式期末、或准备 Java 设计模式面试时,会把命令模式理解成“把一类操作封装成一个方法”,其实差了很远。命令模式的本质,是把“动作”本身变成一个对象,让它可以被传递、排队、记录和撤销。今天这篇就围绕这个核心,从一个真实重构案例出发,把命令模式讲透。
不管你是正在赶设计模式大作业,还是想在真实项目里落地这个模式,这篇文章都会比课本里的描述更接近实战。我会先带你看看不用命令模式时代码是怎么烂掉的,再完整写一个可运行的 Java 实现,最后聊聊和策略模式的区别,以及那些我在项目里踩过的坑。
1. 一次重构引发的思考:为什么工具栏代码会越写越烂
1.1 需求一多,按钮处理器就成了垃圾场
很多系统最开始都是一个类来处理所有用户操作。比如做一个文本编辑器,界面上有复制按钮、剪切按钮、粘贴按钮。第一版代码很简单,直接在按钮点击事件里写业务逻辑:
java复制public class EditorToolbar {
private TextEditor editor;
public void onCopyClick() {
String selected = editor.getSelectedText();
Clipboard.set(selected);
}
public void onCutClick() {
String selected = editor.getSelectedText();
Clipboard.set(selected);
editor.deleteSelection();
}
public void onPasteClick() {
String text = Clipboard.get();
editor.insertText(text);
}
}
这个版本没什么问题。三个按钮,三个方法,一眼就能看懂。但需求永远不会停在这里。
接下来产品经理说,工具栏要加一个“历史记录”面板,每个操作都要显示“刚才做了什么”。于是你开始在每个方法里加日志。然后又要支持撤销,复制可以不撤销,但剪切和粘贴必须能撤销。于是每个方法里的逻辑开始变复杂,参数开始变多,异常处理也开始重复。等到要支持“宏录制”——把连续几步操作录成一个新按钮时,这个类已经膨胀得没法看了。
1.2 传统写法为什么改不动:耦合、开闭原则、不可复用
很多人会把问题归咎于“方法里代码太多”,然后去做方法抽取,把每个操作拆成独立的方法。这样确实比写在按钮事件里舒服一点,但核心矛盾还在:操作的定义和操作的触发被硬绑在了一起。
这种写法有几个非常典型的问题:
- 违背开闭原则。每新增一个操作,都要打开这个类,加一个方法,再加一个 if/switch 分支或按钮绑定。类被反复修改,一不小心就影响老功能。
- UI 和业务逻辑耦合。按钮只负责被点击,但业务逻辑直接暴露在了 UI 层。换一个界面框架,所有点击方法都要跟着改。
- 无法记录动作历史。撤销的本质是“反着执行上一次的操作”,但普通方法调用不保存“上一次操作是什么”,调用完就丢了。
- 无法把多个操作组合成一个操作。宏、批量操作、事务操作,都需要“把一系列动作当作一个整体”,传统方法调用做不到。
这些痛点其实指向同一个设计方向:能不能把每一步操作包装成一个对象,让调用方只关心“执行”和“撤销”这两个动作? 这就是命令模式出现的理由。
1.3 从需求倒推设计:命令模式就是给“操作”穿了一层外套
命令模式的设计思路非常直白:把所有操作弄成一个接口,接口里定义 execute() 方法,每个具体操作写成一个实现类。这样,按钮不直接认识“Light”或者“Editor”,它只认识一个 Command 接口。
你可以把命令对象想象成一个快递单。快递单本身不搬货,但它记录着“从哪取”“送到哪”“找谁签收”。物流中心只要按照快递单上的信息执行就行。命令对象也是这样,它不包含业务实体的全部信息,只记录“执行这个动作所需的最小信息”,通常是一个 Receiver 的引用和一个方法调用。
从需求倒推来看,命令模式能帮你做到的是:
- 新增操作时,不用改已有类,只新增一个 Command 实现类。
- UI 层只依赖 Command 接口,不依赖具体业务类。
- 命令对象可以放进队列、栈、列表,天然支持撤销、重做、日志、事务。
- 多个命令可以组合成复合命令,实现宏和批处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 命令模式的三个核心角色:Invoker、Command 和 Receiver 到底怎么配合
2.1 四个参与者的最小模型
课本上通常会说命令模式有四个角色:Client、Invoker、Command、ConcreteCommand、Receiver。但实际编码时,很多人会混淆谁创建命令、谁执行命令、谁真正干活。我更
