周四早上出门,太阳已经升起来了。
连着阴了几天,一抬头,天是亮的,连路边那排共享单车都被照得发白。小王眯了一下眼,昨晚那点闷,散了一半。
昨晚弄到很晚。到家过了十一点,洗漱完躺下,已经过了零点。睡着以后还在排查问题,梦里那个报错一直在响,怎么都关不掉。
到公司坐稳,电脑刚亮起来,消息就跟排队似的往外冒:群里三条 @,私聊两条,工位隔板上还贴着一张不知道谁留的便利贴,上面写着"帮忙看下这个,急"。都急。

一个早上,六件事,件件都说自己急
他以前的做法是:哪条最新看哪条,谁催得紧先回谁。一个上午过去,最急的那件还没动,最不着急的倒被他翻来覆去看了三遍。
中间查 A 的时候发现是 B 引起的,查 B 又牵出 C 的配置不对。一个接一个,一环套一环,脑子里那根线,越扯越乱。
不是不够努力,是"要做的事"全堆在脑子里,没有一件能被拿起来。
今天聊的就是把它拿出来的写法:命令(Command)------把"要做什么"写成一个对象,于是它能排队、能排序、能撤销、能留档。系列第六篇。
01 别把事堆在脑子里
先看一个不太聪明的写法。三个求助砸过来,最直接的写法就是在流程里挨个调:
java
// 三个求助,直接调
restartService("pay-service");
clearCache("item-detail");
setConfig("pay-switch", "on");
三行下去,事就过去了,什么都没留下
问题不在于这三行写得不对,在于它们只存在于执行的那一瞬间。谁提的、几点要、做到哪一步、做错了怎么退,全在脑子里。
脑子还扛得住三件的时候,这套写法看不出毛病。扛到六件就开始丢------昨晚他就是这么丢的:查 A 查到 B,查 B 又牵出 C,回头发现最早那条催得最急的消息,还躺在聊天列表里没点开。
丢的不是记性。是这些事本身,没有一件能被单独拿起来。
没有被写成对象的事,只能活在记忆里。
02 一张便签,三行字
命令模式的第一步很简单:把"要做什么"写成对象。
一张便签上其实就三样东西:干什么、做错了怎么退,还有顶上那行抬头------一眼能认出是哪件事。
java
interface Ticket { // 一张便签
void execute(); // 干这件事
void undo(); // 退回去
String summary(); // 抬头:一眼认出哪件
}
// 改配置:老值记下来,undo 才退得回去
class ConfigChange implements Ticket {
private final String key, from, to;
ConfigChange(String key, String from, String to) {
this.key = key; this.from = from; this.to = to;
}
public void execute() { setConfig(key, to); }
public void undo() { setConfig(key, from); } // 有旧值,就能退
public String summary() { return "改 " + key + ":" + from + " → " + to; }
}
// 重启服务:没有反向操作
class RestartService implements Ticket {
private final String service;
RestartService(String service) { this.service = service; }
public void execute() { restart(service); }
public void undo() { } // 重启退不回去,这里只能空着
public String summary() { return "重启 " + service; }
}
同一张便签的形状,退不回去的那张只能空着
关键不是"封装成类"这四个字。是这张便签可以离开你的脑子单独存在:能递给别人,能排进一列,能贴在那儿先不办。
03 便签墙:贴、挑、记
便签写好了,接下来得有个地方贴。
java
// 便签墙:只管排序、挑一张、记账
class Board {
private final List<Ticket> todo = new ArrayList<>();
private final Deque<Ticket> done = new ArrayDeque<>();
void add(Ticket t) { todo.add(t); } // 新便签贴到队尾,不打断手上这张
void runNext() {
Ticket t = todo.remove(0); // 挑最前面那张
t.execute();
done.push(t); // 做完记一笔
}
void undoLast() {
if (!done.isEmpty()) done.pop().undo(); // 上一步错了,退回去
}
}
便签墙不认识"重启"和"改配置"。它不知道重启要多久、缓存怎么刷,它只做三件事:贴、挑、记。
反过来,便签也不认识墙。ConfigChange 不需要知道自己是第几张被执行的。两边只通过 Ticket 这一个接口说话。
以后要加"回滚发布""扩一台机器",只是多写一个 Ticket 实现,墙一行都不用改。
调用者只管排队,执行者只管干活,中间隔着一张便签。

