[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-18":26,"\u002Farticles\u002Fdesign-pattern-18\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-18\u002Fadjacent":71,"\u002Fcategories":75,"render-design-pattern-18":101},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113932265082851329","design-pattern-18","设计模式修炼之路 ⑱ 命令模式——把请求封装成对象","把请求封装成对象、增量式撤销、白送的排队与重放、Executor 的铁证","2113897628365230081",1,2089,7080,18,"2024-08-09T12:36:13","> 行为型第五站。\n\n上一篇结尾说：责任链传的\"请求\"始终只是个参数，命令模式更进一步——**把请求本身变成对象**。这一篇兑现承诺。\n\n场景就从最日常的编辑器讲起。你写文档时手一抖删了一段，按 Ctrl+Z，内容回来了；再按 Ctrl+Y，又删掉了。撤销、重做、任务队列、宏录制……这些功能的第一块基石，都是命令模式。\n\n## 一、命令模式是什么\n\nGoF 的原始定义：\n\n> Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.\n> （将一个请求封装成一个对象，从而使你可以用不同的请求对客户进行参数化；可以对请求排队、记录日志，以及支持可撤销的操作。）\n\n翻译成人话：**把\"要干的事\"打包成对象。**调用方手里拿的不再是\"干这件事\"的过程调用，而是\"干这件事\"这个对象本身——而对象能存储、能传递、能排队、能记账、能反悔。\n\n四个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Command（抽象命令）\u003C\u002Ftd>\u003Ctd>定义执行与撤销的统一接口\u003C\u002Ftd>\u003Ctd>Command\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>ConcreteCommand（具体命令）\u003C\u002Ftd>\u003Ctd>绑定接收者，实现\"怎么执行、怎么撤销\"\u003C\u002Ftd>\u003Ctd>InsertCommand \u002F DeleteCommand\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Receiver（接收者）\u003C\u002Ftd>\u003Ctd>真正干活的对象\u003C\u002Ftd>\u003Ctd>TextEditor\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Invoker（调用者）\u003C\u002Ftd>\u003Ctd>持有命令、负责执行、记录历史\u003C\u002Ftd>\u003Ctd>CommandHistory\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n最好的类比是餐厅点单：你（客户端）不冲进后厨喊\"来份宫保鸡丁\"（直接方法调用），而是把需求写在订单上（命令对象）交给服务员（调用者）。服务员拿着订单能干的事就多了：立刻递后厨（执行）、压单等位（排队）、你说错了撤单（撤销）、月底翻存根对账（日志）。**订单本身不会炒菜——真正炒菜的是后厨（接收者）**，订单只是把\"要干什么\"固化成了可搬运的纸片。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **请求与执行解耦**。调用者只认识 Command 接口，根本不知道接收者是谁、操作怎么落地——命令对象把这一切打包带走了。\n2. **撤销是白送的**。命令知道自己\"干了什么\"，就有能力描述\"怎么反着干\"。调用者只管把命令压进历史栈，撤销时弹出来调 undo 即可。\n3. **排队、延迟、重试都是白送的**。命令是对象，对象就能进队列。进队列之后什么时候执行、在哪个线程执行、失败了几时重试，全是调度层的自由。\n4. **可组合**。一组命令可以拼成一个宏命令（组合模式 ⑨ 的老朋友），一键回放。\n\n**缺点：**\n\n1. **类数量膨胀**。每个操作一个命令类，操作多了类也多——Java 8 之前写起来相当啰嗦（lambda 缓解了大半，见第五节）。\n2. **间接层带来的理解成本**。看代码时找不到直接调用，执行逻辑藏在命令对象的 execute 里，追起来要绕一层。\n3. **撤销的实现并不总是轻巧**。多数操作可以定义逆操作，但\"发邮件\"这种不可逆操作，撤销只能语义补偿（再发一封道歉邮件？），设计时要掂量。\n\n## 三、什么时候用\n\n判断标准：**\"请求\"需要在时间或空间上离开\"发起者\"——要排队、要延迟执行、要事后撤销、要记录重放，或者要跨线程跨进程搬运。**\n\n- **撤销\u002F重做**：编辑器、绘图工具、数据库事务——本文场景\n- **任务队列**：线程池、消息队列、定时任务，队列里排的一个个任务全是命令对象\n- **操作日志与恢复**：把命令序列化进日志，宕机后重放恢复现场\n- **宏录制**：把用户一连串操作录成命令列表，一键回放\n- **跨进程分发**：MQ 的消息体，本质是\"生产者想让消费者干的活\"的对象化\n\n反面对照：请求发起后立刻执行、执行完就完、不需要历史——直接调用方法就好，别为了模式而模式。\n\n## 四、实现方式：增量式撤销\n\n### 4.1 反面教材：快照式撤销\n\n撤销最容易想到的做法：每干一步，存一份全文快照，撤销就回滚到上一份。\n\n```java\npublic class BadEditor {\n\n    private String text = \"\";\n    \u002F\u002F 为了撤销，只能每一步都存一份全文快照\n    private final Deque\u003CString> snapshots = new ArrayDeque\u003C>();\n\n    public void insert(String s) {\n        snapshots.push(text);\n        text = text + s;\n        System.out.println(\"  [编辑器] 插入 \\\"\" + s + \"\\\"，当前：\\\"\" + text + \"\\\"\");\n    }\n\n    public void delete(int count) {\n        snapshots.push(text);\n        text = text.substring(0, text.length() - count);\n        System.out.println(\"  [编辑器] 删除 \" + count + \" 个字符，当前：\\\"\" + text + \"\\\"\");\n    }\n\n    \u002F\u002F 撤销 = 回滚到上一份快照\n    \u002F\u002F 文本是 String 还好；如果编辑的是 10MB 的文档对象，每一步存一份快照，内存直接爆炸\n    \u002F\u002F 而且想加\"重做\"、\"宏录制\"？全部要在这个类里继续堆代码\n    public void undo() {\n        if (snapshots.isEmpty()) {\n            System.out.println(\"  [编辑器] 没有可撤销的操作\");\n            return;\n        }\n        text = snapshots.pop();\n        System.out.println(\"  [编辑器] 撤销，当前：\\\"\" + text + \"\\\"\");\n    }\n}\n```\n\n看着能跑，但病灶清晰：**撤销的单位是\"整个文档\"，而不是\"那一步操作\"。**String 倒还扛得住，换成 10MB 的富文档对象，每敲一个字存一份快照，内存曲线直接起飞。更糟的是\"撤销\"这个能力被焊死在编辑器内部——想加重做、宏录制、操作日志，全得往这一个类里继续堆。\n\n### 4.2 正解：每一步操作变成一个命令对象\n\n第一步，命令接口——除了执行，撤销是命令模式的招牌能力：\n\n```java\npublic interface Command {\n    void execute();\n    void undo();\n}\n```\n\n第二步，接收者。一个最朴素的文本编辑器：\n\n```java\npublic class TextEditor {\n    private String text = \"\";\n\n    public void insert(String s) { text = text + s; }\n    public void delete(int count) { text = text.substring(0, text.length() - count); }\n    public String getText() { return text; }\n}\n```\n\n第三步，具体命令。插入命令的执行和撤销是一对完美互逆：\n\n```java\npublic class InsertCommand implements Command {\n\n    private final TextEditor editor;\n    private final String s;\n\n    public InsertCommand(TextEditor editor, String s) {\n        this.editor = editor;\n        this.s = s;\n    }\n\n    @Override\n    public void execute() {\n        editor.insert(s);\n        System.out.println(\"  [插入] \\\"\" + s + \"\\\"，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n\n    @Override\n    public void undo() {\n        editor.delete(s.length());\n        System.out.println(\"  [撤销插入] \\\"\" + s + \"\\\" 被移除，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n```\n\n删除命令多一个心眼——**执行时把删掉的内容记下来**，不然撤销时拿什么还原？\n\n```java\npublic class DeleteCommand implements Command {\n\n    private final TextEditor editor;\n    private final int count;\n    private String deleted;   \u002F\u002F 执行时记下被删的内容，撤销时才能还原\n\n    public DeleteCommand(TextEditor editor, int count) {\n        this.editor = editor;\n        this.count = count;\n    }\n\n    @Override\n    public void execute() {\n        String current = editor.getText();\n        deleted = current.substring(current.length() - count);\n        editor.delete(count);\n        System.out.println(\"  [删除] 删掉 \\\"\" + deleted + \"\\\"，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n\n    @Override\n    public void undo() {\n        editor.insert(deleted);\n        System.out.println(\"  [撤销删除] \\\"\" + deleted + \"\\\" 回来了，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n```\n\n第四步，调用者。它只认识 Command 接口，干的就三件事：执行、记账、反悔：\n\n```java\npublic class CommandHistory {\n\n    private final Deque\u003CCommand> history = new ArrayDeque\u003C>();\n\n    public void execute(Command command) {\n        command.execute();\n        history.push(command);\n    }\n\n    public void undo() {\n        if (history.isEmpty()) {\n            System.out.println(\"  [历史] 没有可撤销的操作\");\n            return;\n        }\n        history.pop().undo();\n    }\n}\n```\n\n最后串起来，四条命令压栈，再连按五次撤销：\n\n```java\npublic class CommandTest {\n\n    public static void main(String[] args) {\n        System.out.println(\"========== 反面教材：快照式撤销 ==========\");\n        BadEditor bad = new BadEditor();\n        bad.insert(\"设计模式\");\n        bad.delete(4);\n        bad.insert(\"修炼之路\");\n        bad.undo();\n        bad.undo();\n        bad.undo();\n        bad.undo();\n\n        System.out.println();\n        System.out.println(\"========== 正解：命令模式 ==========\");\n        TextEditor editor = new TextEditor();\n        CommandHistory history = new CommandHistory();\n\n        history.execute(new InsertCommand(editor, \"设计\"));\n        history.execute(new InsertCommand(editor, \"模式\"));\n        history.execute(new DeleteCommand(editor, 2));\n        history.execute(new InsertCommand(editor, \"修炼\"));\n        System.out.println(\"  当前文本：\\\"\" + editor.getText() + \"\\\"\");\n\n        System.out.println();\n        System.out.println(\"-- 连按五次撤销，按执行逆序回放 --\");\n        history.undo();\n        history.undo();\n        history.undo();\n        history.undo();\n        history.undo();\n        System.out.println(\"  当前文本：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n```\n\n运行输出：\n\n```text\n========== 反面教材：快照式撤销 ==========\n  [编辑器] 插入 \"设计模式\"，当前：\"设计模式\"\n  [编辑器] 删除 4 个字符，当前：\"\"\n  [编辑器] 插入 \"修炼之路\"，当前：\"修炼之路\"\n  [编辑器] 撤销，当前：\"\"\n  [编辑器] 撤销，当前：\"设计模式\"\n  [编辑器] 撤销，当前：\"\"\n  [编辑器] 没有可撤销的操作\n\n========== 正解：命令模式 ==========\n  [插入] \"设计\"，当前：\"设计\"\n  [插入] \"模式\"，当前：\"设计模式\"\n  [删除] 删掉 \"模式\"，当前：\"设计\"\n  [插入] \"修炼\"，当前：\"设计修炼\"\n  当前文本：\"设计修炼\"\n\n-- 连按五次撤销，按执行逆序回放 --\n  [撤销插入] \"修炼\" 被移除，当前：\"设计\"\n  [撤销删除] \"模式\" 回来了，当前：\"设计模式\"\n  [撤销插入] \"模式\" 被移除，当前：\"设计\"\n  [撤销插入] \"设计\" 被移除，当前：\"\"\n  [历史] 没有可撤销的操作\n  当前文本：\"\"\n```\n\n### 4.3 三处细节咂摸一下\n\n**细节一：撤销的本质是\"逆操作\"，不是\"回滚\"。**反面教材的撤销单位是整个文档（快照），命令模式的撤销单位是那一步操作本身（增量）。撤销一条插入只需删掉那几个字，撤销一条删除只需补回那几个字——文档 10MB 也好 100MB 也好，每条命令只携带自己的增量。Ctrl+Z 能做到瞬响应，靠的就是这个。\n\n**细节二：命令对象是有状态的。**DeleteCommand 的 `deleted` 字段在执行那一刻才被填上——同一个命令对象，执行前和执行后携带的信息不一样。这一点和策略模式形成了鲜明对比（策略通常无状态），第六节展开。\n\n**细节三：CommandHistory 完全不知道 TextEditor 的存在。**它只认识 Command 接口，账本里存的也全是接口引用。这意味着\"历史记录\"这个能力可以原封不动地搬到任何命令体系上：绘图软件的撤销栈、数据库的事务日志、游戏录像的回放器——换个命令实现，调用者一行不改。\n\n### 4.4 白送的能力\n\n把请求变成对象之后，很多东西是\"白送\"的——调用者已经持有命令对象了，剩下的只是用法问题：\n\n- **重做**：undo 时别扔，压进另一个 redo 栈；Ctrl+Z 撤掉的 Ctrl+Y 再执行回来。\n- **宏录制**：`List\u003CCommand>` 就是一个宏。再套一层 CompositeCommand（内部持有一组命令，execute 遍历执行、undo 逆序回滚）——组合模式 ⑨ 的经典复用。\n- **操作日志**：execute 时把命令序列化进日志，系统宕机后按序重放，现场自动恢复——数据库 WAL、Redis AOF 的思路同源。\n- **排队与调度**：命令对象扔进 BlockingQueue，什么时候执行、在哪个线程执行、失败了隔多久重试，全是调度层说了算——线程池干的就是这个买卖，第五节看实物。\n\n## 五、在框架里找它\n\n命令模式在 JDK 里有一个官方盖章的铁证——线程池的执行方法签名：\n\n```java\npublic interface Executor {\n    void execute(Runnable command);\n}\n```\n\n参数名就叫 `command`，JDK 自己承认了。`Runnable` 是命令接口，你的任务逻辑是命令对象，`ThreadPoolExecutor` 是调用者：它把命令对象排进队列（排队）、延迟调度（延迟执行）、必要时拒绝或重试。**\"把任务扔进线程池\"这个每天写的动作，本质上和\"把订单交给服务员\"一模一样。**\n\n而且 JDK 在这里把命令接口的演化史也一并演示了。Java 8 之前提交一个任务得写匿名内部类，命令模式的\"类爆炸\"缺点被诟病多年；lambda 之后：\n\n```java\nexecutor.submit(() -> orderService.close(orderId));\n```\n\n一行完事。函数式接口让命令模式从\"重武器\"退化成了日常空气——你天天在用，只是没意识到。\n\n其他几处：\n\n**Swing Action。**按钮和菜单项共用同一个 Action，点谁都是执行同一份命令——\"工具栏的保存按钮\"和\"Ctrl+S\"背后是同一个对象，启用禁用状态还能联动。GUI 领域命令模式的老前辈。\n\n**Netty EventLoop。**每个 EventLoop 一条线程一个任务队列，普通任务和定时任务都是排队的命令对象——Netty 的定时任务能力就是\"命令 + 优先级队列\"的组合。\n\n**MQ 消息。**生产者把\"希望消费者干什么\"序列化成消息体扔进队列，消费者取出来反序列化执行。这是命令模式的跨进程放大版：发起者和执行者不仅解耦，连进程、机器、时区都解耦了。事件驱动架构的底牌之一。\n\n一句话总结这一节：**凡是\"请求要离开发起者独立旅行\"的地方，命令模式都在场。**\n\n## 六、命令 vs 策略：结构上的孪生兄弟\n\n写到这你可能后背发凉：接口 + 一堆实现类 + 一个持有者——这不就是 ⑮ 策略模式吗？长得确实几乎一样，区别全在灵魂上：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>策略模式 ⑮\u003C\u002Fth>\u003Cth>命令模式 ⑱\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>打包的是什么\u003C\u002Ftd>\u003Ctd>一种**算法**（怎么做）\u003C\u002Ftd>\u003Ctd>一次**请求**（做什么）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>生命周期\u003C\u002Ftd>\u003Ctd>用完即弃，不关心历史\u003C\u002Ftd>\u003Ctd>可存储、可排队、可撤销、可重放\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>有状态吗\u003C\u002Ftd>\u003Ctd>通常无状态\u003C\u002Ftd>\u003Ctd>通常有状态（执行时记增量，撤销时要还）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>持有者持几个\u003C\u002Ftd>\u003Ctd>Context 持**一个**，随时可换\u003C\u002Ftd>\u003Ctd>Invoker 持**一摞**（历史、队列）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>典型场景\u003C\u002Ftd>\u003Ctd>促销算法、支付渠道\u003C\u002Ftd>\u003Ctd>撤销栈、任务队列\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n分辨口诀：**策略回答\"这事有几种做法\"，命令回答\"这事能不能存起来反悔\"。**同样是接口加实现类，策略是给调用方换算法的插槽，命令是给系统留的记账凭证。\n\n## 写在最后\n\n命令模式一句话总结：**把请求封装成对象，执行与发起就此分离；对象能存能传能排队，撤销、重做、日志、重放全是白送。**\n\n下一篇 ⑲ 状态模式——换个场景，电商订单。\"待支付\"的订单点取消，直接关单退款都不用；\"已发货\"的订单点取消，只能走退货流程；同一个按钮，状态不同行为完全不同。如果用 if-else 判断状态，状态一多每个方法都是灾难现场。**把每个状态做成类，状态一换行为整套跟着换**——状态模式，下一篇见。","\u003Cblockquote>\n \u003Cp>行为型第五站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇结尾说：责任链传的\"请求\"始终只是个参数，命令模式更进一步——\u003Cstrong>把请求本身变成对象\u003C\u002Fstrong>。这一篇兑现承诺。\u003C\u002Fp>\n\u003Cp>场景就从最日常的编辑器讲起。你写文档时手一抖删了一段，按 Ctrl+Z，内容回来了；再按 Ctrl+Y，又删掉了。撤销、重做、任务队列、宏录制……这些功能的第一块基石，都是命令模式。\u003C\u002Fp>\n\u003Ch2>一、命令模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. （将一个请求封装成一个对象，从而使你可以用不同的请求对客户进行参数化；可以对请求排队、记录日志，以及支持可撤销的操作。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：**把\"要干的事\"打包成对象。**调用方手里拿的不再是\"干这件事\"的过程调用，而是\"干这件事\"这个对象本身——而对象能存储、能传递、能排队、能记账、能反悔。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Command（抽象命令）\u003C\u002Ftd>\n   \u003Ctd>定义执行与撤销的统一接口\u003C\u002Ftd>\n   \u003Ctd>Command\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteCommand（具体命令）\u003C\u002Ftd>\n   \u003Ctd>绑定接收者，实现\"怎么执行、怎么撤销\"\u003C\u002Ftd>\n   \u003Ctd>InsertCommand \u002F DeleteCommand\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Receiver（接收者）\u003C\u002Ftd>\n   \u003Ctd>真正干活的对象\u003C\u002Ftd>\n   \u003Ctd>TextEditor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Invoker（调用者）\u003C\u002Ftd>\n   \u003Ctd>持有命令、负责执行、记录历史\u003C\u002Ftd>\n   \u003Ctd>CommandHistory\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>最好的类比是餐厅点单：你（客户端）不冲进后厨喊\"来份宫保鸡丁\"（直接方法调用），而是把需求写在订单上（命令对象）交给服务员（调用者）。服务员拿着订单能干的事就多了：立刻递后厨（执行）、压单等位（排队）、你说错了撤单（撤销）、月底翻存根对账（日志）。\u003Cstrong>订单本身不会炒菜——真正炒菜的是后厨（接收者）\u003C\u002Fstrong>，订单只是把\"要干什么\"固化成了可搬运的纸片。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>请求与执行解耦\u003C\u002Fstrong>。调用者只认识 Command 接口，根本不知道接收者是谁、操作怎么落地——命令对象把这一切打包带走了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>撤销是白送的\u003C\u002Fstrong>。命令知道自己\"干了什么\"，就有能力描述\"怎么反着干\"。调用者只管把命令压进历史栈，撤销时弹出来调 undo 即可。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>排队、延迟、重试都是白送的\u003C\u002Fstrong>。命令是对象，对象就能进队列。进队列之后什么时候执行、在哪个线程执行、失败了几时重试，全是调度层的自由。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可组合\u003C\u002Fstrong>。一组命令可以拼成一个宏命令（组合模式 ⑨ 的老朋友），一键回放。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>。每个操作一个命令类，操作多了类也多——Java 8 之前写起来相当啰嗦（lambda 缓解了大半，见第五节）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>间接层带来的理解成本\u003C\u002Fstrong>。看代码时找不到直接调用，执行逻辑藏在命令对象的 execute 里，追起来要绕一层。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>撤销的实现并不总是轻巧\u003C\u002Fstrong>。多数操作可以定义逆操作，但\"发邮件\"这种不可逆操作，撤销只能语义补偿（再发一封道歉邮件？），设计时要掂量。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>\"请求\"需要在时间或空间上离开\"发起者\"——要排队、要延迟执行、要事后撤销、要记录重放，或者要跨线程跨进程搬运。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>撤销\u002F重做\u003C\u002Fstrong>：编辑器、绘图工具、数据库事务——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>任务队列\u003C\u002Fstrong>：线程池、消息队列、定时任务，队列里排的一个个任务全是命令对象\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作日志与恢复\u003C\u002Fstrong>：把命令序列化进日志，宕机后重放恢复现场\u003C\u002Fli>\n \u003Cli>\u003Cstrong>宏录制\u003C\u002Fstrong>：把用户一连串操作录成命令列表，一键回放\u003C\u002Fli>\n \u003Cli>\u003Cstrong>跨进程分发\u003C\u002Fstrong>：MQ 的消息体，本质是\"生产者想让消费者干的活\"的对象化\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：请求发起后立刻执行、执行完就完、不需要历史——直接调用方法就好，别为了模式而模式。\u003C\u002Fp>\n\u003Ch2>四、实现方式：增量式撤销\u003C\u002Fh2>\n\u003Ch3>4.1 反面教材：快照式撤销\u003C\u002Fh3>\n\u003Cp>撤销最容易想到的做法：每干一步，存一份全文快照，撤销就回滚到上一份。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadEditor {\n\n    private String text = \"\";\n    \u002F\u002F 为了撤销，只能每一步都存一份全文快照\n    private final Deque&lt;String&gt; snapshots = new ArrayDeque&lt;&gt;();\n\n    public void insert(String s) {\n        snapshots.push(text);\n        text = text + s;\n        System.out.println(\"  [编辑器] 插入 \\\"\" + s + \"\\\"，当前：\\\"\" + text + \"\\\"\");\n    }\n\n    public void delete(int count) {\n        snapshots.push(text);\n        text = text.substring(0, text.length() - count);\n        System.out.println(\"  [编辑器] 删除 \" + count + \" 个字符，当前：\\\"\" + text + \"\\\"\");\n    }\n\n    \u002F\u002F 撤销 = 回滚到上一份快照\n    \u002F\u002F 文本是 String 还好；如果编辑的是 10MB 的文档对象，每一步存一份快照，内存直接爆炸\n    \u002F\u002F 而且想加\"重做\"、\"宏录制\"？全部要在这个类里继续堆代码\n    public void undo() {\n        if (snapshots.isEmpty()) {\n            System.out.println(\"  [编辑器] 没有可撤销的操作\");\n            return;\n        }\n        text = snapshots.pop();\n        System.out.println(\"  [编辑器] 撤销，当前：\\\"\" + text + \"\\\"\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>看着能跑，但病灶清晰：**撤销的单位是\"整个文档\"，而不是\"那一步操作\"。**String 倒还扛得住，换成 10MB 的富文档对象，每敲一个字存一份快照，内存曲线直接起飞。更糟的是\"撤销\"这个能力被焊死在编辑器内部——想加重做、宏录制、操作日志，全得往这一个类里继续堆。\u003C\u002Fp>\n\u003Ch3>4.2 正解：每一步操作变成一个命令对象\u003C\u002Fh3>\n\u003Cp>第一步，命令接口——除了执行，撤销是命令模式的招牌能力：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface Command {\n    void execute();\n    void undo();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第二步，接收者。一个最朴素的文本编辑器：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class TextEditor {\n    private String text = \"\";\n\n    public void insert(String s) { text = text + s; }\n    public void delete(int count) { text = text.substring(0, text.length() - count); }\n    public String getText() { return text; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第三步，具体命令。插入命令的执行和撤销是一对完美互逆：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class InsertCommand implements Command {\n\n    private final TextEditor editor;\n    private final String s;\n\n    public InsertCommand(TextEditor editor, String s) {\n        this.editor = editor;\n        this.s = s;\n    }\n\n    @Override\n    public void execute() {\n        editor.insert(s);\n        System.out.println(\"  [插入] \\\"\" + s + \"\\\"，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n\n    @Override\n    public void undo() {\n        editor.delete(s.length());\n        System.out.println(\"  [撤销插入] \\\"\" + s + \"\\\" 被移除，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>删除命令多一个心眼——\u003Cstrong>执行时把删掉的内容记下来\u003C\u002Fstrong>，不然撤销时拿什么还原？\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class DeleteCommand implements Command {\n\n    private final TextEditor editor;\n    private final int count;\n    private String deleted;   \u002F\u002F 执行时记下被删的内容，撤销时才能还原\n\n    public DeleteCommand(TextEditor editor, int count) {\n        this.editor = editor;\n        this.count = count;\n    }\n\n    @Override\n    public void execute() {\n        String current = editor.getText();\n        deleted = current.substring(current.length() - count);\n        editor.delete(count);\n        System.out.println(\"  [删除] 删掉 \\\"\" + deleted + \"\\\"，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n\n    @Override\n    public void undo() {\n        editor.insert(deleted);\n        System.out.println(\"  [撤销删除] \\\"\" + deleted + \"\\\" 回来了，当前：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第四步，调用者。它只认识 Command 接口，干的就三件事：执行、记账、反悔：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class CommandHistory {\n\n    private final Deque&lt;Command&gt; history = new ArrayDeque&lt;&gt;();\n\n    public void execute(Command command) {\n        command.execute();\n        history.push(command);\n    }\n\n    public void undo() {\n        if (history.isEmpty()) {\n            System.out.println(\"  [历史] 没有可撤销的操作\");\n            return;\n        }\n        history.pop().undo();\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最后串起来，四条命令压栈，再连按五次撤销：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class CommandTest {\n\n    public static void main(String[] args) {\n        System.out.println(\"========== 反面教材：快照式撤销 ==========\");\n        BadEditor bad = new BadEditor();\n        bad.insert(\"设计模式\");\n        bad.delete(4);\n        bad.insert(\"修炼之路\");\n        bad.undo();\n        bad.undo();\n        bad.undo();\n        bad.undo();\n\n        System.out.println();\n        System.out.println(\"========== 正解：命令模式 ==========\");\n        TextEditor editor = new TextEditor();\n        CommandHistory history = new CommandHistory();\n\n        history.execute(new InsertCommand(editor, \"设计\"));\n        history.execute(new InsertCommand(editor, \"模式\"));\n        history.execute(new DeleteCommand(editor, 2));\n        history.execute(new InsertCommand(editor, \"修炼\"));\n        System.out.println(\"  当前文本：\\\"\" + editor.getText() + \"\\\"\");\n\n        System.out.println();\n        System.out.println(\"-- 连按五次撤销，按执行逆序回放 --\");\n        history.undo();\n        history.undo();\n        history.undo();\n        history.undo();\n        history.undo();\n        System.out.println(\"  当前文本：\\\"\" + editor.getText() + \"\\\"\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">========== 反面教材：快照式撤销 ==========\n  [编辑器] 插入 \"设计模式\"，当前：\"设计模式\"\n  [编辑器] 删除 4 个字符，当前：\"\"\n  [编辑器] 插入 \"修炼之路\"，当前：\"修炼之路\"\n  [编辑器] 撤销，当前：\"\"\n  [编辑器] 撤销，当前：\"设计模式\"\n  [编辑器] 撤销，当前：\"\"\n  [编辑器] 没有可撤销的操作\n\n========== 正解：命令模式 ==========\n  [插入] \"设计\"，当前：\"设计\"\n  [插入] \"模式\"，当前：\"设计模式\"\n  [删除] 删掉 \"模式\"，当前：\"设计\"\n  [插入] \"修炼\"，当前：\"设计修炼\"\n  当前文本：\"设计修炼\"\n\n-- 连按五次撤销，按执行逆序回放 --\n  [撤销插入] \"修炼\" 被移除，当前：\"设计\"\n  [撤销删除] \"模式\" 回来了，当前：\"设计模式\"\n  [撤销插入] \"模式\" 被移除，当前：\"设计\"\n  [撤销插入] \"设计\" 被移除，当前：\"\"\n  [历史] 没有可撤销的操作\n  当前文本：\"\"\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>4.3 三处细节咂摸一下\u003C\u002Fh3>\n\u003Cp>**细节一：撤销的本质是\"逆操作\"，不是\"回滚\"。**反面教材的撤销单位是整个文档（快照），命令模式的撤销单位是那一步操作本身（增量）。撤销一条插入只需删掉那几个字，撤销一条删除只需补回那几个字——文档 10MB 也好 100MB 也好，每条命令只携带自己的增量。Ctrl+Z 能做到瞬响应，靠的就是这个。\u003C\u002Fp>\n\u003Cp>**细节二：命令对象是有状态的。**DeleteCommand 的 \u003Ccode>deleted\u003C\u002Fcode> 字段在执行那一刻才被填上——同一个命令对象，执行前和执行后携带的信息不一样。这一点和策略模式形成了鲜明对比（策略通常无状态），第六节展开。\u003C\u002Fp>\n\u003Cp>**细节三：CommandHistory 完全不知道 TextEditor 的存在。**它只认识 Command 接口，账本里存的也全是接口引用。这意味着\"历史记录\"这个能力可以原封不动地搬到任何命令体系上：绘图软件的撤销栈、数据库的事务日志、游戏录像的回放器——换个命令实现，调用者一行不改。\u003C\u002Fp>\n\u003Ch3>4.4 白送的能力\u003C\u002Fh3>\n\u003Cp>把请求变成对象之后，很多东西是\"白送\"的——调用者已经持有命令对象了，剩下的只是用法问题：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>重做\u003C\u002Fstrong>：undo 时别扔，压进另一个 redo 栈；Ctrl+Z 撤掉的 Ctrl+Y 再执行回来。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>宏录制\u003C\u002Fstrong>：\u003Ccode>List&lt;Command&gt;\u003C\u002Fcode> 就是一个宏。再套一层 CompositeCommand（内部持有一组命令，execute 遍历执行、undo 逆序回滚）——组合模式 ⑨ 的经典复用。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作日志\u003C\u002Fstrong>：execute 时把命令序列化进日志，系统宕机后按序重放，现场自动恢复——数据库 WAL、Redis AOF 的思路同源。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>排队与调度\u003C\u002Fstrong>：命令对象扔进 BlockingQueue，什么时候执行、在哪个线程执行、失败了隔多久重试，全是调度层说了算——线程池干的就是这个买卖，第五节看实物。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Cp>命令模式在 JDK 里有一个官方盖章的铁证——线程池的执行方法签名：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface Executor {\n    void execute(Runnable command);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>参数名就叫 \u003Ccode>command\u003C\u002Fcode>，JDK 自己承认了。\u003Ccode>Runnable\u003C\u002Fcode> 是命令接口，你的任务逻辑是命令对象，\u003Ccode>ThreadPoolExecutor\u003C\u002Fcode> 是调用者：它把命令对象排进队列（排队）、延迟调度（延迟执行）、必要时拒绝或重试。\u003Cstrong>\"把任务扔进线程池\"这个每天写的动作，本质上和\"把订单交给服务员\"一模一样。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>而且 JDK 在这里把命令接口的演化史也一并演示了。Java 8 之前提交一个任务得写匿名内部类，命令模式的\"类爆炸\"缺点被诟病多年；lambda 之后：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">executor.submit(() -&gt; orderService.close(orderId));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>一行完事。函数式接口让命令模式从\"重武器\"退化成了日常空气——你天天在用，只是没意识到。\u003C\u002Fp>\n\u003Cp>其他几处：\u003C\u002Fp>\n\u003Cp>**Swing Action。**按钮和菜单项共用同一个 Action，点谁都是执行同一份命令——\"工具栏的保存按钮\"和\"Ctrl+S\"背后是同一个对象，启用禁用状态还能联动。GUI 领域命令模式的老前辈。\u003C\u002Fp>\n\u003Cp>**Netty EventLoop。**每个 EventLoop 一条线程一个任务队列，普通任务和定时任务都是排队的命令对象——Netty 的定时任务能力就是\"命令 + 优先级队列\"的组合。\u003C\u002Fp>\n\u003Cp>**MQ 消息。**生产者把\"希望消费者干什么\"序列化成消息体扔进队列，消费者取出来反序列化执行。这是命令模式的跨进程放大版：发起者和执行者不仅解耦，连进程、机器、时区都解耦了。事件驱动架构的底牌之一。\u003C\u002Fp>\n\u003Cp>一句话总结这一节：\u003Cstrong>凡是\"请求要离开发起者独立旅行\"的地方，命令模式都在场。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>六、命令 vs 策略：结构上的孪生兄弟\u003C\u002Fh2>\n\u003Cp>写到这你可能后背发凉：接口 + 一堆实现类 + 一个持有者——这不就是 ⑮ 策略模式吗？长得确实几乎一样，区别全在灵魂上：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>策略模式 ⑮\u003C\u002Fth>\n   \u003Cth>命令模式 ⑱\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>打包的是什么\u003C\u002Ftd>\n   \u003Ctd>一种**算法**（怎么做）\u003C\u002Ftd>\n   \u003Ctd>一次**请求**（做什么）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>生命周期\u003C\u002Ftd>\n   \u003Ctd>用完即弃，不关心历史\u003C\u002Ftd>\n   \u003Ctd>可存储、可排队、可撤销、可重放\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>有状态吗\u003C\u002Ftd>\n   \u003Ctd>通常无状态\u003C\u002Ftd>\n   \u003Ctd>通常有状态（执行时记增量，撤销时要还）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>持有者持几个\u003C\u002Ftd>\n   \u003Ctd>Context 持**一个**，随时可换\u003C\u002Ftd>\n   \u003Ctd>Invoker 持**一摞**（历史、队列）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型场景\u003C\u002Ftd>\n   \u003Ctd>促销算法、支付渠道\u003C\u002Ftd>\n   \u003Ctd>撤销栈、任务队列\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>分辨口诀：**策略回答\"这事有几种做法\"，命令回答\"这事能不能存起来反悔\"。**同样是接口加实现类，策略是给调用方换算法的插槽，命令是给系统留的记账凭证。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>命令模式一句话总结：\u003Cstrong>把请求封装成对象，执行与发起就此分离；对象能存能传能排队，撤销、重做、日志、重放全是白送。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>下一篇 ⑲ 状态模式——换个场景，电商订单。\"待支付\"的订单点取消，直接关单退款都不用；\"已发货\"的订单点取消，只能走退货流程；同一个按钮，状态不同行为完全不同。如果用 if-else 判断状态，状态一多每个方法都是灾难现场。\u003Cstrong>把每个状态做成类，状态一换行为整套跟着换\u003C\u002Fstrong>——状态模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113932208883372034","design-pattern-17","设计模式修炼之路 ⑰ 责任链模式——请求沿链条传递","客户端只认识链头、节点自治三段式、纯链与管线式、从 Servlet Filter 到 Netty Pipeline",3708,5950,15,"2024-07-23T20:24:49",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113934123146940417","design-pattern-21","设计模式修炼之路 ㉑ 备忘录模式——不破坏封装的存档术","不破坏封装的存档术、宽窄接口、Git 与 Savepoint、命令+备忘录合订",3711,6474,17,"2024-10-08T16:37:17",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":61,"publishedAt":70},"2113933842262790145","design-pattern-19","设计模式修炼之路 ⑲ 状态模式——状态驱动行为","状态驱动行为、流转规则住进状态家、Thread 状态机、状态 vs 策略终局对决",1320,6715,"2024-09-02T16:09:32",{"code":4,"message":5,"data":72},{"prev":73,"next":74},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":61,"publishedAt":70},{"code":4,"message":5,"data":76},[77,83,88,94],{"id":78,"name":79,"slug":80,"description":81,"sortOrder":4,"articleCount":82},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":84,"slug":85,"description":86,"sortOrder":4,"articleCount":87},"设计模式","design-pattern","设计模式系列","34",{"id":89,"name":90,"slug":91,"description":92,"sortOrder":33,"articleCount":93},"1946000000000000002","随笔","essay","生活与思考","0",{"id":95,"name":96,"slug":97,"description":98,"sortOrder":99,"articleCount":100},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":102,"toc":103},"\u003Cblockquote>\n \u003Cp>行为型第五站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇结尾说：责任链传的\"请求\"始终只是个参数，命令模式更进一步——\u003Cstrong>把请求本身变成对象\u003C\u002Fstrong>。这一篇兑现承诺。\u003C\u002Fp>\n\u003Cp>场景就从最日常的编辑器讲起。你写文档时手一抖删了一段，按 Ctrl+Z，内容回来了；再按 Ctrl+Y，又删掉了。撤销、重做、任务队列、宏录制……这些功能的第一块基石，都是命令模式。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、命令模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. （将一个请求封装成一个对象，从而使你可以用不同的请求对客户进行参数化；可以对请求排队、记录日志，以及支持可撤销的操作。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：**把\"要干的事\"打包成对象。**调用方手里拿的不再是\"干这件事\"的过程调用，而是\"干这件事\"这个对象本身——而对象能存储、能传递、能排队、能记账、能反悔。\u003C\u002Fp>\n\u003Cp>四个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Command（抽象命令）\u003C\u002Ftd>\n   \u003Ctd>定义执行与撤销的统一接口\u003C\u002Ftd>\n   \u003Ctd>Command\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteCommand（具体命令）\u003C\u002Ftd>\n   \u003Ctd>绑定接收者，实现\"怎么执行、怎么撤销\"\u003C\u002Ftd>\n   \u003Ctd>InsertCommand \u002F DeleteCommand\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Receiver（接收者）\u003C\u002Ftd>\n   \u003Ctd>真正干活的对象\u003C\u002Ftd>\n   \u003Ctd>TextEditor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Invoker（调用者）\u003C\u002Ftd>\n   \u003Ctd>持有命令、负责执行、记录历史\u003C\u002Ftd>\n   \u003Ctd>CommandHistory\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>最好的类比是餐厅点单：你（客户端）不冲进后厨喊\"来份宫保鸡丁\"（直接方法调用），而是把需求写在订单上（命令对象）交给服务员（调用者）。服务员拿着订单能干的事就多了：立刻递后厨（执行）、压单等位（排队）、你说错了撤单（撤销）、月底翻存根对账（日志）。\u003Cstrong>订单本身不会炒菜——真正炒菜的是后厨（接收者）\u003C\u002Fstrong>，订单只是把\"要干什么\"固化成了可搬运的纸片。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>请求与执行解耦\u003C\u002Fstrong>。调用者只认识 Command 接口，根本不知道接收者是谁、操作怎么落地——命令对象把这一切打包带走了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>撤销是白送的\u003C\u002Fstrong>。命令知道自己\"干了什么\"，就有能力描述\"怎么反着干\"。调用者只管把命令压进历史栈，撤销时弹出来调 undo 即可。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>排队、延迟、重试都是白送的\u003C\u002Fstrong>。命令是对象，对象就能进队列。进队列之后什么时候执行、在哪个线程执行、失败了几时重试，全是调度层的自由。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>可组合\u003C\u002Fstrong>。一组命令可以拼成一个宏命令（组合模式 ⑨ 的老朋友），一键回放。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>。每个操作一个命令类，操作多了类也多——Java 8 之前写起来相当啰嗦（lambda 缓解了大半，见第五节）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>间接层带来的理解成本\u003C\u002Fstrong>。看代码时找不到直接调用，执行逻辑藏在命令对象的 execute 里，追起来要绕一层。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>撤销的实现并不总是轻巧\u003C\u002Fstrong>。多数操作可以定义逆操作，但\"发邮件\"这种不可逆操作，撤销只能语义补偿（再发一封道歉邮件？），设计时要掂量。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>\"请求\"需要在时间或空间上离开\"发起者\"——要排队、要延迟执行、要事后撤销、要记录重放，或者要跨线程跨进程搬运。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>撤销\u002F重做\u003C\u002Fstrong>：编辑器、绘图工具、数据库事务——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>任务队列\u003C\u002Fstrong>：线程池、消息队列、定时任务，队列里排的一个个任务全是命令对象\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作日志与恢复\u003C\u002Fstrong>：把命令序列化进日志，宕机后重放恢复现场\u003C\u002Fli>\n \u003Cli>\u003Cstrong>宏录制\u003C\u002Fstrong>：把用户一连串操作录成命令列表，一键回放\u003C\u002Fli>\n \u003Cli>\u003Cstrong>跨进程分发\u003C\u002Fstrong>：MQ 的消息体，本质是\"生产者想让消费者干的活\"的对象化\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：请求发起后立刻执行、执行完就完、不需要历史——直接调用方法就好，别为了模式而模式。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、实现方式：增量式撤销\u003C\u002Fh2>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：快照式撤销\u003C\u002Fh3>\n\u003Cp>撤销最容易想到的做法：每干一步，存一份全文快照，撤销就回滚到上一份。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadEditor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 为了撤销，只能每一步都存一份全文快照\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Deque&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> snapshots \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayDeque&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">s\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        snapshots.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">push\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [编辑器] 插入 \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> delete\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> count\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        snapshots.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">push\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">substring\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">length\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [编辑器] 删除 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 个字符，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 撤销 = 回滚到上一份快照\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 文本是 String 还好；如果编辑的是 10MB 的文档对象，每一步存一份快照，内存直接爆炸\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 而且想加\"重做\"、\"宏录制\"？全部要在这个类里继续堆代码\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (snapshots.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isEmpty\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [编辑器] 没有可撤销的操作\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> snapshots.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">pop\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [编辑器] 撤销，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>看着能跑，但病灶清晰：**撤销的单位是\"整个文档\"，而不是\"那一步操作\"。**String 倒还扛得住，换成 10MB 的富文档对象，每敲一个字存一份快照，内存曲线直接起飞。更糟的是\"撤销\"这个能力被焊死在编辑器内部——想加重做、宏录制、操作日志，全得往这一个类里继续堆。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：每一步操作变成一个命令对象\u003C\u002Fh3>\n\u003Cp>第一步，命令接口——除了执行，撤销是命令模式的招牌能力：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第二步，接收者。一个最朴素的文本编辑器：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> TextEditor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">s\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> delete\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> count\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">substring\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">length\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count); }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text; }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第三步，具体命令。插入命令的执行和撤销是一对完美互逆：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InsertCommand\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> TextEditor editor;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String s;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InsertCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(TextEditor \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">editor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">s\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.editor \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.s \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(s);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [插入] \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">delete\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(s.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">length\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [撤销插入] \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 被移除，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>删除命令多一个心眼——\u003Cstrong>执行时把删掉的内容记下来\u003C\u002Fstrong>，不然撤销时拿什么还原？\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DeleteCommand\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> TextEditor editor;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String deleted;   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 执行时记下被删的内容，撤销时才能还原\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DeleteCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(TextEditor \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">editor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> count\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.editor \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.count \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String current \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        deleted \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">substring\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(current.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">length\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">delete\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(count);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [删除] 删掉 \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> deleted \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(deleted);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [撤销删除] \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> deleted \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 回来了，当前：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第四步，调用者。它只认识 Command 接口，干的就三件事：执行、记账、反悔：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CommandHistory\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Deque&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> history \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayDeque&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Command \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        command.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">push\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(command);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isEmpty\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  [历史] 没有可撤销的操作\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">pop\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最后串起来，四条命令压栈，再连按五次撤销：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CommandTest\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> main\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[] \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">args\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"========== 反面教材：快照式撤销 ==========\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        BadEditor bad \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadEditor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"设计模式\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">delete\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">4\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">insert\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"修炼之路\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bad.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"========== 正解：命令模式 ==========\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        TextEditor editor \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> TextEditor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        CommandHistory history \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CommandHistory\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InsertCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(editor, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"设计\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InsertCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(editor, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"模式\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DeleteCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(editor, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> InsertCommand\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(editor, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"修炼\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  当前文本：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-- 连按五次撤销，按执行逆序回放 --\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        history.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">undo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  当前文本：\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：快照式撤销 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 插入 \"设计模式\"，当前：\"设计模式\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 删除 4 个字符，当前：\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 插入 \"修炼之路\"，当前：\"修炼之路\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 撤销，当前：\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 撤销，当前：\"设计模式\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 撤销，当前：\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [编辑器] 没有可撤销的操作\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>========== 正解：命令模式 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [插入] \"设计\"，当前：\"设计\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [插入] \"模式\"，当前：\"设计模式\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [删除] 删掉 \"模式\"，当前：\"设计\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [插入] \"修炼\"，当前：\"设计修炼\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  当前文本：\"设计修炼\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 连按五次撤销，按执行逆序回放 --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [撤销插入] \"修炼\" 被移除，当前：\"设计\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [撤销删除] \"模式\" 回来了，当前：\"设计模式\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [撤销插入] \"模式\" 被移除，当前：\"设计\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [撤销插入] \"设计\" 被移除，当前：\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [历史] 没有可撤销的操作\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  当前文本：\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"toc-7\">4.3 三处细节咂摸一下\u003C\u002Fh3>\n\u003Cp>**细节一：撤销的本质是\"逆操作\"，不是\"回滚\"。**反面教材的撤销单位是整个文档（快照），命令模式的撤销单位是那一步操作本身（增量）。撤销一条插入只需删掉那几个字，撤销一条删除只需补回那几个字——文档 10MB 也好 100MB 也好，每条命令只携带自己的增量。Ctrl+Z 能做到瞬响应，靠的就是这个。\u003C\u002Fp>\n\u003Cp>**细节二：命令对象是有状态的。**DeleteCommand 的 \u003Ccode>deleted\u003C\u002Fcode> 字段在执行那一刻才被填上——同一个命令对象，执行前和执行后携带的信息不一样。这一点和策略模式形成了鲜明对比（策略通常无状态），第六节展开。\u003C\u002Fp>\n\u003Cp>**细节三：CommandHistory 完全不知道 TextEditor 的存在。**它只认识 Command 接口，账本里存的也全是接口引用。这意味着\"历史记录\"这个能力可以原封不动地搬到任何命令体系上：绘图软件的撤销栈、数据库的事务日志、游戏录像的回放器——换个命令实现，调用者一行不改。\u003C\u002Fp>\n\u003Ch3 id=\"toc-8\">4.4 白送的能力\u003C\u002Fh3>\n\u003Cp>把请求变成对象之后，很多东西是\"白送\"的——调用者已经持有命令对象了，剩下的只是用法问题：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>重做\u003C\u002Fstrong>：undo 时别扔，压进另一个 redo 栈；Ctrl+Z 撤掉的 Ctrl+Y 再执行回来。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>宏录制\u003C\u002Fstrong>：\u003Ccode>List&lt;Command&gt;\u003C\u002Fcode> 就是一个宏。再套一层 CompositeCommand（内部持有一组命令，execute 遍历执行、undo 逆序回滚）——组合模式 ⑨ 的经典复用。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作日志\u003C\u002Fstrong>：execute 时把命令序列化进日志，系统宕机后按序重放，现场自动恢复——数据库 WAL、Redis AOF 的思路同源。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>排队与调度\u003C\u002Fstrong>：命令对象扔进 BlockingQueue，什么时候执行、在哪个线程执行、失败了隔多久重试，全是调度层说了算——线程池干的就是这个买卖，第五节看实物。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-9\">五、在框架里找它\u003C\u002Fh2>\n\u003Cp>命令模式在 JDK 里有一个官方盖章的铁证——线程池的执行方法签名：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Executor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Runnable \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">command\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>参数名就叫 \u003Ccode>command\u003C\u002Fcode>，JDK 自己承认了。\u003Ccode>Runnable\u003C\u002Fcode> 是命令接口，你的任务逻辑是命令对象，\u003Ccode>ThreadPoolExecutor\u003C\u002Fcode> 是调用者：它把命令对象排进队列（排队）、延迟调度（延迟执行）、必要时拒绝或重试。\u003Cstrong>\"把任务扔进线程池\"这个每天写的动作，本质上和\"把订单交给服务员\"一模一样。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>而且 JDK 在这里把命令接口的演化史也一并演示了。Java 8 之前提交一个任务得写匿名内部类，命令模式的\"类爆炸\"缺点被诟病多年；lambda 之后：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">executor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">submit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orderService.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">close\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(orderId));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>一行完事。函数式接口让命令模式从\"重武器\"退化成了日常空气——你天天在用，只是没意识到。\u003C\u002Fp>\n\u003Cp>其他几处：\u003C\u002Fp>\n\u003Cp>**Swing Action。**按钮和菜单项共用同一个 Action，点谁都是执行同一份命令——\"工具栏的保存按钮\"和\"Ctrl+S\"背后是同一个对象，启用禁用状态还能联动。GUI 领域命令模式的老前辈。\u003C\u002Fp>\n\u003Cp>**Netty EventLoop。**每个 EventLoop 一条线程一个任务队列，普通任务和定时任务都是排队的命令对象——Netty 的定时任务能力就是\"命令 + 优先级队列\"的组合。\u003C\u002Fp>\n\u003Cp>**MQ 消息。**生产者把\"希望消费者干什么\"序列化成消息体扔进队列，消费者取出来反序列化执行。这是命令模式的跨进程放大版：发起者和执行者不仅解耦，连进程、机器、时区都解耦了。事件驱动架构的底牌之一。\u003C\u002Fp>\n\u003Cp>一句话总结这一节：\u003Cstrong>凡是\"请求要离开发起者独立旅行\"的地方，命令模式都在场。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">六、命令 vs 策略：结构上的孪生兄弟\u003C\u002Fh2>\n\u003Cp>写到这你可能后背发凉：接口 + 一堆实现类 + 一个持有者——这不就是 ⑮ 策略模式吗？长得确实几乎一样，区别全在灵魂上：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>策略模式 ⑮\u003C\u002Fth>\n   \u003Cth>命令模式 ⑱\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>打包的是什么\u003C\u002Ftd>\n   \u003Ctd>一种**算法**（怎么做）\u003C\u002Ftd>\n   \u003Ctd>一次**请求**（做什么）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>生命周期\u003C\u002Ftd>\n   \u003Ctd>用完即弃，不关心历史\u003C\u002Ftd>\n   \u003Ctd>可存储、可排队、可撤销、可重放\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>有状态吗\u003C\u002Ftd>\n   \u003Ctd>通常无状态\u003C\u002Ftd>\n   \u003Ctd>通常有状态（执行时记增量，撤销时要还）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>持有者持几个\u003C\u002Ftd>\n   \u003Ctd>Context 持**一个**，随时可换\u003C\u002Ftd>\n   \u003Ctd>Invoker 持**一摞**（历史、队列）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型场景\u003C\u002Ftd>\n   \u003Ctd>促销算法、支付渠道\u003C\u002Ftd>\n   \u003Ctd>撤销栈、任务队列\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>分辨口诀：**策略回答\"这事有几种做法\"，命令回答\"这事能不能存起来反悔\"。**同样是接口加实现类，策略是给调用方换算法的插槽，命令是给系统留的记账凭证。\u003C\u002Fp>\n\u003Ch2 id=\"toc-11\">写在最后\u003C\u002Fh2>\n\u003Cp>命令模式一句话总结：\u003Cstrong>把请求封装成对象，执行与发起就此分离；对象能存能传能排队，撤销、重做、日志、重放全是白送。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>下一篇 ⑲ 状态模式——换个场景，电商订单。\"待支付\"的订单点取消，直接关单退款都不用；\"已发货\"的订单点取消，只能走退货流程；同一个按钮，状态不同行为完全不同。如果用 if-else 判断状态，状态一多每个方法都是灾难现场。\u003Cstrong>把每个状态做成类，状态一换行为整套跟着换\u003C\u002Fstrong>——状态模式，下一篇见。\u003C\u002Fp>",[104,107,110,113,116,120,123,126,129,132,135],{"id":105,"text":106,"level":99},"toc-1","一、命令模式是什么",{"id":108,"text":109,"level":99},"toc-2","二、优点与缺点",{"id":111,"text":112,"level":99},"toc-3","三、什么时候用",{"id":114,"text":115,"level":99},"toc-4","四、实现方式：增量式撤销",{"id":117,"text":118,"level":119},"toc-5","4.1 反面教材：快照式撤销",3,{"id":121,"text":122,"level":119},"toc-6","4.2 正解：每一步操作变成一个命令对象",{"id":124,"text":125,"level":119},"toc-7","4.3 三处细节咂摸一下",{"id":127,"text":128,"level":119},"toc-8","4.4 白送的能力",{"id":130,"text":131,"level":99},"toc-9","五、在框架里找它",{"id":133,"text":134,"level":99},"toc-10","六、命令 vs 策略：结构上的孪生兄弟",{"id":136,"text":137,"level":99},"toc-11","写在最后"]