前面几篇的行为型,处理的是行为怎么变“对象怎么协作”。这一篇的命令模式(Command)换了个视角,它做一件听起来有点抽象、但威力很大的事:把一个请求/操作本身,封装成一个对象。平时我们调用一个方法,是当场执行——service.doSomething(),调了就立刻做了,这个操作本身是一闪而过的、抓不住的。命令模式反其道而行:它把要做什么这件事,变成一个实实在在的对象(命令对象)。一旦操作变成了对象,神奇的事情就发生了——对象可以被存进变量、放进集合、传来传去、排队、记日志、甚至反过来撤销。这些是当场执行的方法调用永远做不到的。它的核心价值,一句话:把发出请求的人和执行请求的人解耦,并且让请求本身具备可存储、可排队、可撤销、可重做的能力。打个比方:你在餐厅点菜,把需求写在点菜单上交给服务员,服务员传给后厨。这张点菜单就是一个命令对象——它把我要一份宫保鸡丁这个请求固化成了一个可传递的东西。你(请求发起者)不用直接跟厨师(执行者)说话;点菜单可以排队(后厨按单做);可以记录(账单);甚至可以取消(退单)。我们的订单场景里,命令模式最能发光的地方是**“可撤销的操作”**:用户在后台对订单做了一串操作(修改地址、追加备注、改数量……),然后想撤销上一步、甚至连撤三步。如果操作都是当场执行的方法调用,撤销无从谈起;但如果每个操作都是一个命令对象,记录下来,撤销就变成了把命令栈弹出一个、执行它的逆操作。这一篇我们就从这个撤销需求切入。这篇文章按这条线索展开:先看操作是一闪而过的方法调用,想撤销却无从下手的困境;再引出命令模式如何把操作封装成对象;然后讲清它的角色、以及撤销/重做是怎么实现的;接着看它在现实里的身影;最后给出适用边界。贯穿例是订单操作与撤销。目录想撤销操作,却无从下手命令模式:把操作封装成对象角色,与撤销/重做的实现现实身影:线程池任务、事务与队列什么时候用命令模式一、想撤销操作,却无从下手看后台对订单的操作。最直觉的写法,是需要什么操作就直接调什么方法:publicclassOrderAdminService{publicvoidrun(){order.setAddress(北京市朝阳区);// 改地址order.addRemark(尽快发货);// 加备注order.setCount(3);// 改数量// 用户说:撤销刚才改的数量 —— 怎么办??}}需求来了:用户想撤销上一步操作,甚至连续撤销好几步。这时候你会发现,上面这种写法根本无从下手:操作是一闪而过的:order.setCount(3)执行完,这个操作就消失了——你没有任何东西记录刚才发生了什么操作、之前的值是什么。想撤销,你连撤销什么、恢复成什么都不知道。发起者和执行者紧耦合:OrderAdminService直接调用order的具体方法,它俩焊死了。想给操作加上记录日志“排队执行”权限检查等通用能力,只能去改每一处调用。没法统一管理操作:如果想把一批操作排队、批量执行、或者做成操作历史,你手里只有一堆散落的方法调用,没有一个统一的操作概念可以收集、存储。问题的根源是:操作没有被表示成一个具体的东西,它只是一个转瞬即逝的方法调用。而撤销“重做”“排队”记录这些能力,统统需要先抓住这个操作——把它变成一个能被存储、能记住自己做了什么、怎么撤销的对象。我们真正想要的是:把每个操作(改地址、改数量……)都封装成一个命令对象,这个对象知道怎么执行自己,也知道怎么撤销自己;把执行过的命令记在一个栈里,撤销就是弹栈 执行逆操作。这就是命令模式。二、命令模式:把操作封装成对象命令模式的做法:定义一个命令接口,声明execute()(执行)和undo()(撤销);每个具体操作是一个命令类,封装对谁、做什么、怎么撤销;由一个调用者来触发命令,而不直接调用执行者。第一步,定义命令接口:publicinterfaceCommand{voidexecute();// 执行操作voidundo();// 撤销操作(逆操作)}第二步,每个操作是一个具体命令,它持有执行者和撤销所需的数据:// 修改数量的命令publicclassChangeCountCommandimplementsCommand{privatefinalOrderorder;// 执行者(接收者)privatefinalintnewCount;privateintoldCount;// 记住旧值,用于撤销publicChangeCountCommand(Orderorder,intnewCount){this.orderorder;this.newCountnewCount;}publicvoidexecute(){this.oldCountorder.getCount();// 执行前先记下旧值order.setCount(newCount);}publicvoidundo(){order.setCount(oldCount);// 撤销:恢复旧值}}// ChangeAddressCommand、AddRemarkCommand 同理,各自记住自己的撤销数据第三步,调用者维护一个命令历史栈,执行时入栈,撤销时弹栈:publicclassOrderCommandInvoker{privatefinalDequeCommandhistorynewArrayDeque();// 命令历史栈publicvoidexecute(Commandcommand){command.execute();history.push(command);// 执行过的命令入栈,为撤销做准备}publicvoidundo(){if(!history.isEmpty()){Commandlasthistory.pop();// 弹出最近一条last.undo();// 执行它的逆操作}}}用起来,操作变成了提交命令,撤销变得轻而易举:OrderCommandInvokerinvokernewOrderCommandInvoker();invoker.execute(newChangeAddressCommand(order,北京市朝阳区));// 改地址invoker.execute(newChangeCountCommand(order,3));// 改数量invoker.undo();// 撤销改数量 → 数量恢复invoker.undo();// 再撤销改地址 → 地址恢复对比第一节,升级点非常清晰:每个操作都成了一个能被存储、能自我撤销的命令对象;OrderCommandInvoker(发起者)不再直接调用order的方法,而是通过命令间接触发——发起者和执行者解耦了;而撤销这个曾经无从下手的需求,现在只是弹栈 undo()这么简单。用一张图看这个请求对象化的结构最清楚:图里最该记住的,是那条发起者 → 命令对象 → 接收者的间接链条,以及那个命令历史栈。命令模式的精髓,就是在发起请求和执行请求之间,插入了一个命令对象作为中间层——正是这个中间层,把一次性的方法调用,变成了可以被收集、排队、记录、撤销的一等公民。三、角色,与撤销/重做的实现命令模式的角色,四个:角色本例中是谁职责命令接口(Command)Command声明execute()/undo()具体命令(ConcreteCommand)ChangeCountCommand等封装对谁、做什么、怎么撤销接收者(Receiver)Order真正执行操作的对象调用者(Invoker)OrderCommandInvoker触发命令、管理命令历史还有一个隐含的客户端,负责创建具体命令、指定接收者。重点说说撤销(undo)和重做(redo)是怎么实现的,这是命令模式最亮眼的能力:撤销 undo:靠一个已执行命令栈。每执行一个命令就push进栈;撤销时pop出最近一个、调它的undo()。因为每个命令都记住了自己执行前的状态(如oldCount),所以能精确回滚。连续撤销,就是连续弹栈。重做 redo:再加一个已撤销命令栈。撤销一个命令时,把它从已执行栈弹出、压入已撤销栈;重做时,从已撤销栈弹出、重新execute()、再压回已执行栈。两个栈配合,就实现了编辑器里那种撤销—重做—再撤销的完整能力。这里有个和上一篇的呼应:命令模式实现撤销,是靠每个命令记住自己的逆操作/旧值。如果一个操作的状态很复杂、记录旧值不方便,还有另一种撤销思路——直接给对象拍个快照,撤销时整个恢复快照。那就是下一篇之后要讲的备忘录模式了。两者常常配合:命令负责触发和管理,备忘录负责存快照。四、现实身影:线程池任务、事务与队列命令模式的身影,凡是把操作当对象来传递、排队、延迟执行的地方都有它:Runnable/ 线程池任务:Runnable就是一个最纯粹的命令对象——它把要执行的任务封装成一个对象(run()就是execute())。你把Runnable提交给线程池(executor.submit(task)),线程池就是调用者,它把任务排进队列、择机执行。“把任务对象化、提交给执行器排队执行”,正是命令模式的核心思想。消息队列 / 任务队列:把要做的操作封装成消息丢进队列,消费者取出来执行——这是命令模式在分布式层面的放大。请求被对象化、序列化、排队、异步执行。数据库事务与回滚日志:事务里的每个操作都记录了怎么做和怎么撤销(redo log / undo log),回滚时执行逆操作——这和命令模式的 undo 思想高度一致。GUI 的菜单/按钮操作、编辑器的撤销重做:每个菜单项、每次编辑都是一个命令,编辑器的 CtrlZ / CtrlY 就是命令栈的撤销/重做。这是命令模式的经典发源场景。Spring 的JdbcTemplate回调、各种XxxCallback:把要执行的逻辑封装成一个回调对象传进去,也带有命令模式的影子。一个识别信号:凡是把一个操作/任务封装成对象,以便存储、传递、排队、延迟执行或撤销,就是命令模式。它是任务队列和撤销重做这两大功能的底层思想。五、什么时候用命令模式适合用命令模式的信号:你需要把操作存储、排队、延迟执行、或异步执行(任务队列、线程池);你需要支持撤销/重做;你想解耦请求的发起者和请求的执行者,让发起者不必知道具体怎么执行;你想给一组操作统一加上日志、事务、权限等通用处理(在 Invoker 里统一做)。不必用的信号:操作就是简单的、一次性的、不需要撤销/排队的方法调用——那直接调用最清晰,套命令模式凭空多出一堆命令类,是过度设计;没有把操作当对象来管理的任何需求——命令模式的价值全在操作对象化带来的那些能力上,用不上这些能力就别用它。判断的核心还是那句话:先确认真的需要撤销/重做、排队、延迟执行、发起与执行解耦中的某一项,命令模式才值得上。为普通的方法调用套一层命令,除了增加间接层和类数量,没有收益——这是命令模式最常见的过度设计。一个务实提醒:现代 Java 里,如果命令逻辑很简单,不必为每个命令都写一个类——直接用 Lambda 或方法引用当命令(Runnable、Consumer等函数式接口),就是最轻量的命令对象。只有当命令需要携带撤销逻辑、或有复杂状态时,才值得写成完整的命令类。小结。命令模式把一个请求/操作封装成对象,从而让操作这种转瞬即逝的东西,变成可以被存储、传递、排队、记录和撤销的一等公民。它在发起者和执行者之间插入命令对象这个中间层,实现了两者解耦;并通过命令历史栈 每个命令记住自己的逆操作,优雅地实现了撤销与重做。Runnable/线程池任务、消息队列、事务回滚、编辑器的撤销重做,都是它的身影,是任务队列和撤销重做的底层思想。用它的前提是真的需要那些操作对象化才能带来的能力,否则就是过度设计;简单命令直接用 Lambda 即可。下一篇我们讲迭代器模式——它把遍历一个集合的逻辑,从集合本身分离出来,让你能用统一的方式遍历各种不同结构的集合,而不必关心它内部是数组还是链表。