一张便签,把三方隔开
上午那根线,现在能理顺了。
上午刚开工,测试在群里发截图,下单报错。他翻日志,看见 pay-switch 这个配置昨晚发布时被关掉了。改回来,再重启支付服务。
还没坐稳,产品又来一条:商品详情页的价格不对。查下去是同一条线------昨晚那批配置改动上线后,商品缓存没刷。清一下缓存,价格就回去了。
三张便签,两个根因。
回头复盘,这三张便签的理想顺序是:先改配置,再清缓存,最后才重启。顺序不是拍脑袋定的,是"能不能退"定的------重启退不回去,就放到最后。

先做退得回去的,最后做退不回去的
这条规矩写在便签的 undo 里,比记在脑子里可靠得多。
04 最朴素的命令,你天天在写
这套东西不新鲜,JDK 里天天见:
java
executor.submit(() -> restart("pay-service"));
一张没写抬头、也没有 undo 的便签
Runnable 就是最朴素的便签:只有一个 execute(run),没有 undo,顶上也没有那行抬头。submit 是把便签贴到墙上,线程池就是那个一张一张把它们做掉的人。
再往身边看,形状全一样:
- 界面上的按钮,点一下,就是执行一张便签;
- 编辑器的 Ctrl+Z,按一下,就是从"做过的事"那一列弹出一张,调它的
undo; - 数据库能回滚,靠的是日志里记着每一笔改动怎么退回去。
它们都在做同一件事:把"要做的事"打包成一个能传递的东西。
05 什么时候,别乱写便签
便签不是越多越好。
只有一次、不排队、不撤销、也不用留档的调用,直接调方法就行。把每个方法都包成命令,本来一眼能看明白的代码,现在得跳三层。
undo 也不是白送的。得自己写反向操作,而且很多事根本没有反向:重启退不回去,发出去的短信撤不回,扣掉的钱要退得走另一套流程。便签撕掉是免费的,代码里往回退一步,是要还的。
还有一件事便签不管:轻重缓急。
便签墙上先挑哪张,是排序的事,排序规则本身还能是个策略(第四篇那个)。命令管"这件事怎么被拿起来",不管"先办哪件"。这两件事混在一起写,最后一定是两者都做不好。
这两年排查问题,AI 给方向确实快。它甚至能直接把这三张便签的代码写好。
但有两件事它替不了:一是这张便签到底该不该执行,二是执行完了退不回来怎么办。方向对了省一小时,方向错了,你得有本事退回来。
命令模式不替你做决定,它只让你做的决定能被拿起来。
06 收工
下班时天已经擦黑。便签墙上一列划掉,一列还空着。
他头一回在下班前,把今天干了什么一口气说完:六件事,追下去只有两个根因。
昨晚梦里那个一直响的报错,今天算是关掉了。
跟前面五篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把"要做的事"本身做成一个东西。
带走一句话:命令 = 把"要做的事"变成一个能排队、能撤销、能留档的对象。
能写下来的事,才轮得到你安排。
参考资料
- 📕 《设计模式:可复用面向对象软件的基础》(GoF 原著)------命令模式的原始定义,把"请求"本身对象化
- 📗 《Head First 设计模式》第 6 章------遥控器与命令对象,本文"便签"的近亲
- 📘 Java 官方文档 · Runnable------最朴素的命令,只有一个 execute
- 📙 Java 官方文档 · Executor------把便签贴到墙上的人