[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-24":26,"\u002Farticles\u002Fdesign-pattern-24\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-24\u002Fadjacent":72,"\u002Fcategories":76,"render-design-pattern-24":102},{"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},"2113934492581236737","design-pattern-24","设计模式修炼之路 ㉔ 访问者模式——结构与操作分离","双分派、结构与操作分离、visit(this) 不能省、FileVisitor 与 ASM","2113897628365230081",1,2450,5892,15,"2024-12-12T14:45:37","> 行为型第十一站。\n\n上一篇的 Member 有 VIP 与普通之分，折扣各不相同——好在行为简单，直接长在每个类自己身上，干净利落。这一篇情况反过来：一群**类型稳定**的对象，要面对**频繁变化的一堆操作**。\n\n拿本文正在用的文档举例子：段落、表格、图片——这哥仨十年不变。可\"要对它们做的事\"天天出新：字数统计、导出 Markdown、导出 HTML、导出 PDF、SEO 分析、无障碍校验……把每种操作做成每个类的方法，加一种操作，九个类全体手术；写在类外面，`instanceof` 分支满天飞，还一点类型安全都没有。\n\n**把\"对一群对象做什么\"从对象家里抽出来，装进访问者自己家，双分派穿针引线**——访问者模式，GoF 二十三式的倒数第三位狠角色。\n\n## 一、访问者模式是什么\n\nGoF 的原始定义：\n\n> Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.\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>Visitor（抽象访问者）\u003C\u002Ftd>\u003Ctd>对每种具体元素各声明一个重载入口\u003C\u002Ftd>\u003Ctd>DocVisitor\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>ConcreteVisitor（具体访问者）\u003C\u002Ftd>\u003Ctd>一个访问者 = 一个完整的操作\u003C\u002Ftd>\u003Ctd>CountVisitor \u002F MarkdownExportVisitor \u002F HtmlExportVisitor\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Element（抽象元素）\u003C\u002Ftd>\u003Ctd>声明 accept，接待访问者\u003C\u002Ftd>\u003Ctd>DocElement\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>ConcreteElement（具体元素）\u003C\u002Ftd>\u003Ctd>实现 accept，把自己递给访问者\u003C\u002Ftd>\u003Ctd>Paragraph \u002F Table \u002F Image\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>ObjectStructure（对象结构）\u003C\u002Ftd>\u003Ctd>能枚举所有元素的高层容器\u003C\u002Ftd>\u003Ctd>List\\\u003CDocElement\\>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **新增操作零侵入**：加一种操作 = 加一个 Visitor 类，五个元素类一行不改——开闭原则教科书级兑现。\n2. **操作内聚**：一种操作的全部代码住在一个访问者类里，而不是切成碎片散落在九个类中。Markdown 导出的所有细节，MarkdownExportVisitor 一个文件看完。\n3. **类型安全**：新增元素类型时，所有访问者会被编译器挨个揪出来强制补充实现——`instanceof` 链只会默默漏掉新类型，访问者不会。\n4. **操作可携带状态**：访问者是普通对象，可以积累中间结果（CountVisitor 边走边数），这是多态方法做不到的。\n\n**缺点：**\n\n1. **破坏封装**：访问者要干活就得调元素的 getter——Paragraph 得暴露 getText，Table 得暴露 getCells。\"操作住在外面\"的代价就是元素的结构细节必须可见。\n2. **加元素类型是灾难**：新增一种元素（比如音频），DocVisitor 接口加方法、所有现存访问者全体手术——它把\"加操作的痛\"原样转嫁给了\"加元素类型\"。\n3. **双分派晦涩**：`accept(visitor)` + `visit(this)` 的接力赛，初读每个字都认识，连起来看不懂，必须理解两次分派的机制。\n4. **适用面窄**：押注\"结构稳定、操作常变\"——押错了（两边都常变）就是双重灾难。\n\n## 三、什么时候用\n\n判断标准：**对象结构稳定不常加类型，但作用于结构的操作频繁新增，且操作种类 > 元素种类、或操作属于跨切面的\"外挂\"逻辑。**\n\n- **文档\u002F报表生成**：结构（段落\u002F表格\u002F图）稳定，导出格式（MD\u002FHTML\u002FPDF\u002FWord）天天加——本文场景\n- **编译器语法树**：AST 节点类型由语言规范锁死，而遍历操作（类型检查、优化、代码生成）层出不穷——访问者的出生地\n- **字节码处理**：类结构由 JVM 规范锁死，改字节码的工具却百花齐放\n- **文件系统遍历**：目录\u002F文件两类元素万年不变，对它们的操作千变万化\n\n反面对照：**元素类型预期会增长**（业务模型还在演化）——访问者会把每次扩容变成全体手术，改回多态方法；**只有一两种操作**——老实写方法或工具类，别上访问者；**操作和元素强绑定、属于元素的本职**（段落渲染自己的文本）——那本来就该是它的方法。\n\n## 四、实现方式：文档导出\n\n### 4.1 反面教材：instanceof 链\n\n操作写在结构外面，最直觉的做法就是类型判断：\n\n```java\npublic class BadDocTool {\n\n    \u002F\u002F 操作一：字数统计——instanceof 链一号\n    public static void countWords(List\u003CDocElement> doc) {\n        for (DocElement e : doc) {\n            if (e instanceof Paragraph) {\n                chars += ((Paragraph) e).getText().length();\n            } else if (e instanceof Table) {\n                \u002F\u002F 嵌套两层 for 数格子里的字……\n            } else if (e instanceof Image) {\n                images++;\n            }\n        }\n    }\n\n    \u002F\u002F 操作二：导出 Markdown——instanceof 链二号，和一号结构一模一样\n    public static String exportMarkdown(List\u003CDocElement> doc) { \u002F* 同款链再来一遍 *\u002F }\n}\n```\n\n运行：\n\n```\n========== 反面教材：instanceof 链 ==========\n  [统计] 字符 62 个，表格 1 张，图片 1 张\n  [导出 Markdown]\n    设计模式修炼之路：访问者篇\n\n    访问者模式把操作从对象结构中抽离出来。\n\n    | 元素 | 访问者 |\n    |---|---|\n    | Paragraph | visit(Paragraph) |\n\n    ![双分派示意图](双分派示意图.png)\n```\n\n功能没毛病，病在结构：**N 种元素 × M 种操作 = N×M 个分支盒子**，全靠人肉对齐。更阴险的是漏分支不报错——哪天加了 Audio 元素，两条链谁也不提醒你补 `instanceof (Audio)`，统计悄悄少算、导出悄悄吞音频，又是一场\"聚餐事故\"式的灵异现场（㉒ 老朋友了）。\n\n### 4.2 正解：双分派\n\n先看访问者接口——对每种元素一个重载入口，这就是\"操作 × 元素\"行为矩阵的声明处：\n\n```java\npublic interface DocVisitor {\n\n    void visit(Paragraph paragraph);\n\n    void visit(Table table);\n\n    void visit(Image image);\n}\n```\n\n元素这边只接待，不干活：\n\n```java\npublic class Paragraph implements DocElement {\n\n    @Override\n    public void accept(DocVisitor visitor) {\n        visitor.visit(this);   \u002F\u002F this 的静态类型是 Paragraph——关键伏笔\n    }\n}\n```\n\n具体访问者一：统计——一个类装下完整操作：\n\n```java\npublic class CountVisitor implements DocVisitor {\n\n    private int chars;\n    private int tables;\n    private int images;\n\n    @Override\n    public void visit(Paragraph paragraph) {\n        chars += paragraph.getText().length();\n    }\n\n    @Override\n    public void visit(Table table) {\n        tables++;\n        for (String[] row : table.getCells()) {\n            for (String cell : row) {\n                chars += cell.length();\n            }\n        }\n    }\n\n    @Override\n    public void visit(Image image) {\n        images++;\n    }\n\n    public void report() { \u002F* 打印统计结果 *\u002F }\n}\n```\n\nMarkdown 导出、HTML 导出同款套路各一个类。测试里三路访问者一起上：\n\n```java\nCountVisitor counter = new CountVisitor();\nMarkdownExportVisitor markdownExport = new MarkdownExportVisitor();\nHtmlExportVisitor htmlExport = new HtmlExportVisitor();\n\nfor (DocElement e : doc) {\n    e.accept(counter);\n    e.accept(markdownExport);\n    e.accept(htmlExport);\n}\n```\n\n运行：\n\n```\n========== 正解：双分派 ==========\n  [统计] 字符 62 个，表格 1 张，图片 1 张\n  [导出 Markdown]\n    设计模式修炼之路：访问者篇\n\n    访问者模式把操作从对象结构中抽离出来。\n\n    | 元素 | 访问者 |\n    |---|---|\n    | Paragraph | visit(Paragraph) |\n\n    ![双分派示意图](双分派示意图.png)\n\n  [导出 HTML]\n    \u003Chtml>\u003Cbody>\n    \u003Cp>设计模式修炼之路：访问者篇\u003C\u002Fp>\n    \u003Cp>访问者模式把操作从对象结构中抽离出来。\u003C\u002Fp>\n    \u003Ctable border=\"1\">\n      \u003Ctr>\u003Ctd>元素\u003C\u002Ftd>\u003Ctd>访问者\u003C\u002Ftd>\u003C\u002Ftr>\n      \u003Ctr>\u003Ctd>Paragraph\u003C\u002Ftd>\u003Ctd>visit(Paragraph)\u003C\u002Ftd>\u003C\u002Ftr>\n    \u003C\u002Ftable>\n    \u003Cimg src=\"双分派示意图.png\" alt=\"双分派示意图\"\u002F>\n    \u003C\u002Fbody>\u003C\u002Fhtml>\n  —— 三个操作全程只有一层 for，元素类零 if、零 instanceof ——\n```\n\n注意最后的含金量：HtmlExportVisitor 是**事后新增的操作**——写它的时候，Paragraph、Table、Image 九个类一个都没碰。这就是定义里\"不改变元素的类，定义新操作\"的兑现现场。\n\n### 4.3 三处细节\n\n**细节一：双分派解剖。** `e.accept(counter)` 表面是一行，实际是两棒接力：第一棒，虚方法分派按元素的真实类型（Paragraph）进入 `Paragraph.accept`；第二棒，`visitor.visit(this)` 中 this 的静态类型是 Paragraph，触发重载解析进入 `visit(Paragraph)`。**两棒连跑，\"元素类型 × 访问者类型\"两个运行期维度共同定位了唯一的行为**——这正是反面教材里 instanceof 在人肉完成的事，双分派把它下沉成了语言机制。\n\n**细节二：visit(this) 那行\"废话\"不能省。** 有人会想：把 accept 干掉，遍历时直接 `visitor.visit(e)` 不行吗？不行——`e` 的静态类型是 DocElement，而 Java 的重载是**编译期静态分派**，`visit(e)` 根本匹配不到 `visit(Paragraph)`，编译直接报错。所以每个具体元素里都得有那行看似重复的 `visitor.visit(this)`：只有在那里，this 的编译期类型才是具体类。双分派不是炫技，是被 Java 分派规则逼出来的生存智慧。\n\n**细节三：结构稳定是生死线。** 加操作：加一个类，天下太平（本节已演示）。加元素：比如新增 Audio——DocVisitor 接口加 `visit(Audio)`，CountVisitor、MarkdownExportVisitor、HtmlExportVisitor 全体强制手术。注意这里编译器是帮手不是敌人：它会把每一个没实现新 visit 的访问者揪出来标红，**强制你处理，绝不静默漏掉**——这正是访问者比 instanceof 链安全的地方，也是它把痛苦前置换来的尊严。\n\n## 五、在框架里找它\n\n1. **java.nio.file.FileVisitor**：`Files.walkFileTree()` 的核心接口——preVisitDirectory、visitFile、postVisitDirectory、visitFileFailed 四个回调就是\"目录树结构 × 你的操作\"。顺带一个彩蛋：配套的 SimpleFileVisitor 把四个方法全实现成空操作，让你只覆写关心的那个——㉓ 空对象模式在 JDK 里的联袂出演。\n\n2. **javax.lang.model.ElementVisitor**：注解处理器的核心 API。javac 处理 @Override、Lombok 改写字节码，背后都在用访问者遍历语法树元素——TypeElement、ExecutableElement 各有对应的 visit 重载。编译器世界就是访问者的出生地。\n\n3. **ASM 的 ClassVisitor \u002F MethodVisitor \u002F FieldVisitor**：字节码操作库——\"类结构（JVM 规范锁定）× 操作（增强、审计、插桩）\"完美命中访问者的适用条件。Spring CGLIB 动态代理底层就是 ASM：ClassVisitor 的 visitMethod 里织入拦截逻辑，动态生成子类字节码。你在 ⑬ 代理模式里看到的运行期魔法，引擎室里烧的是访问者。\n\n## 六、操作应该住在哪——三方终审\n\n同一道题（N 种元素 × M 种操作）的三种解法摊开对账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>方案\u003C\u002Fth>\u003Cth>加一种操作\u003C\u002Fth>\u003Cth>加一种元素类型\u003C\u002Fth>\u003Cth>安全性\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>操作做成元素的方法（纯多态）\u003C\u002Ftd>\u003Ctd>所有元素类各加一个方法\u003C\u002Ftd>\u003Ctd>加一个类，互不干扰\u003C\u002Ftd>\u003Ctd>编译期安全\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>外部工具类 + instanceof 链\u003C\u002Ftd>\u003Ctd>再抄一条链\u003C\u002Ftd>\u003Ctd>每条链都要改，**漏了不报错**\u003C\u002Ftd>\u003Ctd>无保护\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>访问者模式\u003C\u002Ftd>\u003Ctd>加一个 Visitor 类，元素零改动\u003C\u002Ftd>\u003Ctd>接口 + 所有访问者强制改，**漏了编译报错**\u003C\u002Ftd>\u003Ctd>编译期安全\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n看清了吗？访问者和 instanceof 链的行为矩阵完全一样，差别只在**变化的成本由谁承担**：instanceof 把结构变化的成本藏起来（静默漏掉），访问者把它摆上台面（编译报错）。它和多态方案则是镜像互补：多态对\"加类\"开闭、对\"加操作\"关门；访问者对\"加操作\"开闭、对\"加类\"关门。开闭原则的\"开\"永远只对一个变化方向开放——**先判断你的系统里哪个方向变得勤，再决定把哪个方向写进开闭**，这是本系列从 ⑨ 组合模式一路埋到这里的终极答案。\n\n## 写在最后\n\n- 结构稳定、操作常变，是访问者模式唯一的准入证\n- 双分派两棒接力：accept 按元素真实类型分派，visit(this) 按静态类型触发重载\n- visit(this) 那行不能省——Java 重载是编译期分派，被逼出来的生存智慧\n- 加操作零侵入是糖，加元素全体手术是药——糖药同源，看清再吞\n- 比 instanceof 链安全的本质：变化成本摆在编译期，而不是藏进生产事故\n\n㉔ 的文档能把结构导出成 Markdown 文本——这是\"结构到文本\"的单行道。反过来呢？给程序一段文本，比如促销规则 `\"vip && age > 18\"`，让它自己解析出结构、算出 true \u002F false，把规则写成人话、让代码读懂它——**自己实现一个小规则解释器**——解释器模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>行为型第十一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇的 Member 有 VIP 与普通之分，折扣各不相同——好在行为简单，直接长在每个类自己身上，干净利落。这一篇情况反过来：一群\u003Cstrong>类型稳定\u003C\u002Fstrong>的对象，要面对\u003Cstrong>频繁变化的一堆操作\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>拿本文正在用的文档举例子：段落、表格、图片——这哥仨十年不变。可\"要对它们做的事\"天天出新：字数统计、导出 Markdown、导出 HTML、导出 PDF、SEO 分析、无障碍校验……把每种操作做成每个类的方法，加一种操作，九个类全体手术；写在类外面，\u003Ccode>instanceof\u003C\u002Fcode> 分支满天飞，还一点类型安全都没有。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把\"对一群对象做什么\"从对象家里抽出来，装进访问者自己家，双分派穿针引线\u003C\u002Fstrong>——访问者模式，GoF 二十三式的倒数第三位狠角色。\u003C\u002Fp>\n\u003Ch2>一、访问者模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. （表示一个作用于某对象结构中各元素的操作。访问者使你可以在不改变各元素的类的前提下，定义作用于这些元素的新操作。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>定义的后半句就是它的使命宣言：\u003Cstrong>新增操作，元素类零改动\u003C\u002Fstrong>。这是全系列唯一把\"对某类东西开闭\"写得这么露骨的模式。\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>Visitor（抽象访问者）\u003C\u002Ftd>\n   \u003Ctd>对每种具体元素各声明一个重载入口\u003C\u002Ftd>\n   \u003Ctd>DocVisitor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteVisitor（具体访问者）\u003C\u002Ftd>\n   \u003Ctd>一个访问者 = 一个完整的操作\u003C\u002Ftd>\n   \u003Ctd>CountVisitor \u002F MarkdownExportVisitor \u002F HtmlExportVisitor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Element（抽象元素）\u003C\u002Ftd>\n   \u003Ctd>声明 accept，接待访问者\u003C\u002Ftd>\n   \u003Ctd>DocElement\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteElement（具体元素）\u003C\u002Ftd>\n   \u003Ctd>实现 accept，把自己递给访问者\u003C\u002Ftd>\n   \u003Ctd>Paragraph \u002F Table \u002F Image\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ObjectStructure（对象结构）\u003C\u002Ftd>\n   \u003Ctd>能枚举所有元素的高层容器\u003C\u002Ftd>\n   \u003Ctd>List\\\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>新增操作零侵入\u003C\u002Fstrong>：加一种操作 = 加一个 Visitor 类，五个元素类一行不改——开闭原则教科书级兑现。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作内聚\u003C\u002Fstrong>：一种操作的全部代码住在一个访问者类里，而不是切成碎片散落在九个类中。Markdown 导出的所有细节，MarkdownExportVisitor 一个文件看完。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>类型安全\u003C\u002Fstrong>：新增元素类型时，所有访问者会被编译器挨个揪出来强制补充实现——\u003Ccode>instanceof\u003C\u002Fcode> 链只会默默漏掉新类型，访问者不会。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作可携带状态\u003C\u002Fstrong>：访问者是普通对象，可以积累中间结果（CountVisitor 边走边数），这是多态方法做不到的。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>破坏封装\u003C\u002Fstrong>：访问者要干活就得调元素的 getter——Paragraph 得暴露 getText，Table 得暴露 getCells。\"操作住在外面\"的代价就是元素的结构细节必须可见。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>加元素类型是灾难\u003C\u002Fstrong>：新增一种元素（比如音频），DocVisitor 接口加方法、所有现存访问者全体手术——它把\"加操作的痛\"原样转嫁给了\"加元素类型\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>双分派晦涩\u003C\u002Fstrong>：\u003Ccode>accept(visitor)\u003C\u002Fcode> + \u003Ccode>visit(this)\u003C\u002Fcode> 的接力赛，初读每个字都认识，连起来看不懂，必须理解两次分派的机制。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>适用面窄\u003C\u002Fstrong>：押注\"结构稳定、操作常变\"——押错了（两边都常变）就是双重灾难。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>对象结构稳定不常加类型，但作用于结构的操作频繁新增，且操作种类 &gt; 元素种类、或操作属于跨切面的\"外挂\"逻辑。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>文档\u002F报表生成\u003C\u002Fstrong>：结构（段落\u002F表格\u002F图）稳定，导出格式（MD\u002FHTML\u002FPDF\u002FWord）天天加——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>编译器语法树\u003C\u002Fstrong>：AST 节点类型由语言规范锁死，而遍历操作（类型检查、优化、代码生成）层出不穷——访问者的出生地\u003C\u002Fli>\n \u003Cli>\u003Cstrong>字节码处理\u003C\u002Fstrong>：类结构由 JVM 规范锁死，改字节码的工具却百花齐放\u003C\u002Fli>\n \u003Cli>\u003Cstrong>文件系统遍历\u003C\u002Fstrong>：目录\u002F文件两类元素万年不变，对它们的操作千变万化\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>元素类型预期会增长\u003C\u002Fstrong>（业务模型还在演化）——访问者会把每次扩容变成全体手术，改回多态方法；\u003Cstrong>只有一两种操作\u003C\u002Fstrong>——老实写方法或工具类，别上访问者；\u003Cstrong>操作和元素强绑定、属于元素的本职\u003C\u002Fstrong>（段落渲染自己的文本）——那本来就该是它的方法。\u003C\u002Fp>\n\u003Ch2>四、实现方式：文档导出\u003C\u002Fh2>\n\u003Ch3>4.1 反面教材：instanceof 链\u003C\u002Fh3>\n\u003Cp>操作写在结构外面，最直觉的做法就是类型判断：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadDocTool {\n\n    \u002F\u002F 操作一：字数统计——instanceof 链一号\n    public static void countWords(List&lt;DocElement&gt; doc) {\n        for (DocElement e : doc) {\n            if (e instanceof Paragraph) {\n                chars += ((Paragraph) e).getText().length();\n            } else if (e instanceof Table) {\n                \u002F\u002F 嵌套两层 for 数格子里的字……\n            } else if (e instanceof Image) {\n                images++;\n            }\n        }\n    }\n\n    \u002F\u002F 操作二：导出 Markdown——instanceof 链二号，和一号结构一模一样\n    public static String exportMarkdown(List&lt;DocElement&gt; doc) { \u002F* 同款链再来一遍 *\u002F }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：instanceof 链 ==========\n  [统计] 字符 62 个，表格 1 张，图片 1 张\n  [导出 Markdown]\n    设计模式修炼之路：访问者篇\n\n    访问者模式把操作从对象结构中抽离出来。\n\n    | 元素 | 访问者 |\n    |---|---|\n    | Paragraph | visit(Paragraph) |\n\n    ![双分派示意图](双分派示意图.png)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>功能没毛病，病在结构：\u003Cstrong>N 种元素 × M 种操作 = N×M 个分支盒子\u003C\u002Fstrong>，全靠人肉对齐。更阴险的是漏分支不报错——哪天加了 Audio 元素，两条链谁也不提醒你补 \u003Ccode>instanceof (Audio)\u003C\u002Fcode>，统计悄悄少算、导出悄悄吞音频，又是一场\"聚餐事故\"式的灵异现场（㉒ 老朋友了）。\u003C\u002Fp>\n\u003Ch3>4.2 正解：双分派\u003C\u002Fh3>\n\u003Cp>先看访问者接口——对每种元素一个重载入口，这就是\"操作 × 元素\"行为矩阵的声明处：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface DocVisitor {\n\n    void visit(Paragraph paragraph);\n\n    void visit(Table table);\n\n    void visit(Image image);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>元素这边只接待，不干活：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Paragraph implements DocElement {\n\n    @Override\n    public void accept(DocVisitor visitor) {\n        visitor.visit(this);   \u002F\u002F this 的静态类型是 Paragraph——关键伏笔\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>具体访问者一：统计——一个类装下完整操作：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class CountVisitor implements DocVisitor {\n\n    private int chars;\n    private int tables;\n    private int images;\n\n    @Override\n    public void visit(Paragraph paragraph) {\n        chars += paragraph.getText().length();\n    }\n\n    @Override\n    public void visit(Table table) {\n        tables++;\n        for (String[] row : table.getCells()) {\n            for (String cell : row) {\n                chars += cell.length();\n            }\n        }\n    }\n\n    @Override\n    public void visit(Image image) {\n        images++;\n    }\n\n    public void report() { \u002F* 打印统计结果 *\u002F }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Markdown 导出、HTML 导出同款套路各一个类。测试里三路访问者一起上：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">CountVisitor counter = new CountVisitor();\nMarkdownExportVisitor markdownExport = new MarkdownExportVisitor();\nHtmlExportVisitor htmlExport = new HtmlExportVisitor();\n\nfor (DocElement e : doc) {\n    e.accept(counter);\n    e.accept(markdownExport);\n    e.accept(htmlExport);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：双分派 ==========\n  [统计] 字符 62 个，表格 1 张，图片 1 张\n  [导出 Markdown]\n    设计模式修炼之路：访问者篇\n\n    访问者模式把操作从对象结构中抽离出来。\n\n    | 元素 | 访问者 |\n    |---|---|\n    | Paragraph | visit(Paragraph) |\n\n    ![双分派示意图](双分派示意图.png)\n\n  [导出 HTML]\n    &lt;html&gt;&lt;body&gt;\n    &lt;p&gt;设计模式修炼之路：访问者篇&lt;\u002Fp&gt;\n    &lt;p&gt;访问者模式把操作从对象结构中抽离出来。&lt;\u002Fp&gt;\n    &lt;table border=\"1\"&gt;\n      &lt;tr&gt;&lt;td&gt;元素&lt;\u002Ftd&gt;&lt;td&gt;访问者&lt;\u002Ftd&gt;&lt;\u002Ftr&gt;\n      &lt;tr&gt;&lt;td&gt;Paragraph&lt;\u002Ftd&gt;&lt;td&gt;visit(Paragraph)&lt;\u002Ftd&gt;&lt;\u002Ftr&gt;\n    &lt;\u002Ftable&gt;\n    &lt;img src=\"双分派示意图.png\" alt=\"双分派示意图\"\u002F&gt;\n    &lt;\u002Fbody&gt;&lt;\u002Fhtml&gt;\n  —— 三个操作全程只有一层 for，元素类零 if、零 instanceof ——\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意最后的含金量：HtmlExportVisitor 是\u003Cstrong>事后新增的操作\u003C\u002Fstrong>——写它的时候，Paragraph、Table、Image 九个类一个都没碰。这就是定义里\"不改变元素的类，定义新操作\"的兑现现场。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：双分派解剖。\u003C\u002Fstrong> \u003Ccode>e.accept(counter)\u003C\u002Fcode> 表面是一行，实际是两棒接力：第一棒，虚方法分派按元素的真实类型（Paragraph）进入 \u003Ccode>Paragraph.accept\u003C\u002Fcode>；第二棒，\u003Ccode>visitor.visit(this)\u003C\u002Fcode> 中 this 的静态类型是 Paragraph，触发重载解析进入 \u003Ccode>visit(Paragraph)\u003C\u002Fcode>。\u003Cstrong>两棒连跑，\"元素类型 × 访问者类型\"两个运行期维度共同定位了唯一的行为\u003C\u002Fstrong>——这正是反面教材里 instanceof 在人肉完成的事，双分派把它下沉成了语言机制。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：visit(this) 那行\"废话\"不能省。\u003C\u002Fstrong> 有人会想：把 accept 干掉，遍历时直接 \u003Ccode>visitor.visit(e)\u003C\u002Fcode> 不行吗？不行——\u003Ccode>e\u003C\u002Fcode> 的静态类型是 DocElement，而 Java 的重载是\u003Cstrong>编译期静态分派\u003C\u002Fstrong>，\u003Ccode>visit(e)\u003C\u002Fcode> 根本匹配不到 \u003Ccode>visit(Paragraph)\u003C\u002Fcode>，编译直接报错。所以每个具体元素里都得有那行看似重复的 \u003Ccode>visitor.visit(this)\u003C\u002Fcode>：只有在那里，this 的编译期类型才是具体类。双分派不是炫技，是被 Java 分派规则逼出来的生存智慧。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：结构稳定是生死线。\u003C\u002Fstrong> 加操作：加一个类，天下太平（本节已演示）。加元素：比如新增 Audio——DocVisitor 接口加 \u003Ccode>visit(Audio)\u003C\u002Fcode>，CountVisitor、MarkdownExportVisitor、HtmlExportVisitor 全体强制手术。注意这里编译器是帮手不是敌人：它会把每一个没实现新 visit 的访问者揪出来标红，\u003Cstrong>强制你处理，绝不静默漏掉\u003C\u002Fstrong>——这正是访问者比 instanceof 链安全的地方，也是它把痛苦前置换来的尊严。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.nio.file.FileVisitor\u003C\u002Fstrong>：\u003Ccode>Files.walkFileTree()\u003C\u002Fcode> 的核心接口——preVisitDirectory、visitFile、postVisitDirectory、visitFileFailed 四个回调就是\"目录树结构 × 你的操作\"。顺带一个彩蛋：配套的 SimpleFileVisitor 把四个方法全实现成空操作，让你只覆写关心的那个——㉓ 空对象模式在 JDK 里的联袂出演。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>javax.lang.model.ElementVisitor\u003C\u002Fstrong>：注解处理器的核心 API。javac 处理 @Override、Lombok 改写字节码，背后都在用访问者遍历语法树元素——TypeElement、ExecutableElement 各有对应的 visit 重载。编译器世界就是访问者的出生地。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>ASM 的 ClassVisitor \u002F MethodVisitor \u002F FieldVisitor\u003C\u002Fstrong>：字节码操作库——\"类结构（JVM 规范锁定）× 操作（增强、审计、插桩）\"完美命中访问者的适用条件。Spring CGLIB 动态代理底层就是 ASM：ClassVisitor 的 visitMethod 里织入拦截逻辑，动态生成子类字节码。你在 ⑬ 代理模式里看到的运行期魔法，引擎室里烧的是访问者。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、操作应该住在哪——三方终审\u003C\u002Fh2>\n\u003Cp>同一道题（N 种元素 × M 种操作）的三种解法摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>方案\u003C\u002Fth>\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   \u003Ctd>编译期安全\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>外部工具类 + instanceof 链\u003C\u002Ftd>\n   \u003Ctd>再抄一条链\u003C\u002Ftd>\n   \u003Ctd>每条链都要改，**漏了不报错**\u003C\u002Ftd>\n   \u003Ctd>无保护\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>访问者模式\u003C\u002Ftd>\n   \u003Ctd>加一个 Visitor 类，元素零改动\u003C\u002Ftd>\n   \u003Ctd>接口 + 所有访问者强制改，**漏了编译报错**\u003C\u002Ftd>\n   \u003Ctd>编译期安全\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看清了吗？访问者和 instanceof 链的行为矩阵完全一样，差别只在\u003Cstrong>变化的成本由谁承担\u003C\u002Fstrong>：instanceof 把结构变化的成本藏起来（静默漏掉），访问者把它摆上台面（编译报错）。它和多态方案则是镜像互补：多态对\"加类\"开闭、对\"加操作\"关门；访问者对\"加操作\"开闭、对\"加类\"关门。开闭原则的\"开\"永远只对一个变化方向开放——\u003Cstrong>先判断你的系统里哪个方向变得勤，再决定把哪个方向写进开闭\u003C\u002Fstrong>，这是本系列从 ⑨ 组合模式一路埋到这里的终极答案。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>结构稳定、操作常变，是访问者模式唯一的准入证\u003C\u002Fli>\n \u003Cli>双分派两棒接力：accept 按元素真实类型分派，visit(this) 按静态类型触发重载\u003C\u002Fli>\n \u003Cli>visit(this) 那行不能省——Java 重载是编译期分派，被逼出来的生存智慧\u003C\u002Fli>\n \u003Cli>加操作零侵入是糖，加元素全体手术是药——糖药同源，看清再吞\u003C\u002Fli>\n \u003Cli>比 instanceof 链安全的本质：变化成本摆在编译期，而不是藏进生产事故\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>㉔ 的文档能把结构导出成 Markdown 文本——这是\"结构到文本\"的单行道。反过来呢？给程序一段文本，比如促销规则 \u003Ccode>\"vip &amp;&amp; age &gt; 18\"\u003C\u002Fcode>，让它自己解析出结构、算出 true \u002F false，把规则写成人话、让代码读懂它——\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},"2113934683157827585","design-pattern-25","设计模式修炼之路 ㉕ 解释器模式——让程序读懂规则","GoF 收官、规则即数据零发版、文法即代码、递归下降解析器、短路求值白送、解释 vs 编译 vs 配置表",3389,8660,22,"2025-01-19T13:11:38",{"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},"2113932265082851329","design-pattern-18","设计模式修炼之路 ⑱ 命令模式——把请求封装成对象","把请求封装成对象、增量式撤销、白送的排队与重放、Executor 的铁证",2089,7080,18,"2024-08-09T12:36:13",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113934307482406914","design-pattern-23","设计模式修炼之路 ㉓ 空对象模式——让判空从代码里消失","非 GoF、用空对象消灭 null 判断、十亿美元错误、NOPLogger 与 emptyList、空对象 vs Optional",1620,4606,12,"2024-11-20T12:21:47",{"code":4,"message":5,"data":73},{"prev":74,"next":75},{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},{"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},{"code":4,"message":5,"data":77},[78,84,89,95],{"id":79,"name":80,"slug":81,"description":82,"sortOrder":4,"articleCount":83},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":85,"slug":86,"description":87,"sortOrder":4,"articleCount":88},"设计模式","design-pattern","设计模式系列","34",{"id":90,"name":91,"slug":92,"description":93,"sortOrder":33,"articleCount":94},"1946000000000000002","随笔","essay","生活与思考","0",{"id":96,"name":97,"slug":98,"description":99,"sortOrder":100,"articleCount":101},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":103,"toc":104},"\u003Cblockquote>\n \u003Cp>行为型第十一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇的 Member 有 VIP 与普通之分，折扣各不相同——好在行为简单，直接长在每个类自己身上，干净利落。这一篇情况反过来：一群\u003Cstrong>类型稳定\u003C\u002Fstrong>的对象，要面对\u003Cstrong>频繁变化的一堆操作\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>拿本文正在用的文档举例子：段落、表格、图片——这哥仨十年不变。可\"要对它们做的事\"天天出新：字数统计、导出 Markdown、导出 HTML、导出 PDF、SEO 分析、无障碍校验……把每种操作做成每个类的方法，加一种操作，九个类全体手术；写在类外面，\u003Ccode>instanceof\u003C\u002Fcode> 分支满天飞，还一点类型安全都没有。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>把\"对一群对象做什么\"从对象家里抽出来，装进访问者自己家，双分派穿针引线\u003C\u002Fstrong>——访问者模式，GoF 二十三式的倒数第三位狠角色。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、访问者模式是什么\u003C\u002Fh2>\n\u003Cp>GoF 的原始定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. （表示一个作用于某对象结构中各元素的操作。访问者使你可以在不改变各元素的类的前提下，定义作用于这些元素的新操作。）\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>定义的后半句就是它的使命宣言：\u003Cstrong>新增操作，元素类零改动\u003C\u002Fstrong>。这是全系列唯一把\"对某类东西开闭\"写得这么露骨的模式。\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>Visitor（抽象访问者）\u003C\u002Ftd>\n   \u003Ctd>对每种具体元素各声明一个重载入口\u003C\u002Ftd>\n   \u003Ctd>DocVisitor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteVisitor（具体访问者）\u003C\u002Ftd>\n   \u003Ctd>一个访问者 = 一个完整的操作\u003C\u002Ftd>\n   \u003Ctd>CountVisitor \u002F MarkdownExportVisitor \u002F HtmlExportVisitor\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Element（抽象元素）\u003C\u002Ftd>\n   \u003Ctd>声明 accept，接待访问者\u003C\u002Ftd>\n   \u003Ctd>DocElement\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ConcreteElement（具体元素）\u003C\u002Ftd>\n   \u003Ctd>实现 accept，把自己递给访问者\u003C\u002Ftd>\n   \u003Ctd>Paragraph \u002F Table \u002F Image\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>ObjectStructure（对象结构）\u003C\u002Ftd>\n   \u003Ctd>能枚举所有元素的高层容器\u003C\u002Ftd>\n   \u003Ctd>List\\\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>新增操作零侵入\u003C\u002Fstrong>：加一种操作 = 加一个 Visitor 类，五个元素类一行不改——开闭原则教科书级兑现。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作内聚\u003C\u002Fstrong>：一种操作的全部代码住在一个访问者类里，而不是切成碎片散落在九个类中。Markdown 导出的所有细节，MarkdownExportVisitor 一个文件看完。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>类型安全\u003C\u002Fstrong>：新增元素类型时，所有访问者会被编译器挨个揪出来强制补充实现——\u003Ccode>instanceof\u003C\u002Fcode> 链只会默默漏掉新类型，访问者不会。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>操作可携带状态\u003C\u002Fstrong>：访问者是普通对象，可以积累中间结果（CountVisitor 边走边数），这是多态方法做不到的。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>破坏封装\u003C\u002Fstrong>：访问者要干活就得调元素的 getter——Paragraph 得暴露 getText，Table 得暴露 getCells。\"操作住在外面\"的代价就是元素的结构细节必须可见。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>加元素类型是灾难\u003C\u002Fstrong>：新增一种元素（比如音频），DocVisitor 接口加方法、所有现存访问者全体手术——它把\"加操作的痛\"原样转嫁给了\"加元素类型\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>双分派晦涩\u003C\u002Fstrong>：\u003Ccode>accept(visitor)\u003C\u002Fcode> + \u003Ccode>visit(this)\u003C\u002Fcode> 的接力赛，初读每个字都认识，连起来看不懂，必须理解两次分派的机制。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>适用面窄\u003C\u002Fstrong>：押注\"结构稳定、操作常变\"——押错了（两边都常变）就是双重灾难。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>对象结构稳定不常加类型，但作用于结构的操作频繁新增，且操作种类 &gt; 元素种类、或操作属于跨切面的\"外挂\"逻辑。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>文档\u002F报表生成\u003C\u002Fstrong>：结构（段落\u002F表格\u002F图）稳定，导出格式（MD\u002FHTML\u002FPDF\u002FWord）天天加——本文场景\u003C\u002Fli>\n \u003Cli>\u003Cstrong>编译器语法树\u003C\u002Fstrong>：AST 节点类型由语言规范锁死，而遍历操作（类型检查、优化、代码生成）层出不穷——访问者的出生地\u003C\u002Fli>\n \u003Cli>\u003Cstrong>字节码处理\u003C\u002Fstrong>：类结构由 JVM 规范锁死，改字节码的工具却百花齐放\u003C\u002Fli>\n \u003Cli>\u003Cstrong>文件系统遍历\u003C\u002Fstrong>：目录\u002F文件两类元素万年不变，对它们的操作千变万化\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>元素类型预期会增长\u003C\u002Fstrong>（业务模型还在演化）——访问者会把每次扩容变成全体手术，改回多态方法；\u003Cstrong>只有一两种操作\u003C\u002Fstrong>——老实写方法或工具类，别上访问者；\u003Cstrong>操作和元素强绑定、属于元素的本职\u003C\u002Fstrong>（段落渲染自己的文本）——那本来就该是它的方法。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、实现方式：文档导出\u003C\u002Fh2>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：instanceof 链\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\"> BadDocTool\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:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 操作一：字数统计——instanceof 链一号\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\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> countWords\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">DocElement\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">doc\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (DocElement e \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> doc) {\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\"> (e \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Paragraph) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                chars \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((Paragraph) e).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\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\">            } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Table) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">                \u002F\u002F 嵌套两层 for 数格子里的字……\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Image) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                images\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\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\">\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 操作二：导出 Markdown——instanceof 链二号，和一号结构一模一样\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\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">exportMarkdown\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">DocElement\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">doc\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 同款链再来一遍 *\u002F\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>========== 反面教材：instanceof 链 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [统计] 字符 62 个，表格 1 张，图片 1 张\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [导出 Markdown]\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>    | Paragraph | visit(Paragraph) |\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ![双分派示意图](双分派示意图.png)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>功能没毛病，病在结构：\u003Cstrong>N 种元素 × M 种操作 = N×M 个分支盒子\u003C\u002Fstrong>，全靠人肉对齐。更阴险的是漏分支不报错——哪天加了 Audio 元素，两条链谁也不提醒你补 \u003Ccode>instanceof (Audio)\u003C\u002Fcode>，统计悄悄少算、导出悄悄吞音频，又是一场\"聚餐事故\"式的灵异现场（㉒ 老朋友了）。\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\"> DocVisitor\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\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Paragraph \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">paragraph\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\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Table \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">table\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\">    void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Image \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">image\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\"> Paragraph\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DocElement\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\">    @\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\"> accept\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(DocVisitor \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">visitor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        visitor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F this 的静态类型是 Paragraph——关键伏笔\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\"> CountVisitor\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> DocVisitor\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\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chars;\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\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tables;\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\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> images;\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\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Paragraph \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">paragraph\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chars \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> paragraph.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\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\">    }\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\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Table \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">table\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tables\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        for\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\">[] row \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> table.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCells\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (String cell \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                chars \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cell.\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\">            }\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>\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\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Image \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">image\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        images\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\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\"> report\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 打印统计结果 *\u002F\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>Markdown 导出、HTML 导出同款套路各一个类。测试里三路访问者一起上：\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\">CountVisitor counter \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\"> CountVisitor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">MarkdownExportVisitor markdownExport \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\"> MarkdownExportVisitor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">HtmlExportVisitor htmlExport \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\"> HtmlExportVisitor\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\">for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (DocElement e \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> doc) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    e.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">accept\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(counter);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    e.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">accept\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(markdownExport);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    e.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">accept\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(htmlExport);\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>  [统计] 字符 62 个，表格 1 张，图片 1 张\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [导出 Markdown]\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>    | Paragraph | visit(Paragraph) |\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ![双分派示意图](双分派示意图.png)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  [导出 HTML]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;html>&#x3C;body>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;p>设计模式修炼之路：访问者篇&#x3C;\u002Fp>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;p>访问者模式把操作从对象结构中抽离出来。&#x3C;\u002Fp>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;table border=\"1\">\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      &#x3C;tr>&#x3C;td>元素&#x3C;\u002Ftd>&#x3C;td>访问者&#x3C;\u002Ftd>&#x3C;\u002Ftr>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      &#x3C;tr>&#x3C;td>Paragraph&#x3C;\u002Ftd>&#x3C;td>visit(Paragraph)&#x3C;\u002Ftd>&#x3C;\u002Ftr>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;\u002Ftable>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;img src=\"双分派示意图.png\" alt=\"双分派示意图\"\u002F>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    &#x3C;\u002Fbody>&#x3C;\u002Fhtml>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  —— 三个操作全程只有一层 for，元素类零 if、零 instanceof ——\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意最后的含金量：HtmlExportVisitor 是\u003Cstrong>事后新增的操作\u003C\u002Fstrong>——写它的时候，Paragraph、Table、Image 九个类一个都没碰。这就是定义里\"不改变元素的类，定义新操作\"的兑现现场。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：双分派解剖。\u003C\u002Fstrong> \u003Ccode>e.accept(counter)\u003C\u002Fcode> 表面是一行，实际是两棒接力：第一棒，虚方法分派按元素的真实类型（Paragraph）进入 \u003Ccode>Paragraph.accept\u003C\u002Fcode>；第二棒，\u003Ccode>visitor.visit(this)\u003C\u002Fcode> 中 this 的静态类型是 Paragraph，触发重载解析进入 \u003Ccode>visit(Paragraph)\u003C\u002Fcode>。\u003Cstrong>两棒连跑，\"元素类型 × 访问者类型\"两个运行期维度共同定位了唯一的行为\u003C\u002Fstrong>——这正是反面教材里 instanceof 在人肉完成的事，双分派把它下沉成了语言机制。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：visit(this) 那行\"废话\"不能省。\u003C\u002Fstrong> 有人会想：把 accept 干掉，遍历时直接 \u003Ccode>visitor.visit(e)\u003C\u002Fcode> 不行吗？不行——\u003Ccode>e\u003C\u002Fcode> 的静态类型是 DocElement，而 Java 的重载是\u003Cstrong>编译期静态分派\u003C\u002Fstrong>，\u003Ccode>visit(e)\u003C\u002Fcode> 根本匹配不到 \u003Ccode>visit(Paragraph)\u003C\u002Fcode>，编译直接报错。所以每个具体元素里都得有那行看似重复的 \u003Ccode>visitor.visit(this)\u003C\u002Fcode>：只有在那里，this 的编译期类型才是具体类。双分派不是炫技，是被 Java 分派规则逼出来的生存智慧。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：结构稳定是生死线。\u003C\u002Fstrong> 加操作：加一个类，天下太平（本节已演示）。加元素：比如新增 Audio——DocVisitor 接口加 \u003Ccode>visit(Audio)\u003C\u002Fcode>，CountVisitor、MarkdownExportVisitor、HtmlExportVisitor 全体强制手术。注意这里编译器是帮手不是敌人：它会把每一个没实现新 visit 的访问者揪出来标红，\u003Cstrong>强制你处理，绝不静默漏掉\u003C\u002Fstrong>——这正是访问者比 instanceof 链安全的地方，也是它把痛苦前置换来的尊严。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>java.nio.file.FileVisitor\u003C\u002Fstrong>：\u003Ccode>Files.walkFileTree()\u003C\u002Fcode> 的核心接口——preVisitDirectory、visitFile、postVisitDirectory、visitFileFailed 四个回调就是\"目录树结构 × 你的操作\"。顺带一个彩蛋：配套的 SimpleFileVisitor 把四个方法全实现成空操作，让你只覆写关心的那个——㉓ 空对象模式在 JDK 里的联袂出演。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>javax.lang.model.ElementVisitor\u003C\u002Fstrong>：注解处理器的核心 API。javac 处理 @Override、Lombok 改写字节码，背后都在用访问者遍历语法树元素——TypeElement、ExecutableElement 各有对应的 visit 重载。编译器世界就是访问者的出生地。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>ASM 的 ClassVisitor \u002F MethodVisitor \u002F FieldVisitor\u003C\u002Fstrong>：字节码操作库——\"类结构（JVM 规范锁定）× 操作（增强、审计、插桩）\"完美命中访问者的适用条件。Spring CGLIB 动态代理底层就是 ASM：ClassVisitor 的 visitMethod 里织入拦截逻辑，动态生成子类字节码。你在 ⑬ 代理模式里看到的运行期魔法，引擎室里烧的是访问者。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、操作应该住在哪——三方终审\u003C\u002Fh2>\n\u003Cp>同一道题（N 种元素 × M 种操作）的三种解法摊开对账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>方案\u003C\u002Fth>\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   \u003Ctd>编译期安全\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>外部工具类 + instanceof 链\u003C\u002Ftd>\n   \u003Ctd>再抄一条链\u003C\u002Ftd>\n   \u003Ctd>每条链都要改，**漏了不报错**\u003C\u002Ftd>\n   \u003Ctd>无保护\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>访问者模式\u003C\u002Ftd>\n   \u003Ctd>加一个 Visitor 类，元素零改动\u003C\u002Ftd>\n   \u003Ctd>接口 + 所有访问者强制改，**漏了编译报错**\u003C\u002Ftd>\n   \u003Ctd>编译期安全\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看清了吗？访问者和 instanceof 链的行为矩阵完全一样，差别只在\u003Cstrong>变化的成本由谁承担\u003C\u002Fstrong>：instanceof 把结构变化的成本藏起来（静默漏掉），访问者把它摆上台面（编译报错）。它和多态方案则是镜像互补：多态对\"加类\"开闭、对\"加操作\"关门；访问者对\"加操作\"开闭、对\"加类\"关门。开闭原则的\"开\"永远只对一个变化方向开放——\u003Cstrong>先判断你的系统里哪个方向变得勤，再决定把哪个方向写进开闭\u003C\u002Fstrong>，这是本系列从 ⑨ 组合模式一路埋到这里的终极答案。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>结构稳定、操作常变，是访问者模式唯一的准入证\u003C\u002Fli>\n \u003Cli>双分派两棒接力：accept 按元素真实类型分派，visit(this) 按静态类型触发重载\u003C\u002Fli>\n \u003Cli>visit(this) 那行不能省——Java 重载是编译期分派，被逼出来的生存智慧\u003C\u002Fli>\n \u003Cli>加操作零侵入是糖，加元素全体手术是药——糖药同源，看清再吞\u003C\u002Fli>\n \u003Cli>比 instanceof 链安全的本质：变化成本摆在编译期，而不是藏进生产事故\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>㉔ 的文档能把结构导出成 Markdown 文本——这是\"结构到文本\"的单行道。反过来呢？给程序一段文本，比如促销规则 \u003Ccode>\"vip &amp;&amp; age &gt; 18\"\u003C\u002Fcode>，让它自己解析出结构、算出 true \u002F false，把规则写成人话、让代码读懂它——\u003Cstrong>自己实现一个小规则解释器\u003C\u002Fstrong>——解释器模式，下一篇见。\u003C\u002Fp>",[105,108,111,114,117,121,124,127,130,133],{"id":106,"text":107,"level":100},"toc-1","一、访问者模式是什么",{"id":109,"text":110,"level":100},"toc-2","二、优点与缺点",{"id":112,"text":113,"level":100},"toc-3","三、什么时候用",{"id":115,"text":116,"level":100},"toc-4","四、实现方式：文档导出",{"id":118,"text":119,"level":120},"toc-5","4.1 反面教材：instanceof 链",3,{"id":122,"text":123,"level":120},"toc-6","4.2 正解：双分派",{"id":125,"text":126,"level":120},"toc-7","4.3 三处细节",{"id":128,"text":129,"level":100},"toc-8","五、在框架里找它",{"id":131,"text":132,"level":100},"toc-9","六、操作应该住在哪——三方终审",{"id":134,"text":135,"level":100},"toc-10","写在最后"]