[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-09":26,"\u002Farticles\u002Fdesign-pattern-09\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-09\u002Fadjacent":70,"\u002Fcategories":74,"render-design-pattern-09":100},{"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},"2113931567565901825","design-pattern-09","设计模式修炼之路 ⑨ 组合模式——叶子和容器一视同仁","部分-整体、叶子和容器一视同仁、在 DOM\u002FMyBatis 里找它","2113897628365230081",1,3281,7735,20,"2024-01-25T15:35:13","上一篇过滤器模式的结尾，And 嵌着 Or 拼出的那棵\"条件树\"已经提前剧透了一切：条件是树、文件系统是树、组织架构是树、前端组件还是树。今天正主登场：**组合模式（Composite Pattern）**，GoF 23 人名单正牌选手、结构型第四位，有的书也叫它\"部分-整体模式（Part-Whole）\"。它专门伺候一种结构——树，而它给出的答案简单到让人怀疑：**让叶子和整棵树，用同一个接口**。\n\n## 一、什么是组合模式？\n\n先看定义：\n\n> 将对象组合成树形结构以表示\"部分-整体\"的层次结构。组合模式使客户端对单个对象和组合对象的使用保持一致性。\n\n翻译成人话：**叶子和容器实现同一个接口，容器里装的还是这个接口——树就这么长出来了。客户端拿到一个节点，不用关心它是单片叶还是一整棵树，调就完了**。\n\n生活类比是文件资源管理器：想知道一个文件夹多大，右键属性，系统自己一层层加到底——你不需要（也不可能）区分\"文件的算法\"和\"文件夹的算法\"，因为系统早就替你统一了。或者想想公司算人数：CEO 管总监、总监管经理、经理管员工，但\"全公司多少人\"从来不分\"领导怎么算、员工怎么算\"，就是一路加到底。\n\n它有三个角色：\n\n- **Component（组件接口）**：叶子和容器的公共抽象，本篇里是 getName、getSizeKB、print；\n- **Leaf（叶子）**：树的最末端，没有孩子，干活的，本篇里是 File；\n- **Composite（容器）**：持有 `List\u003CComponent>`，本篇里是 Folder，它的方法基本都是\"把孩子挨个问一遍再汇总\"。\n\n精髓在 Composite 的字段类型：**容器持有的不是叶子，是组件自己这个类型**。孩子可以是叶子，也可以是另一个容器，容器套容器，树形结构是这个类型的自然结果。上一篇里 AndCriteria 装着 Criteria、还能再被别的组合器装走，是同一个把戏——组合模式就是它的\"有户口\"版本。递归也不用谁特意发明：容器的方法是\"把孩子的事问一遍\"，孩子若是容器，它会继续往下问，一路问到叶子为止。\n\n## 二、组合模式的优缺点\n\n### 优点\n\n1. **客户端一视同仁**：拿到 Component 就调用，没有 instanceof、没有分支——定义里那句\"单个对象和组合对象的使用保持一致性\"，整个模式就为这一句话活着。\n2. **新增节点类型零成本**：哪天要加一种\"快捷方式\"节点？实现 FileComponent 就能进树，全树所有遍历、统计、打印代码一行不改——开闭原则。\n3. **树形操作天然递归**：算大小、打印结构、查找、序列化……一套递归通吃，而且每种逻辑全树只写一份——对照反面教材的\"文件一遍、文件夹一遍\"，下面看事故现场。\n\n### 缺点\n\n1. **接口不好设计**：Component 的方法必须对叶子和容器**都有**意义。getSize 好说，\"打开文件内容\"这种叶子独有的行为就尴尬了——硬塞进接口，容器只能抛异常；不塞，客户端又得下转型。接口粒度是这个模式里最见功力的地方。\n2. **时机敏感**：它属于\"动工前就该想清楚\"的模式。项目半路想把两个既有类硬统一成树，牵动面往往大到不值得。\n3. **类型安全总有让步**：透明式叶子被迫实现无意义方法（错误拖到运行时才炸），安全式客户端想管理孩子得拿到具体容器类型——两种姿势各有代价，第五节专门掰扯。\n\n## 三、组合模式的应用场景\n\n- **AWT \u002F Swing（JDK）**：教科书级实现。Component 是组件抽象，Button、Label 是叶子，Container 继承 Component 并持有组件列表——Panel 里塞得进 Button，Button 里什么都塞不进，类型系统如实相告。\n- **DOM（org.w3c.dom）**：XML\u002FHTML 的文档对象模型。Node 是组件，Element 是容器（可 appendChild），Text、Comment 是叶子。前端世界的前二十年，就活在这棵树里。\n- **MyBatis 动态 SQL**：SqlNode 接口只有一个 apply() 方法；TextSqlNode 是叶子，MixedSqlNode、IfSqlNode、ForEachSqlNode 全是容器——ForEachSqlNode 里套着的还是 SqlNode，动态标签互相嵌套出任意结构，靠的就是这棵树。本系列已经第三次在 MyBatis 里撞见老朋友了。\n- **Jackson 的 JsonNode**：JSON 天生是树。ObjectNode、ArrayNode 是容器，TextNode、IntNode、NullNode 是叶子，readTree 之后一套递归走天下。\n- **前端组件树**：React\u002FVue 里组件嵌组件、递归 render，本质就是运行时在组装组合模式——所以前端工程师其实天天在写\"JSX 版组合模式\"。\n- **后台系统里的各种树**：菜单树、部门树、权限树、评论盖楼、商品类目——前端那个 Tree 组件渲染的每一份数据，后端十有八九在给组合模式供货。\n\n**什么时候该用**：需求里出现\"层级\"\"树\"\"容器套容器\"这样的字眼，或者你发现自己在写\"递归 + instanceof\"——就该想起它。反过来，对象之间没有层级关系时，别为了套模式硬造一棵树。\n\n## 四、组合模式的实现方式\n\n示例代码在 design-pattern 项目的 composite 包里：\n\n```\ndesign-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── composite                 \u002F\u002F 组合模式\n    ├── BadFile.java          \u002F\u002F 反面教材：文件类（与 BadFolder 无公共抽象）\n    ├── BadFolder.java        \u002F\u002F 反面教材：文件、子文件夹两个列表\n    ├── FileComponent.java    \u002F\u002F 组件接口：叶子和容器的公共抽象\n    ├── File.java             \u002F\u002F 叶子：文件\n    ├── Folder.java           \u002F\u002F 容器：文件夹\n    └── CompositeTest.java    \u002F\u002F 测试入口\n```\n\n场景：文件系统。文件有大小，文件夹由内容组成，要打印目录树、算总大小。先看文件、文件夹\"两套体系\"的下场，再看组合模式怎么救。\n\n### 1. 反面教材：两套体系\n\n最直觉的建模：文件一个类、文件夹一个类，各管各的。文件夹为了同时装下文件和子文件夹，开了两个列表：\n\n```java\n\u002F\u002F 反面教材：文件一个列表、子文件夹另一个列表，每个操作都得遍历两遍\npublic class BadFolder {\n    private final String name;\n    private final List\u003CBadFile> files = new ArrayList\u003C>();\n    private final List\u003CBadFolder> subFolders = new ArrayList\u003C>();\n\n    \u002F\u002F 算总大小：先遍历文件再遍历子文件夹，递归还得自己写\n    public long totalSizeKB() {\n        long total = 0;\n        for (BadFile file : files) {\n            total += file.getSizeKB();\n        }\n        for (BadFolder folder : subFolders) {\n            total += folder.totalSizeKB();\n        }\n        return total;\n    }\n\n    \u002F\u002F 打印目录树：又是两遍遍历，结构和上面几乎重复\n    public void print(String indent) {\n        System.out.println(indent + name + \"\u002F\");\n        for (BadFile file : files) {\n            System.out.println(indent + \"  \" + file.getName() + \"（\" + file.getSizeKB() + \"KB）\");\n        }\n        for (BadFolder folder : subFolders) {\n            folder.print(indent + \"  \");\n        }\n    }\n}\n```\n\n（构造器、getter、addFile\u002FaddFolder 三个简单方法略，完整代码见项目。）\n\n测试代码，末尾专门演示\"想统一处理\"时的下场：\n\n```java\nBadFolder docs = new BadFolder(\"docs\");\ndocs.addFile(new BadFile(\"简历.pdf\", 512));\nBadFolder projects = new BadFolder(\"projects\");\nprojects.addFile(new BadFile(\"pom.xml\", 8));\nprojects.addFile(new BadFile(\"App.java\", 16));\ndocs.addFolder(projects);\n\ndocs.print(\"\");\nSystem.out.println(\"docs 总大小：\" + docs.totalSizeKB() + \"KB\");\n\n\u002F\u002F 想把文件和文件夹放进同一个列表统一处理？没有公共父类型，只能 instanceof 硬判\nList\u003CObject> mixed = new ArrayList\u003C>();\nmixed.add(new BadFile(\"临时文件.txt\", 1));\nmixed.add(new BadFolder(\"新建文件夹\"));\nfor (Object o : mixed) {\n    if (o instanceof BadFile f) {\n        System.out.println(\"这是个文件：\" + f.getName());\n    } else if (o instanceof BadFolder f) {\n        System.out.println(\"这是个文件夹：\" + f.getName());\n    }\n}\n```\n\n运行结果：\n\n```\n===== 反面教材：文件、文件夹两套体系 =====\ndocs\u002F\n  简历.pdf（512KB）\n  projects\u002F\n    pom.xml（8KB）\n    App.java（16KB）\ndocs 总大小：536KB\n这是个文件：临时文件.txt\n这是个文件夹：新建文件夹\n```\n\n能跑，但三个雷已经埋下：\n\n1. **每个操作都得\"两遍遍历\"**：算总大小，文件列表一遍、子文件夹一遍再递归；打印目录树，又是同样的两遍——两个方法结构几乎是复制粘贴。再要加个\"统计文件个数\"？还是两遍。\n2. **没法统一处理**：BadFile 和 BadFolder 之间没有任何公共抽象，想把它们放进同一个列表统一处理，只能 List\u003CObject> 加 instanceof 硬判——每来一种新成员，所有硬判处全部加分支。\n3. **加新成员全链路遭殃**：哪天要支持\"快捷方式\"：BadFolder 再加一个 `List\u003CShortcut>`，totalSizeKB、print 以及一切遍历过这个结构的方法，挨个改一遍。\n\n病根只有一个：**文件和文件夹被建成了两个没有血缘关系的类型**。\n\n### 2. 组合模式版：一个接口，一棵树\n\n先立公共抽象，这是全模式的定海神针：\n\n```java\n\u002F\u002F 组合模式的核心：叶子和容器的公共接口，客户端只认它\npublic interface FileComponent {\n    String getName();\n\n    long getSizeKB();\n\n    \u002F\u002F indent 参数只是为了打印出树形缩进，不是模式的一部分\n    void print(String indent);\n}\n```\n\n叶子——文件，大小就是自己：\n\n```java\n\u002F\u002F 叶子节点：文件，大小就是自己\npublic class File implements FileComponent {\n    private final String name;\n    private final long sizeKB;\n\n    public File(String name, long sizeKB) {\n        this.name = name;\n        this.sizeKB = sizeKB;\n    }\n\n    @Override\n    public long getSizeKB() {\n        return sizeKB;\n    }\n\n    @Override\n    public void print(String indent) {\n        System.out.println(indent + name + \"（\" + sizeKB + \"KB）\");\n    }\n}\n```\n\n（getName 就是返回 name，略。）\n\n重头戏是容器——文件夹。注意它的孩子列表是什么类型：\n\n```java\n\u002F\u002F 容器节点：文件夹，把孩子的大小加起来就是自己——递归由此而来\npublic class Folder implements FileComponent {\n    private final String name;\n    private final List\u003CFileComponent> children = new ArrayList\u003C>();\n\n    \u002F\u002F 安全式：管理孩子的方法只放在容器里，叶子压根没这方法；为什么放这里，第五节专门说\n    public Folder add(FileComponent component) {\n        children.add(component);\n        return this;\n    }\n\n    @Override\n    public long getSizeKB() {\n        long total = 0;\n        for (FileComponent child : children) {\n            \u002F\u002F 孩子可能是文件也可能是文件夹，一律递归\n            total += child.getSizeKB();\n        }\n        return total;\n    }\n\n    @Override\n    public void print(String indent) {\n        System.out.println(indent + name + \"\u002F（共 \" + getSizeKB() + \"KB）\");\n        for (FileComponent child : children) {\n            \u002F\u002F 不用关心孩子是什么类型，转发就行\n            child.print(indent + \"  \");\n        }\n    }\n}\n```\n\n`List\u003CFileComponent>`——**容器装的是组件自己这个类型**，文件、文件夹、文件夹套文件夹，全都合法。建一棵三层目录树，然后玩本篇的核心戏法：把叶子和整棵树扔进同一个列表：\n\n```java\nprivate static FileComponent buildTree() {\n    Folder projects = new Folder(\"projects\");\n    projects.add(new File(\"pom.xml\", 8));\n    Folder src = new Folder(\"src\");\n    src.add(new File(\"App.java\", 16));\n    src.add(new File(\"Config.java\", 4));\n    Folder test = new Folder(\"test\");\n    test.add(new File(\"AppTest.java\", 2));\n    src.add(test);\n    projects.add(src);\n    projects.add(new File(\"README.md\", 2));\n    return projects;\n}\n\nprivate static void uniform() {\n    List\u003CFileComponent> mixed = new ArrayList\u003C>();\n    mixed.add(new File(\"临时文件.txt\", 1));\n    mixed.add(buildTree());\n    for (FileComponent component : mixed) {\n        System.out.println(component.getName() + \" → \" + component.getSizeKB() + \"KB\");\n    }\n}\n```\n\n运行结果：\n\n```\n===== 正解：组合模式 =====\nprojects\u002F（共 32KB）\n  pom.xml（8KB）\n  src\u002F（共 22KB）\n    App.java（16KB）\n    Config.java（4KB）\n    test\u002F（共 2KB）\n      AppTest.java（2KB）\n  README.md（2KB）\n整棵树总大小：32KB\n\n===== 统一处理：叶子和容器一视同仁 =====\n临时文件.txt → 1KB\nprojects → 32KB\n```\n\n对照反面教材逐条验收：\n\n- 反面教材\"两遍遍历\"的 totalSizeKB 和 print，正解里只剩一份\"遍历孩子\"——孩子是文件还是文件夹，一个 for 通吃，两种代码全树各只有一份；\n- 文件的 getSizeKB 是\"自己\"，文件夹的是\"孩子的和\"——同一个方法名，\"部分\"和\"整体\"各自给出自己的答案，这就是定义里\"部分-整体\"的字面意思；\n- `List\u003CFileComponent>` 里文件和文件夹混装，一个循环统一处理，instanceof 退场，add 的时候也没有任何转型；\n- root.print(\"\") 一行，整棵树递归打印出来，缩进都是各节点自己传下去的；\n- 哪天加\"快捷方式\"：写一个 Shortcut 实现 FileComponent 就能进树，BadFolder 那种\"新列表 + 全链路改\"的事故不再发生——开闭原则。\n\n## 五、透明式 vs 安全式：GoF 留下的经典取舍\n\n有个细节不知道你注意到没有：add\u002Fremove 为什么放在 Folder 里，而不上提到 FileComponent 接口？这不是随手一放，是 GoF 原书里就摆上台面的抉择。\n\n**透明式（GoF 原版）**：管理孩子的方法直接进接口，叶子和容器都实现，客户端彻底不用区分手里是什么：\n\n```java\n\u002F\u002F 透明式：管理孩子的方法上提到接口，叶子被迫\"假装\"能生孩子\npublic interface FileComponent {\n    void add(FileComponent component);\n    void remove(FileComponent component);\n    long getSizeKB();\n}\n\npublic class File implements FileComponent {\n    @Override\n    public void add(FileComponent component) {\n        throw new UnsupportedOperationException(\"文件没有孩子\");\n    }\n    \u002F\u002F remove 同理抛异常，其余方法照旧\n}\n```\n\n**安全式（本篇示例）**：管理孩子的方法只放在 Composite 身上，叶子压根没有这方法。\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>透明式（GoF 原版）\u003C\u002Fth>\u003Cth>安全式（本篇示例）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>add\u002Fremove 放哪\u003C\u002Ftd>\u003Ctd>Component 接口\u003C\u002Ftd>\u003Ctd>只在 Composite\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>叶子怎么交差\u003C\u002Ftd>\u003Ctd>抛 UnsupportedOperationException\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>\u003C\u002Ftbody>\u003C\u002Ftable>\n\nJDK 两种都选过：AWT 走安全式（add 在 Container 上，Button 上没有这个方法），DOM 走透明式（Node 接口就有 appendChild，往 Text 节点里塞孩子会抛 DOMException）。两家都活得好好的，说明这不是对错题，是口味题。\n\n给个倾向性建议：**业务代码默认安全式**——让类型系统如实反映\"叶子生不了孩子\"，编译期诚实好过运行期惊吓；而且只要客户端的操作以\"读\"为主（遍历、统计、渲染），两种方式的体验完全一致，本篇示例的 uniform() 就证明了这点——一个循环处理混合列表，谁也没下转型，也没谁抛异常。\n\n## 写在最后\n\n回顾这一篇：组合模式把\"部分-整体\"组织成树——叶子与容器实现同一接口，容器持有组件列表，树形结构是类型的自然结果，递归不需要发明；反面教材\"两套体系\"两遍遍历、instanceof 硬判、加成员全链路遭殃；AWT、DOM、MyBatis 的 SqlNode、Jackson 的 JsonNode，以及后台系统里的每一棵菜单树部门树，都是它的地盘；安全式与透明式是 GoF 留下的经典取舍，业务代码建议默认安全式。\n\n下一篇 ⑩ 装饰器模式，它是组合模式的近亲：UML 画出来，几乎是\"只准养一个孩子的组合模式\"。用途上两家分了工——组合管\"一对多长成树\"，装饰器管\"一对一层层加\"。不改原类动态加功能，Java IO 流里 `new BufferedInputStream(new FileInputStream(file))` 那套套娃，正是它的封神之作。","\u003Cp>上一篇过滤器模式的结尾，And 嵌着 Or 拼出的那棵\"条件树\"已经提前剧透了一切：条件是树、文件系统是树、组织架构是树、前端组件还是树。今天正主登场：\u003Cstrong>组合模式（Composite Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第四位，有的书也叫它\"部分-整体模式（Part-Whole）\"。它专门伺候一种结构——树，而它给出的答案简单到让人怀疑：\u003Cstrong>让叶子和整棵树，用同一个接口\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2>一、什么是组合模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>将对象组合成树形结构以表示\"部分-整体\"的层次结构。组合模式使客户端对单个对象和组合对象的使用保持一致性。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>叶子和容器实现同一个接口，容器里装的还是这个接口——树就这么长出来了。客户端拿到一个节点，不用关心它是单片叶还是一整棵树，调就完了\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>生活类比是文件资源管理器：想知道一个文件夹多大，右键属性，系统自己一层层加到底——你不需要（也不可能）区分\"文件的算法\"和\"文件夹的算法\"，因为系统早就替你统一了。或者想想公司算人数：CEO 管总监、总监管经理、经理管员工，但\"全公司多少人\"从来不分\"领导怎么算、员工怎么算\"，就是一路加到底。\u003C\u002Fp>\n\u003Cp>它有三个角色：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Component（组件接口）\u003C\u002Fstrong>：叶子和容器的公共抽象，本篇里是 getName、getSizeKB、print；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Leaf（叶子）\u003C\u002Fstrong>：树的最末端，没有孩子，干活的，本篇里是 File；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Composite（容器）\u003C\u002Fstrong>：持有 \u003Ccode>List&lt;Component&gt;\u003C\u002Fcode>，本篇里是 Folder，它的方法基本都是\"把孩子挨个问一遍再汇总\"。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓在 Composite 的字段类型：\u003Cstrong>容器持有的不是叶子，是组件自己这个类型\u003C\u002Fstrong>。孩子可以是叶子，也可以是另一个容器，容器套容器，树形结构是这个类型的自然结果。上一篇里 AndCriteria 装着 Criteria、还能再被别的组合器装走，是同一个把戏——组合模式就是它的\"有户口\"版本。递归也不用谁特意发明：容器的方法是\"把孩子的事问一遍\"，孩子若是容器，它会继续往下问，一路问到叶子为止。\u003C\u002Fp>\n\u003Ch2>二、组合模式的优缺点\u003C\u002Fh2>\n\u003Ch3>优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>客户端一视同仁\u003C\u002Fstrong>：拿到 Component 就调用，没有 instanceof、没有分支——定义里那句\"单个对象和组合对象的使用保持一致性\"，整个模式就为这一句话活着。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>新增节点类型零成本\u003C\u002Fstrong>：哪天要加一种\"快捷方式\"节点？实现 FileComponent 就能进树，全树所有遍历、统计、打印代码一行不改——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>树形操作天然递归\u003C\u002Fstrong>：算大小、打印结构、查找、序列化……一套递归通吃，而且每种逻辑全树只写一份——对照反面教材的\"文件一遍、文件夹一遍\"，下面看事故现场。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>接口不好设计\u003C\u002Fstrong>：Component 的方法必须对叶子和容器\u003Cstrong>都有\u003C\u002Fstrong>意义。getSize 好说，\"打开文件内容\"这种叶子独有的行为就尴尬了——硬塞进接口，容器只能抛异常；不塞，客户端又得下转型。接口粒度是这个模式里最见功力的地方。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>时机敏感\u003C\u002Fstrong>：它属于\"动工前就该想清楚\"的模式。项目半路想把两个既有类硬统一成树，牵动面往往大到不值得。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>类型安全总有让步\u003C\u002Fstrong>：透明式叶子被迫实现无意义方法（错误拖到运行时才炸），安全式客户端想管理孩子得拿到具体容器类型——两种姿势各有代价，第五节专门掰扯。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、组合模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>AWT \u002F Swing（JDK）\u003C\u002Fstrong>：教科书级实现。Component 是组件抽象，Button、Label 是叶子，Container 继承 Component 并持有组件列表——Panel 里塞得进 Button，Button 里什么都塞不进，类型系统如实相告。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>DOM（org.w3c.dom）\u003C\u002Fstrong>：XML\u002FHTML 的文档对象模型。Node 是组件，Element 是容器（可 appendChild），Text、Comment 是叶子。前端世界的前二十年，就活在这棵树里。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 动态 SQL\u003C\u002Fstrong>：SqlNode 接口只有一个 apply() 方法；TextSqlNode 是叶子，MixedSqlNode、IfSqlNode、ForEachSqlNode 全是容器——ForEachSqlNode 里套着的还是 SqlNode，动态标签互相嵌套出任意结构，靠的就是这棵树。本系列已经第三次在 MyBatis 里撞见老朋友了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Jackson 的 JsonNode\u003C\u002Fstrong>：JSON 天生是树。ObjectNode、ArrayNode 是容器，TextNode、IntNode、NullNode 是叶子，readTree 之后一套递归走天下。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>前端组件树\u003C\u002Fstrong>：React\u002FVue 里组件嵌组件、递归 render，本质就是运行时在组装组合模式——所以前端工程师其实天天在写\"JSX 版组合模式\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>后台系统里的各种树\u003C\u002Fstrong>：菜单树、部门树、权限树、评论盖楼、商品类目——前端那个 Tree 组件渲染的每一份数据，后端十有八九在给组合模式供货。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：需求里出现\"层级\"\"树\"\"容器套容器\"这样的字眼，或者你发现自己在写\"递归 + instanceof\"——就该想起它。反过来，对象之间没有层级关系时，别为了套模式硬造一棵树。\u003C\u002Fp>\n\u003Ch2>四、组合模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 composite 包里：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>design-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── composite                 \u002F\u002F 组合模式\n    ├── BadFile.java          \u002F\u002F 反面教材：文件类（与 BadFolder 无公共抽象）\n    ├── BadFolder.java        \u002F\u002F 反面教材：文件、子文件夹两个列表\n    ├── FileComponent.java    \u002F\u002F 组件接口：叶子和容器的公共抽象\n    ├── File.java             \u002F\u002F 叶子：文件\n    ├── Folder.java           \u002F\u002F 容器：文件夹\n    └── CompositeTest.java    \u002F\u002F 测试入口\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：文件系统。文件有大小，文件夹由内容组成，要打印目录树、算总大小。先看文件、文件夹\"两套体系\"的下场，再看组合模式怎么救。\u003C\u002Fp>\n\u003Ch3>1. 反面教材：两套体系\u003C\u002Fh3>\n\u003Cp>最直觉的建模：文件一个类、文件夹一个类，各管各的。文件夹为了同时装下文件和子文件夹，开了两个列表：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 反面教材：文件一个列表、子文件夹另一个列表，每个操作都得遍历两遍\npublic class BadFolder {\n    private final String name;\n    private final List&lt;BadFile&gt; files = new ArrayList&lt;&gt;();\n    private final List&lt;BadFolder&gt; subFolders = new ArrayList&lt;&gt;();\n\n    \u002F\u002F 算总大小：先遍历文件再遍历子文件夹，递归还得自己写\n    public long totalSizeKB() {\n        long total = 0;\n        for (BadFile file : files) {\n            total += file.getSizeKB();\n        }\n        for (BadFolder folder : subFolders) {\n            total += folder.totalSizeKB();\n        }\n        return total;\n    }\n\n    \u002F\u002F 打印目录树：又是两遍遍历，结构和上面几乎重复\n    public void print(String indent) {\n        System.out.println(indent + name + \"\u002F\");\n        for (BadFile file : files) {\n            System.out.println(indent + \"  \" + file.getName() + \"（\" + file.getSizeKB() + \"KB）\");\n        }\n        for (BadFolder folder : subFolders) {\n            folder.print(indent + \"  \");\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>（构造器、getter、addFile\u002FaddFolder 三个简单方法略，完整代码见项目。）\u003C\u002Fp>\n\u003Cp>测试代码，末尾专门演示\"想统一处理\"时的下场：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">BadFolder docs = new BadFolder(\"docs\");\ndocs.addFile(new BadFile(\"简历.pdf\", 512));\nBadFolder projects = new BadFolder(\"projects\");\nprojects.addFile(new BadFile(\"pom.xml\", 8));\nprojects.addFile(new BadFile(\"App.java\", 16));\ndocs.addFolder(projects);\n\ndocs.print(\"\");\nSystem.out.println(\"docs 总大小：\" + docs.totalSizeKB() + \"KB\");\n\n\u002F\u002F 想把文件和文件夹放进同一个列表统一处理？没有公共父类型，只能 instanceof 硬判\nList&lt;Object&gt; mixed = new ArrayList&lt;&gt;();\nmixed.add(new BadFile(\"临时文件.txt\", 1));\nmixed.add(new BadFolder(\"新建文件夹\"));\nfor (Object o : mixed) {\n    if (o instanceof BadFile f) {\n        System.out.println(\"这是个文件：\" + f.getName());\n    } else if (o instanceof BadFolder f) {\n        System.out.println(\"这是个文件夹：\" + f.getName());\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 反面教材：文件、文件夹两套体系 =====\ndocs\u002F\n  简历.pdf（512KB）\n  projects\u002F\n    pom.xml（8KB）\n    App.java（16KB）\ndocs 总大小：536KB\n这是个文件：临时文件.txt\n这是个文件夹：新建文件夹\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>能跑，但三个雷已经埋下：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>每个操作都得\"两遍遍历\"\u003C\u002Fstrong>：算总大小，文件列表一遍、子文件夹一遍再递归；打印目录树，又是同样的两遍——两个方法结构几乎是复制粘贴。再要加个\"统计文件个数\"？还是两遍。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>没法统一处理\u003C\u002Fstrong>：BadFile 和 BadFolder 之间没有任何公共抽象，想把它们放进同一个列表统一处理，只能 List 加 instanceof 硬判——每来一种新成员，所有硬判处全部加分支。\n  \u003Cli>\u003Cstrong>加新成员全链路遭殃\u003C\u002Fstrong>：哪天要支持\"快捷方式\"：BadFolder 再加一个 \u003Ccode>List&lt;Shortcut&gt;\u003C\u002Fcode>，totalSizeKB、print 以及一切遍历过这个结构的方法，挨个改一遍。\u003C\u002Fli>\n  \u003Cp>病根只有一个：\u003Cstrong>文件和文件夹被建成了两个没有血缘关系的类型\u003C\u002Fstrong>。\u003C\u002Fp>\n  \u003Ch3>2. 组合模式版：一个接口，一棵树\u003C\u002Fh3>\n  \u003Cp>先立公共抽象，这是全模式的定海神针：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 组合模式的核心：叶子和容器的公共接口，客户端只认它\npublic interface FileComponent {\n    String getName();\n\n    long getSizeKB();\n\n    \u002F\u002F indent 参数只是为了打印出树形缩进，不是模式的一部分\n    void print(String indent);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>叶子——文件，大小就是自己：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 叶子节点：文件，大小就是自己\npublic class File implements FileComponent {\n    private final String name;\n    private final long sizeKB;\n\n    public File(String name, long sizeKB) {\n        this.name = name;\n        this.sizeKB = sizeKB;\n    }\n\n    @Override\n    public long getSizeKB() {\n        return sizeKB;\n    }\n\n    @Override\n    public void print(String indent) {\n        System.out.println(indent + name + \"（\" + sizeKB + \"KB）\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>（getName 就是返回 name，略。）\u003C\u002Fp>\n  \u003Cp>重头戏是容器——文件夹。注意它的孩子列表是什么类型：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 容器节点：文件夹，把孩子的大小加起来就是自己——递归由此而来\npublic class Folder implements FileComponent {\n    private final String name;\n    private final List&lt;FileComponent&gt; children = new ArrayList&lt;&gt;();\n\n    \u002F\u002F 安全式：管理孩子的方法只放在容器里，叶子压根没这方法；为什么放这里，第五节专门说\n    public Folder add(FileComponent component) {\n        children.add(component);\n        return this;\n    }\n\n    @Override\n    public long getSizeKB() {\n        long total = 0;\n        for (FileComponent child : children) {\n            \u002F\u002F 孩子可能是文件也可能是文件夹，一律递归\n            total += child.getSizeKB();\n        }\n        return total;\n    }\n\n    @Override\n    public void print(String indent) {\n        System.out.println(indent + name + \"\u002F（共 \" + getSizeKB() + \"KB）\");\n        for (FileComponent child : children) {\n            \u002F\u002F 不用关心孩子是什么类型，转发就行\n            child.print(indent + \"  \");\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>\u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>——\u003Cstrong>容器装的是组件自己这个类型\u003C\u002Fstrong>，文件、文件夹、文件夹套文件夹，全都合法。建一棵三层目录树，然后玩本篇的核心戏法：把叶子和整棵树扔进同一个列表：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode class=\"language-java\">private static FileComponent buildTree() {\n    Folder projects = new Folder(\"projects\");\n    projects.add(new File(\"pom.xml\", 8));\n    Folder src = new Folder(\"src\");\n    src.add(new File(\"App.java\", 16));\n    src.add(new File(\"Config.java\", 4));\n    Folder test = new Folder(\"test\");\n    test.add(new File(\"AppTest.java\", 2));\n    src.add(test);\n    projects.add(src);\n    projects.add(new File(\"README.md\", 2));\n    return projects;\n}\n\nprivate static void uniform() {\n    List&lt;FileComponent&gt; mixed = new ArrayList&lt;&gt;();\n    mixed.add(new File(\"临时文件.txt\", 1));\n    mixed.add(buildTree());\n    for (FileComponent component : mixed) {\n        System.out.println(component.getName() + \" → \" + component.getSizeKB() + \"KB\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>运行结果：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode>===== 正解：组合模式 =====\nprojects\u002F（共 32KB）\n  pom.xml（8KB）\n  src\u002F（共 22KB）\n    App.java（16KB）\n    Config.java（4KB）\n    test\u002F（共 2KB）\n      AppTest.java（2KB）\n  README.md（2KB）\n整棵树总大小：32KB\n\n===== 统一处理：叶子和容器一视同仁 =====\n临时文件.txt → 1KB\nprojects → 32KB\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>对照反面教材逐条验收：\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>反面教材\"两遍遍历\"的 totalSizeKB 和 print，正解里只剩一份\"遍历孩子\"——孩子是文件还是文件夹，一个 for 通吃，两种代码全树各只有一份；\u003C\u002Fli>\n   \u003Cli>文件的 getSizeKB 是\"自己\"，文件夹的是\"孩子的和\"——同一个方法名，\"部分\"和\"整体\"各自给出自己的答案，这就是定义里\"部分-整体\"的字面意思；\u003C\u002Fli>\n   \u003Cli>\u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode> 里文件和文件夹混装，一个循环统一处理，instanceof 退场，add 的时候也没有任何转型；\u003C\u002Fli>\n   \u003Cli>root.print(\"\") 一行，整棵树递归打印出来，缩进都是各节点自己传下去的；\u003C\u002Fli>\n   \u003Cli>哪天加\"快捷方式\"：写一个 Shortcut 实现 FileComponent 就能进树，BadFolder 那种\"新列表 + 全链路改\"的事故不再发生——开闭原则。\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Ch2>五、透明式 vs 安全式：GoF 留下的经典取舍\u003C\u002Fh2>\n  \u003Cp>有个细节不知道你注意到没有：add\u002Fremove 为什么放在 Folder 里，而不上提到 FileComponent 接口？这不是随手一放，是 GoF 原书里就摆上台面的抉择。\u003C\u002Fp>\n  \u003Cp>\u003Cstrong>透明式（GoF 原版）\u003C\u002Fstrong>：管理孩子的方法直接进接口，叶子和容器都实现，客户端彻底不用区分手里是什么：\u003C\u002Fp>\n  \u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 透明式：管理孩子的方法上提到接口，叶子被迫\"假装\"能生孩子\npublic interface FileComponent {\n    void add(FileComponent component);\n    void remove(FileComponent component);\n    long getSizeKB();\n}\n\npublic class File implements FileComponent {\n    @Override\n    public void add(FileComponent component) {\n        throw new UnsupportedOperationException(\"文件没有孩子\");\n    }\n    \u002F\u002F remove 同理抛异常，其余方法照旧\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>\u003Cstrong>安全式（本篇示例）\u003C\u002Fstrong>：管理孩子的方法只放在 Composite 身上，叶子压根没有这方法。\u003C\u002Fp>\n  \u003Ctable>\n   \u003Cthead>\n    \u003Ctr>\n     \u003Cth>\u003C\u002Fth>\n     \u003Cth>透明式（GoF 原版）\u003C\u002Fth>\n     \u003Cth>安全式（本篇示例）\u003C\u002Fth>\n    \u003C\u002Ftr>\n   \u003C\u002Fthead>\n   \u003Ctbody>\n    \u003Ctr>\n     \u003Ctd>add\u002Fremove 放哪\u003C\u002Ftd>\n     \u003Ctd>Component 接口\u003C\u002Ftd>\n     \u003Ctd>只在 Composite\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n     \u003Ctd>叶子怎么交差\u003C\u002Ftd>\n     \u003Ctd>抛 UnsupportedOperationException\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   \u003C\u002Ftbody>\n  \u003C\u002Ftable>\n  \u003Cp>JDK 两种都选过：AWT 走安全式（add 在 Container 上，Button 上没有这个方法），DOM 走透明式（Node 接口就有 appendChild，往 Text 节点里塞孩子会抛 DOMException）。两家都活得好好的，说明这不是对错题，是口味题。\u003C\u002Fp>\n  \u003Cp>给个倾向性建议：\u003Cstrong>业务代码默认安全式\u003C\u002Fstrong>——让类型系统如实反映\"叶子生不了孩子\"，编译期诚实好过运行期惊吓；而且只要客户端的操作以\"读\"为主（遍历、统计、渲染），两种方式的体验完全一致，本篇示例的 uniform() 就证明了这点——一个循环处理混合列表，谁也没下转型，也没谁抛异常。\u003C\u002Fp>\n  \u003Ch2>写在最后\u003C\u002Fh2>\n  \u003Cp>回顾这一篇：组合模式把\"部分-整体\"组织成树——叶子与容器实现同一接口，容器持有组件列表，树形结构是类型的自然结果，递归不需要发明；反面教材\"两套体系\"两遍遍历、instanceof 硬判、加成员全链路遭殃；AWT、DOM、MyBatis 的 SqlNode、Jackson 的 JsonNode，以及后台系统里的每一棵菜单树部门树，都是它的地盘；安全式与透明式是 GoF 留下的经典取舍，业务代码建议默认安全式。\u003C\u002Fp>\n  \u003Cp>下一篇 ⑩ 装饰器模式，它是组合模式的近亲：UML 画出来，几乎是\"只准养一个孩子的组合模式\"。用途上两家分了工——组合管\"一对多长成树\"，装饰器管\"一对一层层加\"。不改原类动态加功能，Java IO 流里 \u003Ccode>new BufferedInputStream(new FileInputStream(file))\u003C\u002Fcode> 那套套娃，正是它的封神之作。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,62],{"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},"2113931511261564929","design-pattern-08","设计模式修炼之路 ⑧ 过滤器模式——与或非条件当积木","非 GoF、与或非条件当积木、Predicate 是 JDK 收编版",3801,7442,19,"2024-01-08T20:51:45",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":36,"publishedAt":61},"2113931625669595138","design-pattern-10","设计模式修炼之路 ⑩ 装饰器模式——不改原类，动态加功能","不改原类动态加功能、\"我是组件+我有组件\"、Java IO 流的套娃设计",1003,7847,"2024-02-10T15:14:16",{"id":63,"slug":64,"title":65,"summary":66,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":67,"likeCount":4,"wordCount":68,"readingMinutes":36,"publishedAt":69},"2113933638763548673","design-pattern-11","设计模式修炼之路 ⑪ 外观模式——给杂乱子系统一个入口","给杂乱子系统一个统一入口、只编排不干活、SLF4J 名字里的 Facade",3176,7681,"2024-03-20T15:49:38",{"code":4,"message":5,"data":71},{"prev":72,"next":73},{"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":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":36,"publishedAt":61},{"code":4,"message":5,"data":75},[76,82,87,93],{"id":77,"name":78,"slug":79,"description":80,"sortOrder":4,"articleCount":81},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":83,"slug":84,"description":85,"sortOrder":4,"articleCount":86},"设计模式","design-pattern","设计模式系列","34",{"id":88,"name":89,"slug":90,"description":91,"sortOrder":33,"articleCount":92},"1946000000000000002","随笔","essay","生活与思考","0",{"id":94,"name":95,"slug":96,"description":97,"sortOrder":98,"articleCount":99},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":101,"toc":102},"\u003Cp>上一篇过滤器模式的结尾，And 嵌着 Or 拼出的那棵\"条件树\"已经提前剧透了一切：条件是树、文件系统是树、组织架构是树、前端组件还是树。今天正主登场：\u003Cstrong>组合模式（Composite Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第四位，有的书也叫它\"部分-整体模式（Part-Whole）\"。它专门伺候一种结构——树，而它给出的答案简单到让人怀疑：\u003Cstrong>让叶子和整棵树，用同一个接口\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、什么是组合模式？\u003C\u002Fh2>\n\u003Cp>先看定义：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>将对象组合成树形结构以表示\"部分-整体\"的层次结构。组合模式使客户端对单个对象和组合对象的使用保持一致性。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>翻译成人话：\u003Cstrong>叶子和容器实现同一个接口，容器里装的还是这个接口——树就这么长出来了。客户端拿到一个节点，不用关心它是单片叶还是一整棵树，调就完了\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>生活类比是文件资源管理器：想知道一个文件夹多大，右键属性，系统自己一层层加到底——你不需要（也不可能）区分\"文件的算法\"和\"文件夹的算法\"，因为系统早就替你统一了。或者想想公司算人数：CEO 管总监、总监管经理、经理管员工，但\"全公司多少人\"从来不分\"领导怎么算、员工怎么算\"，就是一路加到底。\u003C\u002Fp>\n\u003Cp>它有三个角色：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Component（组件接口）\u003C\u002Fstrong>：叶子和容器的公共抽象，本篇里是 getName、getSizeKB、print；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Leaf（叶子）\u003C\u002Fstrong>：树的最末端，没有孩子，干活的，本篇里是 File；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Composite（容器）\u003C\u002Fstrong>：持有 \u003Ccode>List&lt;Component&gt;\u003C\u002Fcode>，本篇里是 Folder，它的方法基本都是\"把孩子挨个问一遍再汇总\"。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓在 Composite 的字段类型：\u003Cstrong>容器持有的不是叶子，是组件自己这个类型\u003C\u002Fstrong>。孩子可以是叶子，也可以是另一个容器，容器套容器，树形结构是这个类型的自然结果。上一篇里 AndCriteria 装着 Criteria、还能再被别的组合器装走，是同一个把戏——组合模式就是它的\"有户口\"版本。递归也不用谁特意发明：容器的方法是\"把孩子的事问一遍\"，孩子若是容器，它会继续往下问，一路问到叶子为止。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、组合模式的优缺点\u003C\u002Fh2>\n\u003Ch3 id=\"toc-3\">优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>客户端一视同仁\u003C\u002Fstrong>：拿到 Component 就调用，没有 instanceof、没有分支——定义里那句\"单个对象和组合对象的使用保持一致性\"，整个模式就为这一句话活着。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>新增节点类型零成本\u003C\u002Fstrong>：哪天要加一种\"快捷方式\"节点？实现 FileComponent 就能进树，全树所有遍历、统计、打印代码一行不改——开闭原则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>树形操作天然递归\u003C\u002Fstrong>：算大小、打印结构、查找、序列化……一套递归通吃，而且每种逻辑全树只写一份——对照反面教材的\"文件一遍、文件夹一遍\"，下面看事故现场。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3 id=\"toc-4\">缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>接口不好设计\u003C\u002Fstrong>：Component 的方法必须对叶子和容器\u003Cstrong>都有\u003C\u002Fstrong>意义。getSize 好说，\"打开文件内容\"这种叶子独有的行为就尴尬了——硬塞进接口，容器只能抛异常；不塞，客户端又得下转型。接口粒度是这个模式里最见功力的地方。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>时机敏感\u003C\u002Fstrong>：它属于\"动工前就该想清楚\"的模式。项目半路想把两个既有类硬统一成树，牵动面往往大到不值得。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>类型安全总有让步\u003C\u002Fstrong>：透明式叶子被迫实现无意义方法（错误拖到运行时才炸），安全式客户端想管理孩子得拿到具体容器类型——两种姿势各有代价，第五节专门掰扯。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-5\">三、组合模式的应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Cstrong>AWT \u002F Swing（JDK）\u003C\u002Fstrong>：教科书级实现。Component 是组件抽象，Button、Label 是叶子，Container 继承 Component 并持有组件列表——Panel 里塞得进 Button，Button 里什么都塞不进，类型系统如实相告。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>DOM（org.w3c.dom）\u003C\u002Fstrong>：XML\u002FHTML 的文档对象模型。Node 是组件，Element 是容器（可 appendChild），Text、Comment 是叶子。前端世界的前二十年，就活在这棵树里。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 动态 SQL\u003C\u002Fstrong>：SqlNode 接口只有一个 apply() 方法；TextSqlNode 是叶子，MixedSqlNode、IfSqlNode、ForEachSqlNode 全是容器——ForEachSqlNode 里套着的还是 SqlNode，动态标签互相嵌套出任意结构，靠的就是这棵树。本系列已经第三次在 MyBatis 里撞见老朋友了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Jackson 的 JsonNode\u003C\u002Fstrong>：JSON 天生是树。ObjectNode、ArrayNode 是容器，TextNode、IntNode、NullNode 是叶子，readTree 之后一套递归走天下。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>前端组件树\u003C\u002Fstrong>：React\u002FVue 里组件嵌组件、递归 render，本质就是运行时在组装组合模式——所以前端工程师其实天天在写\"JSX 版组合模式\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>后台系统里的各种树\u003C\u002Fstrong>：菜单树、部门树、权限树、评论盖楼、商品类目——前端那个 Tree 组件渲染的每一份数据，后端十有八九在给组合模式供货。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：需求里出现\"层级\"\"树\"\"容器套容器\"这样的字眼，或者你发现自己在写\"递归 + instanceof\"——就该想起它。反过来，对象之间没有层级关系时，别为了套模式硬造一棵树。\u003C\u002Fp>\n\u003Ch2 id=\"toc-6\">四、组合模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 composite 包里：\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>design-pattern\u002Fsrc\u002Fmain\u002Fjava\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└── composite                 \u002F\u002F 组合模式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadFile.java          \u002F\u002F 反面教材：文件类（与 BadFolder 无公共抽象）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadFolder.java        \u002F\u002F 反面教材：文件、子文件夹两个列表\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── FileComponent.java    \u002F\u002F 组件接口：叶子和容器的公共抽象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── File.java             \u002F\u002F 叶子：文件\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── Folder.java           \u002F\u002F 容器：文件夹\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    └── CompositeTest.java    \u002F\u002F 测试入口\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：文件系统。文件有大小，文件夹由内容组成，要打印目录树、算总大小。先看文件、文件夹\"两套体系\"的下场，再看组合模式怎么救。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">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:#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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadFolder\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:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\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\"> List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">BadFile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> files \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\"> ArrayList&#x3C;>();\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\"> List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">BadFolder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> subFolders \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\"> ArrayList&#x3C;>();\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:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> long\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> totalSizeKB\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        long\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\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\"> (BadFile file \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            total \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> file.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getSizeKB\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:#D73A49;--shiki-dark:#F97583\">        for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (BadFolder folder \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subFolders) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            total \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> folder.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">totalSizeKB\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:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total;\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:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">indent\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\">(indent \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u002F\"\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\"> (BadFile file \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files) {\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\">(indent \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\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\"> file.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\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:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> file.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getSizeKB\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\"> \"KB）\"\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:#D73A49;--shiki-dark:#F97583\">        for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (BadFolder folder \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subFolders) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            folder.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(indent \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:#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>（构造器、getter、addFile\u002FaddFolder 三个简单方法略，完整代码见项目。）\u003C\u002Fp>\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:#24292E;--shiki-dark:#E1E4E8\">BadFolder docs \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\"> BadFolder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"docs\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">docs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addFile\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\"> BadFile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"简历.pdf\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">512\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">BadFolder projects \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\"> BadFolder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"projects\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">projects.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addFile\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\"> BadFile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pom.xml\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">8\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">projects.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addFile\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\"> BadFile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"App.java\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">16\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">docs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addFolder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(projects);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">docs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">print\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\">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\">\"docs 总大小：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> docs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">totalSizeKB\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\"> \"KB\"\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:#24292E;--shiki-dark:#E1E4E8\">List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> mixed \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\"> ArrayList&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">mixed.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> BadFile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"临时文件.txt\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">mixed.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> BadFolder\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\">for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Object o \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mixed) {\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\"> (o \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BadFile f) {\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\"> f.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\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\"> (o \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BadFolder f) {\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\"> f.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\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>docs\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  简历.pdf（512KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  projects\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    pom.xml（8KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    App.java（16KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>docs 总大小：536KB\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>这是个文件：临时文件.txt\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\u003Cp>能跑，但三个雷已经埋下：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>每个操作都得\"两遍遍历\"\u003C\u002Fstrong>：算总大小，文件列表一遍、子文件夹一遍再递归；打印目录树，又是同样的两遍——两个方法结构几乎是复制粘贴。再要加个\"统计文件个数\"？还是两遍。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>没法统一处理\u003C\u002Fstrong>：BadFile 和 BadFolder 之间没有任何公共抽象，想把它们放进同一个列表统一处理，只能 List 加 instanceof 硬判——每来一种新成员，所有硬判处全部加分支。\n  \u003Cli>\u003Cstrong>加新成员全链路遭殃\u003C\u002Fstrong>：哪天要支持\"快捷方式\"：BadFolder 再加一个 \u003Ccode>List&lt;Shortcut&gt;\u003C\u002Fcode>，totalSizeKB、print 以及一切遍历过这个结构的方法，挨个改一遍。\u003C\u002Fli>\n  \u003Cp>病根只有一个：\u003Cstrong>文件和文件夹被建成了两个没有血缘关系的类型\u003C\u002Fstrong>。\u003C\u002Fp>\n  \u003Ch3 id=\"toc-8\">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:#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\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FileComponent\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 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\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\">    long\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getSizeKB\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 indent 参数只是为了打印出树形缩进，不是模式的一部分\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\"> print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">indent\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:#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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FileComponent\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:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\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\"> long\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sizeKB;\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">name\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">long\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> sizeKB\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\">.name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name;\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\">.sizeKB \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sizeKB;\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\"> long\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getSizeKB\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\"> sizeKB;\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\"> print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">indent\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\">(indent \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\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\"> sizeKB \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"KB）\"\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>（getName 就是返回 name，略。）\u003C\u002Fp>\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:#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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Folder\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FileComponent\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:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\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\"> List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">FileComponent\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> children \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\"> ArrayList&#x3C;>();\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:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Folder \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(FileComponent \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">component\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        children.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(component);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> this\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\"> long\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getSizeKB\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        long\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\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\"> (FileComponent child \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> children) {\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:#24292E;--shiki-dark:#E1E4E8\">            total \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getSizeKB\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:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total;\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\"> print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">indent\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\">(indent \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\u002F（共 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getSizeKB\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\"> \"KB）\"\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\"> (FileComponent child \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> children) {\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:#24292E;--shiki-dark:#E1E4E8\">            child.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">print\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(indent \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:#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>\u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode>——\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\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> FileComponent \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">buildTree\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Folder projects \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\"> Folder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"projects\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    projects.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pom.xml\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">8\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Folder src \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\"> Folder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"src\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    src.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"App.java\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">16\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    src.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Config.java\"\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\">    Folder test \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\"> Folder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"test\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    test.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AppTest.java\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \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\">    src.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(test);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    projects.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(src);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    projects.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"README.md\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \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:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> projects;\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\">private\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\"> uniform\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">FileComponent\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> mixed \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\"> ArrayList&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mixed.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"临时文件.txt\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mixed.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">buildTree\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\"> (FileComponent component \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mixed) {\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\">(component.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\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:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> component.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getSizeKB\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\"> \"KB\"\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>projects\u002F（共 32KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  pom.xml（8KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  src\u002F（共 22KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    App.java（16KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    Config.java（4KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    test\u002F（共 2KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>      AppTest.java（2KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  README.md（2KB）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>整棵树总大小：32KB\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>临时文件.txt → 1KB\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>projects → 32KB\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003Cp>对照反面教材逐条验收：\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>反面教材\"两遍遍历\"的 totalSizeKB 和 print，正解里只剩一份\"遍历孩子\"——孩子是文件还是文件夹，一个 for 通吃，两种代码全树各只有一份；\u003C\u002Fli>\n   \u003Cli>文件的 getSizeKB 是\"自己\"，文件夹的是\"孩子的和\"——同一个方法名，\"部分\"和\"整体\"各自给出自己的答案，这就是定义里\"部分-整体\"的字面意思；\u003C\u002Fli>\n   \u003Cli>\u003Ccode>List&lt;FileComponent&gt;\u003C\u002Fcode> 里文件和文件夹混装，一个循环统一处理，instanceof 退场，add 的时候也没有任何转型；\u003C\u002Fli>\n   \u003Cli>root.print(\"\") 一行，整棵树递归打印出来，缩进都是各节点自己传下去的；\u003C\u002Fli>\n   \u003Cli>哪天加\"快捷方式\"：写一个 Shortcut 实现 FileComponent 就能进树，BadFolder 那种\"新列表 + 全链路改\"的事故不再发生——开闭原则。\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Ch2 id=\"toc-9\">五、透明式 vs 安全式：GoF 留下的经典取舍\u003C\u002Fh2>\n  \u003Cp>有个细节不知道你注意到没有：add\u002Fremove 为什么放在 Folder 里，而不上提到 FileComponent 接口？这不是随手一放，是 GoF 原书里就摆上台面的抉择。\u003C\u002Fp>\n  \u003Cp>\u003Cstrong>透明式（GoF 原版）\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:#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\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FileComponent\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\"> add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(FileComponent \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">component\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\"> remove\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(FileComponent \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">component\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    long\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getSizeKB\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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> File\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FileComponent\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\">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\"> add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(FileComponent \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">component\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> UnsupportedOperationException\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\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F remove 同理抛异常，其余方法照旧\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>：管理孩子的方法只放在 Composite 身上，叶子压根没有这方法。\u003C\u002Fp>\n  \u003Ctable>\n   \u003Cthead>\n    \u003Ctr>\n     \u003Cth>\u003C\u002Fth>\n     \u003Cth>透明式（GoF 原版）\u003C\u002Fth>\n     \u003Cth>安全式（本篇示例）\u003C\u002Fth>\n    \u003C\u002Ftr>\n   \u003C\u002Fthead>\n   \u003Ctbody>\n    \u003Ctr>\n     \u003Ctd>add\u002Fremove 放哪\u003C\u002Ftd>\n     \u003Ctd>Component 接口\u003C\u002Ftd>\n     \u003Ctd>只在 Composite\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n     \u003Ctd>叶子怎么交差\u003C\u002Ftd>\n     \u003Ctd>抛 UnsupportedOperationException\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   \u003C\u002Ftbody>\n  \u003C\u002Ftable>\n  \u003Cp>JDK 两种都选过：AWT 走安全式（add 在 Container 上，Button 上没有这个方法），DOM 走透明式（Node 接口就有 appendChild，往 Text 节点里塞孩子会抛 DOMException）。两家都活得好好的，说明这不是对错题，是口味题。\u003C\u002Fp>\n  \u003Cp>给个倾向性建议：\u003Cstrong>业务代码默认安全式\u003C\u002Fstrong>——让类型系统如实反映\"叶子生不了孩子\"，编译期诚实好过运行期惊吓；而且只要客户端的操作以\"读\"为主（遍历、统计、渲染），两种方式的体验完全一致，本篇示例的 uniform() 就证明了这点——一个循环处理混合列表，谁也没下转型，也没谁抛异常。\u003C\u002Fp>\n  \u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n  \u003Cp>回顾这一篇：组合模式把\"部分-整体\"组织成树——叶子与容器实现同一接口，容器持有组件列表，树形结构是类型的自然结果，递归不需要发明；反面教材\"两套体系\"两遍遍历、instanceof 硬判、加成员全链路遭殃；AWT、DOM、MyBatis 的 SqlNode、Jackson 的 JsonNode，以及后台系统里的每一棵菜单树部门树，都是它的地盘；安全式与透明式是 GoF 留下的经典取舍，业务代码建议默认安全式。\u003C\u002Fp>\n  \u003Cp>下一篇 ⑩ 装饰器模式，它是组合模式的近亲：UML 画出来，几乎是\"只准养一个孩子的组合模式\"。用途上两家分了工——组合管\"一对多长成树\"，装饰器管\"一对一层层加\"。不改原类动态加功能，Java IO 流里 \u003Ccode>new BufferedInputStream(new FileInputStream(file))\u003C\u002Fcode> 那套套娃，正是它的封神之作。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>",[103,106,109,113,116,119,122,125,128,131],{"id":104,"text":105,"level":98},"toc-1","一、什么是组合模式？",{"id":107,"text":108,"level":98},"toc-2","二、组合模式的优缺点",{"id":110,"text":111,"level":112},"toc-3","优点",3,{"id":114,"text":115,"level":112},"toc-4","缺点",{"id":117,"text":118,"level":98},"toc-5","三、组合模式的应用场景",{"id":120,"text":121,"level":98},"toc-6","四、组合模式的实现方式",{"id":123,"text":124,"level":112},"toc-7","1. 反面教材：两套体系",{"id":126,"text":127,"level":112},"toc-8","2. 组合模式版：一个接口，一棵树",{"id":129,"text":130,"level":98},"toc-9","五、透明式 vs 安全式：GoF 留下的经典取舍",{"id":132,"text":133,"level":98},"toc-10","写在最后"]