[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-11":26,"\u002Farticles\u002Fdesign-pattern-11\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-11\u002Fadjacent":72,"\u002Fcategories":91,"render-design-pattern-11":117},{"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},"2113933638763548673","design-pattern-11","设计模式修炼之路 ⑪ 外观模式——给杂乱子系统一个入口","给杂乱子系统一个统一入口、只编排不干活、SLF4J 名字里的 Facade","2113897628365230081",1,3176,7681,20,"2024-03-20T15:49:38","上一篇装饰器模式给它留了个悬念式的好评：\"GoF 里最厚道的一个\"。今天正主登场：**外观模式（Facade Pattern）**，GoF 23 人名单正牌选手、结构型第六位。前面几位多少都带了点\"设计感\"——适配器要换接口、桥接要拆维度、装饰器要玩套娃——唯独它朴素到不像个模式：**给你不想伺候的一大堆类，收拢成一个门面**。子系统内部该多乱还多乱，客户端只见一个入口、一个方法、一句话办完。降复杂度不涨姿势，程序员和甲方都爱的那一款。\n\n## 一、什么是外观模式？\n\n先看定义：\n\n> 为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口，这个接口使得这一子系统更加容易使用。\n\n翻译成人话：**一堆类协作起来才能干成一件事，那就写一个类把\"协作的流程\"包起来——客户端只调门面的一个方法，门面在背后按顺序指挥子系统们干活**。客户端从\"自己当指挥\"降级成\"说一句话\"，中间差的那些步骤、顺序、依赖，全是门面替它扛的。\n\n生活类比是 4S 店销售：你想买辆车，不用自己跑车管所上牌、跑保险公司买保险、跑加油站办油卡、跑银行办贷款——销售一句话：\"包在我身上\"，转头他替你把全流程串起来。上牌、保险、贷款这些\"子系统\"照旧各干各的，销售这个\"门面\"什么新业务都没发明，他只是**知道该找谁、按什么顺序找**。\n\n它只有两个角色，GoF 23 种模式里最少的一档：\n\n- **Facade（门面）**：唯一的编排处。持有子系统的引用，对外提供\"一句话\"级别的粗粒度方法；\n- **Subsystem（子系统）**：若干个真正干活的类。**它们不知道门面的存在**——这条单向依赖是外观模式最鲜明的特征：门面 import 子系统，子系统绝不 import 门面。\n\n精髓一句话：**门面只编排，不干活**。它每个方法的方法体，本质都是\"把子系统的方法按正确顺序各调一遍\"，没有一行新的业务逻辑——这既是它厚道的地方（加它不改变任何既有行为），也是判断一个\"门面\"写得对不对的试金石，第五节专门说。\n\n## 二、外观模式的优缺点\n\n### 优点\n\n1. **客户端与子系统解耦**：客户端只认门面，子系统内部怎么重构、类怎么拆、实现怎么换，客户端一行不改——门面是挡在\"变化\"前面的墙。\n2. **复杂度收口**：五步流程收成一句话，而且这份\"流程知识\"从 N 个客户端收回到 1 个门面里，全局只写一份。\n3. **不绑架任何人**：门面是便捷层，不是隔离墙——需要细粒度控制时，客户端随时可以绕过门面直接调子系统，两者共存毫无障碍。对照着想想 API 网关\"只许走网关\"的强制风格，外观这份\"不强制\"格外难得。\n4. **天然分层**：每层对外暴露一个门面、对内自由生长，这是它给大型代码库最值钱的贡献。\n\n### 缺点\n\n1. **门面容易长成上帝类**：什么都觉得\"顺手包一下\"，三年后门面三千行、什么用例都往里塞——门面的边界感，比技术含量更考验人。\n2. **加步骤要动门面**：流程里插一步\"数据脱敏\"，门面方法体要改。好在多数时候是\"加一个门面方法\"而不是\"改老方法\"，开闭原则算是守住了底裤。\n3. **多一层转发**：严格说每调用一次多一跳，不过这层薄得可以忽略，真在意的话你大概不该用反射框架（笑）。\n\n## 三、外观模式的应用场景\n\n- **SLF4J（日志门面）**：全宇宙名字起得最诚实的框架——Simple Logging **Facade** for Java。LoggerFactory.getLogger() 背后，Logback、Log4j2、JUL 这些\"子系统\"随你替换，业务代码一行不动。为什么大厂规范都要求\"只用 SLF4J 的 API、不许直接用实现框架\"？就是在守护门面这堵墙。\n- **Spring 的 JdbcTemplate**：JDBC 原生那套——开 Connection、建 Statement、设参数、执行、遍历 ResultSet、关资源、转异常——六步_protocol 藏在 query()\u002Fupdate() 两个门面方法背后。你只管传 SQL 和回调，顺序和善后全是它编排。\n- **MyBatis 的 SqlSession**：一句 selectOne() 背后，Executor、StatementHandler、ParameterHandler、ResultSetHandler 一长串组件按管线协作，SqlSession 就是它们的总门面——本系列第五次在 MyBatis 里撞见老朋友。\n- **Tomcat 的 RequestFacade**：org.apache.catalina.connector.RequestFacade 包着内部 Request，只暴露 HttpServletRequest 接口，防止应用向下转型篡改容器内部状态——门面的一个变体玩法：保护性门面，重点从\"简化\"变成了\"隔离越权\"。\n- **各路 SDK 的 XxClient**：云厂商 SDK 里那个 XxClient，init 几行配置，一个方法发出去，背后签名、鉴权、重试、序列化全包了——每个\"好用的 SDK\"背后都站着一个尽职的门面。\n\n**什么时候该用**：一段\"多步协作流程\"被第二个客户端复制粘贴的那一刻，就是门面该出场的时刻——一个客户端时流程写哪都行，两个客户端就该收口了；或者子系统预期会换实现（日志框架、缓存实现），门面先把依赖钉死在接口层。\n\n## 四、外观模式的实现方式\n\n示例代码在 design-pattern 项目的 facade 包里：\n\n```\ndesign-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── facade                    \u002F\u002F 外观模式\n    ├── DataFetcher.java      \u002F\u002F 子系统：查数据\n    ├── DataAggregator.java   \u002F\u002F 子系统：汇总指标\n    ├── ExcelWriter.java      \u002F\u002F 子系统：写 Excel\n    ├── OssUploader.java      \u002F\u002F 子系统：传对象存储\n    ├── MailNotifier.java     \u002F\u002F 子系统：发邮件\n    ├── BadController.java    \u002F\u002F 反面教材：Web 客户端自己编排五步\n    ├── BadReportJob.java     \u002F\u002F 反面教材：定时任务复制粘贴同样五步\n    ├── ReportFacade.java     \u002F\u002F 门面：唯一的编排处\n    ├── WebController.java    \u002F\u002F 正解客户端：Web 触发\n    ├── ReportJob.java        \u002F\u002F 正解客户端：定时任务触发\n    └── FacadeTest.java       \u002F\u002F 测试入口\n```\n\n场景：导出报表。完整流程五步——查数据、汇总、写 Excel、传 OSS、发邮件，五个类各管一步。这个流程有两个入口都要用：Web 接口（用户点导出按钮）和定时任务（每周一自动跑）。先看\"每个客户端自己编排\"的下场，再看门面怎么收口。\n\n### 1. 反面教材：流程散落在客户端\n\n五个子系统类先各就各位（实现都只有一行 println，略过），然后 Web 客户端亲自下场当指挥：\n\n```java\n\u002F\u002F 反面教材：Web 接口触发的导出，五步流程全写在客户端里\npublic class BadController {\n    private final DataFetcher fetcher = new DataFetcher();\n    private final DataAggregator aggregator = new DataAggregator();\n    private final ExcelWriter excelWriter = new ExcelWriter();\n    private final OssUploader ossUploader = new OssUploader();\n    private final MailNotifier mailNotifier = new MailNotifier();\n\n    public void exportReport(String bizId, String email) {\n        System.out.println(\"[BadController] 导出 \" + bizId + \" 的报表：\");\n        List\u003CString> raw = fetcher.fetch(bizId);\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(bizId, summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, email);\n    }\n}\n```\n\n看起来还行？第二个客户端进来的那一刻，问题显形——定时任务也要导出报表，只好把五步**原样复制一遍**：\n\n```java\n\u002F\u002F 反面教材：定时任务也要导出报表，五步流程只好复制粘贴一遍\npublic class BadReportJob {\n    public void runWeekly() {\n        System.out.println(\"[BadReportJob] 每周一自动导出报表：\");\n        \u002F\u002F ↓↓ 和 BadController.exportReport 一模一样的五步，复制粘贴来的\n        DataFetcher fetcher = new DataFetcher();\n        DataAggregator aggregator = new DataAggregator();\n        ExcelWriter excelWriter = new ExcelWriter();\n        OssUploader ossUploader = new OssUploader();\n        MailNotifier mailNotifier = new MailNotifier();\n\n        List\u003CString> raw = fetcher.fetch(\"WEEK-2026-42\");\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(\"WEEK-2026-42\", summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, \"data-team@example.com\");\n    }\n}\n```\n\n运行结果：\n\n```\n===== 反面教材：流程散落在客户端 =====\n[BadController] 导出 ORDER-1001 的报表：\n查数据：拉取到 ORDER-1001 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 ORDER-1001-报表.xlsx\n传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n发邮件：报表已发送到 boss@example.com\n\n[BadReportJob] 每周一自动导出报表：\n查数据：拉取到 WEEK-2026-42 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 WEEK-2026-42-报表.xlsx\n传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\n发邮件：报表已发送到 data-team@example.com\n```\n\n能跑，但三颗雷已经埋下：\n\n1. **流程知识全局重复**：五个步骤、它们的顺序、每步的入参出参怎么衔接——这套\"编排知识\"被复制成了两份。明天流程中间加一步\"数据脱敏\"？两个客户端挨个改，漏改一个就是一个线上事故；\n2. **客户端被迫认识全世界**：BadController 得 import 五个子系统、知道五个类的职责边界和协作顺序——它本该只关心\"导出报表，返回 URL\"这一件事；\n3. **子系统失去重构自由**：哪天想把 DataFetcher 和 DataAggregator 合并成一个类？所有编排过它们的客户端全部遭殃——子系统想动一动，得先数数全世界有几个客户端。\n\n病根只有一个：**\"流程该怎么走\"这份知识，写在了每个客户端里，而它本该只有一份**。\n\n### 2. 外观版：一个门面，一句点单\n\n门面登场，它唯一的本事是\"知道该找谁、按什么顺序找\"：\n\n```java\n\u002F\u002F 门面：唯一的编排处——五个子系统怎么配合、什么顺序，只有它知道\npublic class ReportFacade {\n    private final DataFetcher fetcher = new DataFetcher();\n    private final DataAggregator aggregator = new DataAggregator();\n    private final ExcelWriter excelWriter = new ExcelWriter();\n    private final OssUploader ossUploader = new OssUploader();\n    private final MailNotifier mailNotifier = new MailNotifier();\n\n    \u002F\u002F 客户端只需要一句话，五个子系统在门面背后鱼贯而出\n    public String exportAndSend(String bizId, String email) {\n        List\u003CString> raw = fetcher.fetch(bizId);\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(bizId, summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, email);\n        return url;\n    }\n}\n```\n\n注意方法体：和反面教材一模一样的五步，一个字没多、一个字没少——**门面没有发明任何新逻辑，它只是把这段编排从 N 个客户端收回到 1 个类里**。然后两个客户端集体瘦身：\n\n```java\n\u002F\u002F 正解客户端：Web 接口触发的导出，编排细节一概不知\npublic class WebController {\n    private final ReportFacade facade = new ReportFacade();\n\n    public void exportReport(String bizId, String email) {\n        System.out.println(\"[WebController] 导出 \" + bizId + \" 的报表：\");\n        String url = facade.exportAndSend(bizId, email);\n        System.out.println(\"[WebController] 返回给前端：\" + url);\n    }\n}\n\n\u002F\u002F 正解客户端：定时任务触发的导出，同样一句话\npublic class ReportJob {\n    private final ReportFacade facade = new ReportFacade();\n\n    public void runWeekly() {\n        System.out.println(\"[ReportJob] 每周一自动导出报表：\");\n        facade.exportAndSend(\"WEEK-2026-42\", \"data-team@example.com\");\n    }\n}\n```\n\n运行结果：\n\n```\n===== 正解：外观模式 =====\n[WebController] 导出 ORDER-1001 的报表：\n查数据：拉取到 ORDER-1001 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 ORDER-1001-报表.xlsx\n传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n发邮件：报表已发送到 boss@example.com\n[WebController] 返回给前端：https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n\n[ReportJob] 每周一自动导出报表：\n查数据：拉取到 WEEK-2026-42 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 WEEK-2026-42-报表.xlsx\n传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\n发邮件：报表已发送到 data-team@example.com\n```\n\n对照反面教材逐条验收：\n\n- 五步编排从两份（N 份）变回一份，明早加\"数据脱敏\"？改 ReportFacade 一个类，两个客户端睡他们的觉；\n- 客户端只 import 一个 ReportFacade，五个子系统从它的世界里彻底消失——想认识它都不需要；\n- 子系统重获自由：合并 DataFetcher 和 DataAggregator、给 OssUploader 换供应商，只要门面方法的签名不动，客户端零感知；\n- 两个客户端的职责也各自归位了——WebController 回去专心管\"参数校验、返回响应\"，ReportJob 专心管\"调度\"，编排这种脏活谁也不该抢。\n\n## 五、你天天在写外观，只是没叫它名字\n\n外观模式最容易被小看，因为它看起来\"没技术含量\"。但把视角拉高你会发现：**三层架构里的 Service 层，本职工作就是 DAO 层的门面**——Controller 调一个 orderService.createOrder()，背后 DAO 存订单、扣库存、记流水，那不就是一个标准的门面方法？再说细一点，工具类里那些把复杂 API 包一层的静态方法、SDK 里那个什么都管的 XxClient，全是门面。你天天在写外观，只是没叫它名字。\n\n正因为天天在写，才更该有几个判断标准，防它长歪：\n\n1. **门面只编排，不干活**：门面方法体里若出现了大段业务逻辑——if 分支、循环计算、字符串拼接——它已经开始从\"门面\"堕落成\"业务类\"。一个狠一点的检验法：把门面类删掉，它的每个方法体能否在子系统里原样重演？能，它是门面；不能，它偷偷藏了私货。\n2. **子系统不知道门面存在**：子系统类 import 门面？依赖方向反了。门面可以认识所有子系统，子系统一个门面都不许认识——这条单向依赖一旦破坏，门面就成了网状依赖的中心节点，第五节的标题就该换成\"中介者模式\"了（那个模式㉒ 再聊，先把账记下：门面是**单向**的传话筒，中介者是**双向**的交通枢纽）。\n3. **一个门面只管一类用例**：ReportFacade 管导出，就别把\"用户注册\"也塞进来顺手。门面长成三千行上帝类，几乎都是从\"顺手包一下\"开始的——门面的粒度对齐的是**用例**，不是\"整个子系统\"。\n4. **门面不是唯一入口**：客户端偶尔需要绕过门面直接调子系统（比如只要查数据、不要导出全程）？完全可以，外观从不拦路。真想拦，那是上一条里 Tomcat RequestFacade 那种保护性门面的活儿，得明确知道自己为什么要拦。\n\n## 写在最后\n\n回顾这一篇：外观模式给一组子系统类提供一个一致的入口——门面持有子系统、按序编排、不添一行新逻辑，子系统对门面毫不知情；反面教材\"流程散落在客户端\"三宗罪——编排知识全局重复、客户端被迫认识全世界、子系统失去重构自由；SLF4J、JdbcTemplate、SqlSession、各路 SDK 的 Client，全是它的地盘；而\"只编排不干活、单向依赖、用例粒度、不绑架客户端\"四条标准，守的是门面和上帝类之间那条细线。\n\n下一篇 ⑫ 享元模式，结构型阵营只剩它和代理压轴。前面几位省的是\"代码的复杂度\"，它省的是\"货真价实的内存\"——同样的对象造一万份，享元说：留一份共享，其余都指过来。Integer.valueOf 里 -128 到 127 的缓存池、字符串常量池里那个\"字面量只存一份\"的规矩，全是你天天踩在脚下却没低头看过的享元。\n","\u003Cp>上一篇装饰器模式给它留了个悬念式的好评：\"GoF 里最厚道的一个\"。今天正主登场：\u003Cstrong>外观模式（Facade Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第六位。前面几位多少都带了点\"设计感\"——适配器要换接口、桥接要拆维度、装饰器要玩套娃——唯独它朴素到不像个模式：\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>生活类比是 4S 店销售：你想买辆车，不用自己跑车管所上牌、跑保险公司买保险、跑加油站办油卡、跑银行办贷款——销售一句话：\"包在我身上\"，转头他替你把全流程串起来。上牌、保险、贷款这些\"子系统\"照旧各干各的，销售这个\"门面\"什么新业务都没发明，他只是\u003Cstrong>知道该找谁、按什么顺序找\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>它只有两个角色，GoF 23 种模式里最少的一档：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Facade（门面）\u003C\u002Fstrong>：唯一的编排处。持有子系统的引用，对外提供\"一句话\"级别的粗粒度方法；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Subsystem（子系统）\u003C\u002Fstrong>：若干个真正干活的类。\u003Cstrong>它们不知道门面的存在\u003C\u002Fstrong>——这条单向依赖是外观模式最鲜明的特征：门面 import 子系统，子系统绝不 import 门面。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓一句话：\u003Cstrong>门面只编排，不干活\u003C\u002Fstrong>。它每个方法的方法体，本质都是\"把子系统的方法按正确顺序各调一遍\"，没有一行新的业务逻辑——这既是它厚道的地方（加它不改变任何既有行为），也是判断一个\"门面\"写得对不对的试金石，第五节专门说。\u003C\u002Fp>\n\u003Ch2>二、外观模式的优缺点\u003C\u002Fh2>\n\u003Ch3>优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>客户端与子系统解耦\u003C\u002Fstrong>：客户端只认门面，子系统内部怎么重构、类怎么拆、实现怎么换，客户端一行不改——门面是挡在\"变化\"前面的墙。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>复杂度收口\u003C\u002Fstrong>：五步流程收成一句话，而且这份\"流程知识\"从 N 个客户端收回到 1 个门面里，全局只写一份。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不绑架任何人\u003C\u002Fstrong>：门面是便捷层，不是隔离墙——需要细粒度控制时，客户端随时可以绕过门面直接调子系统，两者共存毫无障碍。对照着想想 API 网关\"只许走网关\"的强制风格，外观这份\"不强制\"格外难得。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>天然分层\u003C\u002Fstrong>：每层对外暴露一个门面、对内自由生长，这是它给大型代码库最值钱的贡献。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>缺点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>门面容易长成上帝类\u003C\u002Fstrong>：什么都觉得\"顺手包一下\"，三年后门面三千行、什么用例都往里塞——门面的边界感，比技术含量更考验人。\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>SLF4J（日志门面）\u003C\u002Fstrong>：全宇宙名字起得最诚实的框架——Simple Logging \u003Cstrong>Facade\u003C\u002Fstrong> for Java。LoggerFactory.getLogger() 背后，Logback、Log4j2、JUL 这些\"子系统\"随你替换，业务代码一行不动。为什么大厂规范都要求\"只用 SLF4J 的 API、不许直接用实现框架\"？就是在守护门面这堵墙。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Spring 的 JdbcTemplate\u003C\u002Fstrong>：JDBC 原生那套——开 Connection、建 Statement、设参数、执行、遍历 ResultSet、关资源、转异常——六步_protocol 藏在 query()\u002Fupdate() 两个门面方法背后。你只管传 SQL 和回调，顺序和善后全是它编排。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 的 SqlSession\u003C\u002Fstrong>：一句 selectOne() 背后，Executor、StatementHandler、ParameterHandler、ResultSetHandler 一长串组件按管线协作，SqlSession 就是它们的总门面——本系列第五次在 MyBatis 里撞见老朋友。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Tomcat 的 RequestFacade\u003C\u002Fstrong>：org.apache.catalina.connector.RequestFacade 包着内部 Request，只暴露 HttpServletRequest 接口，防止应用向下转型篡改容器内部状态——门面的一个变体玩法：保护性门面，重点从\"简化\"变成了\"隔离越权\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>各路 SDK 的 XxClient\u003C\u002Fstrong>：云厂商 SDK 里那个 XxClient，init 几行配置，一个方法发出去，背后签名、鉴权、重试、序列化全包了——每个\"好用的 SDK\"背后都站着一个尽职的门面。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：一段\"多步协作流程\"被第二个客户端复制粘贴的那一刻，就是门面该出场的时刻——一个客户端时流程写哪都行，两个客户端就该收口了；或者子系统预期会换实现（日志框架、缓存实现），门面先把依赖钉死在接口层。\u003C\u002Fp>\n\u003Ch2>四、外观模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 facade 包里：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>design-pattern\u002Fsrc\u002Fmain\u002Fjava\n└── facade                    \u002F\u002F 外观模式\n    ├── DataFetcher.java      \u002F\u002F 子系统：查数据\n    ├── DataAggregator.java   \u002F\u002F 子系统：汇总指标\n    ├── ExcelWriter.java      \u002F\u002F 子系统：写 Excel\n    ├── OssUploader.java      \u002F\u002F 子系统：传对象存储\n    ├── MailNotifier.java     \u002F\u002F 子系统：发邮件\n    ├── BadController.java    \u002F\u002F 反面教材：Web 客户端自己编排五步\n    ├── BadReportJob.java     \u002F\u002F 反面教材：定时任务复制粘贴同样五步\n    ├── ReportFacade.java     \u002F\u002F 门面：唯一的编排处\n    ├── WebController.java    \u002F\u002F 正解客户端：Web 触发\n    ├── ReportJob.java        \u002F\u002F 正解客户端：定时任务触发\n    └── FacadeTest.java       \u002F\u002F 测试入口\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：导出报表。完整流程五步——查数据、汇总、写 Excel、传 OSS、发邮件，五个类各管一步。这个流程有两个入口都要用：Web 接口（用户点导出按钮）和定时任务（每周一自动跑）。先看\"每个客户端自己编排\"的下场，再看门面怎么收口。\u003C\u002Fp>\n\u003Ch3>1. 反面教材：流程散落在客户端\u003C\u002Fh3>\n\u003Cp>五个子系统类先各就各位（实现都只有一行 println，略过），然后 Web 客户端亲自下场当指挥：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 反面教材：Web 接口触发的导出，五步流程全写在客户端里\npublic class BadController {\n    private final DataFetcher fetcher = new DataFetcher();\n    private final DataAggregator aggregator = new DataAggregator();\n    private final ExcelWriter excelWriter = new ExcelWriter();\n    private final OssUploader ossUploader = new OssUploader();\n    private final MailNotifier mailNotifier = new MailNotifier();\n\n    public void exportReport(String bizId, String email) {\n        System.out.println(\"[BadController] 导出 \" + bizId + \" 的报表：\");\n        List&lt;String&gt; raw = fetcher.fetch(bizId);\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(bizId, summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, email);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>看起来还行？第二个客户端进来的那一刻，问题显形——定时任务也要导出报表，只好把五步\u003Cstrong>原样复制一遍\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 反面教材：定时任务也要导出报表，五步流程只好复制粘贴一遍\npublic class BadReportJob {\n    public void runWeekly() {\n        System.out.println(\"[BadReportJob] 每周一自动导出报表：\");\n        \u002F\u002F ↓↓ 和 BadController.exportReport 一模一样的五步，复制粘贴来的\n        DataFetcher fetcher = new DataFetcher();\n        DataAggregator aggregator = new DataAggregator();\n        ExcelWriter excelWriter = new ExcelWriter();\n        OssUploader ossUploader = new OssUploader();\n        MailNotifier mailNotifier = new MailNotifier();\n\n        List&lt;String&gt; raw = fetcher.fetch(\"WEEK-2026-42\");\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(\"WEEK-2026-42\", summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, \"data-team@example.com\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 反面教材：流程散落在客户端 =====\n[BadController] 导出 ORDER-1001 的报表：\n查数据：拉取到 ORDER-1001 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 ORDER-1001-报表.xlsx\n传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n发邮件：报表已发送到 boss@example.com\n\n[BadReportJob] 每周一自动导出报表：\n查数据：拉取到 WEEK-2026-42 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 WEEK-2026-42-报表.xlsx\n传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\n发邮件：报表已发送到 data-team@example.com\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>能跑，但三颗雷已经埋下：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>流程知识全局重复\u003C\u002Fstrong>：五个步骤、它们的顺序、每步的入参出参怎么衔接——这套\"编排知识\"被复制成了两份。明天流程中间加一步\"数据脱敏\"？两个客户端挨个改，漏改一个就是一个线上事故；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>客户端被迫认识全世界\u003C\u002Fstrong>：BadController 得 import 五个子系统、知道五个类的职责边界和协作顺序——它本该只关心\"导出报表，返回 URL\"这一件事；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>子系统失去重构自由\u003C\u002Fstrong>：哪天想把 DataFetcher 和 DataAggregator 合并成一个类？所有编排过它们的客户端全部遭殃——子系统想动一动，得先数数全世界有几个客户端。\u003C\u002Fli>\n\u003C\u002Fol>\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 class ReportFacade {\n    private final DataFetcher fetcher = new DataFetcher();\n    private final DataAggregator aggregator = new DataAggregator();\n    private final ExcelWriter excelWriter = new ExcelWriter();\n    private final OssUploader ossUploader = new OssUploader();\n    private final MailNotifier mailNotifier = new MailNotifier();\n\n    \u002F\u002F 客户端只需要一句话，五个子系统在门面背后鱼贯而出\n    public String exportAndSend(String bizId, String email) {\n        List&lt;String&gt; raw = fetcher.fetch(bizId);\n        String summary = aggregator.aggregate(raw);\n        String file = excelWriter.write(bizId, summary);\n        String url = ossUploader.upload(file);\n        mailNotifier.sendMail(url, email);\n        return url;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意方法体：和反面教材一模一样的五步，一个字没多、一个字没少——\u003Cstrong>门面没有发明任何新逻辑，它只是把这段编排从 N 个客户端收回到 1 个类里\u003C\u002Fstrong>。然后两个客户端集体瘦身：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F\u002F 正解客户端：Web 接口触发的导出，编排细节一概不知\npublic class WebController {\n    private final ReportFacade facade = new ReportFacade();\n\n    public void exportReport(String bizId, String email) {\n        System.out.println(\"[WebController] 导出 \" + bizId + \" 的报表：\");\n        String url = facade.exportAndSend(bizId, email);\n        System.out.println(\"[WebController] 返回给前端：\" + url);\n    }\n}\n\n\u002F\u002F 正解客户端：定时任务触发的导出，同样一句话\npublic class ReportJob {\n    private final ReportFacade facade = new ReportFacade();\n\n    public void runWeekly() {\n        System.out.println(\"[ReportJob] 每周一自动导出报表：\");\n        facade.exportAndSend(\"WEEK-2026-42\", \"data-team@example.com\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行结果：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>===== 正解：外观模式 =====\n[WebController] 导出 ORDER-1001 的报表：\n查数据：拉取到 ORDER-1001 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 ORDER-1001-报表.xlsx\n传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n发邮件：报表已发送到 boss@example.com\n[WebController] 返回给前端：https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\n\n[ReportJob] 每周一自动导出报表：\n查数据：拉取到 WEEK-2026-42 的 5 条明细\n汇总：明细 5 条，总金额 1280.00\n写 Excel：生成文件 WEEK-2026-42-报表.xlsx\n传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\n发邮件：报表已发送到 data-team@example.com\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>对照反面教材逐条验收：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>五步编排从两份（N 份）变回一份，明早加\"数据脱敏\"？改 ReportFacade 一个类，两个客户端睡他们的觉；\u003C\u002Fli>\n \u003Cli>客户端只 import 一个 ReportFacade，五个子系统从它的世界里彻底消失——想认识它都不需要；\u003C\u002Fli>\n \u003Cli>子系统重获自由：合并 DataFetcher 和 DataAggregator、给 OssUploader 换供应商，只要门面方法的签名不动，客户端零感知；\u003C\u002Fli>\n \u003Cli>两个客户端的职责也各自归位了——WebController 回去专心管\"参数校验、返回响应\"，ReportJob 专心管\"调度\"，编排这种脏活谁也不该抢。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>五、你天天在写外观，只是没叫它名字\u003C\u002Fh2>\n\u003Cp>外观模式最容易被小看，因为它看起来\"没技术含量\"。但把视角拉高你会发现：\u003Cstrong>三层架构里的 Service 层，本职工作就是 DAO 层的门面\u003C\u002Fstrong>——Controller 调一个 orderService.createOrder()，背后 DAO 存订单、扣库存、记流水，那不就是一个标准的门面方法？再说细一点，工具类里那些把复杂 API 包一层的静态方法、SDK 里那个什么都管的 XxClient，全是门面。你天天在写外观，只是没叫它名字。\u003C\u002Fp>\n\u003Cp>正因为天天在写，才更该有几个判断标准，防它长歪：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>门面只编排，不干活\u003C\u002Fstrong>：门面方法体里若出现了大段业务逻辑——if 分支、循环计算、字符串拼接——它已经开始从\"门面\"堕落成\"业务类\"。一个狠一点的检验法：把门面类删掉，它的每个方法体能否在子系统里原样重演？能，它是门面；不能，它偷偷藏了私货。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>子系统不知道门面存在\u003C\u002Fstrong>：子系统类 import 门面？依赖方向反了。门面可以认识所有子系统，子系统一个门面都不许认识——这条单向依赖一旦破坏，门面就成了网状依赖的中心节点，第五节的标题就该换成\"中介者模式\"了（那个模式㉒ 再聊，先把账记下：门面是\u003Cstrong>单向\u003C\u002Fstrong>的传话筒，中介者是\u003Cstrong>双向\u003C\u002Fstrong>的交通枢纽）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一个门面只管一类用例\u003C\u002Fstrong>：ReportFacade 管导出，就别把\"用户注册\"也塞进来顺手。门面长成三千行上帝类，几乎都是从\"顺手包一下\"开始的——门面的粒度对齐的是\u003Cstrong>用例\u003C\u002Fstrong>，不是\"整个子系统\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>门面不是唯一入口\u003C\u002Fstrong>：客户端偶尔需要绕过门面直接调子系统（比如只要查数据、不要导出全程）？完全可以，外观从不拦路。真想拦，那是上一条里 Tomcat RequestFacade 那种保护性门面的活儿，得明确知道自己为什么要拦。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：外观模式给一组子系统类提供一个一致的入口——门面持有子系统、按序编排、不添一行新逻辑，子系统对门面毫不知情；反面教材\"流程散落在客户端\"三宗罪——编排知识全局重复、客户端被迫认识全世界、子系统失去重构自由；SLF4J、JdbcTemplate、SqlSession、各路 SDK 的 Client，全是它的地盘；而\"只编排不干活、单向依赖、用例粒度、不绑架客户端\"四条标准，守的是门面和上帝类之间那条细线。\u003C\u002Fp>\n\u003Cp>下一篇 ⑫ 享元模式，结构型阵营只剩它和代理压轴。前面几位省的是\"代码的复杂度\"，它省的是\"货真价实的内存\"——同样的对象造一万份，享元说：留一份共享，其余都指过来。Integer.valueOf 里 -128 到 127 的缓存池、字符串常量池里那个\"字面量只存一份\"的规矩，全是你天天踩在脚下却没低头看过的享元。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113932970753527809","design-pattern-13","设计模式修炼之路 ⑬ 代理模式——控制访问，而非增强","控制访问而非增强、JDK 动态代理的 $Proxy0、CGLIB 与 Spring AOP 的老搭档",3804,9153,23,"2024-05-02T10:29:24",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113931305484816386","design-pattern-06","设计模式修炼之路 ⑥ 适配器模式——让不兼容的接口握手","类适配器 vs 对象适配器、在 Spring MVC 里找它",1578,5381,14,"2023-12-11T21:59:29",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113931131085656066","design-pattern-03","设计模式修炼之路 ③ 工厂模式——简单工厂、工厂方法、抽象工厂三兄弟","简单工厂、工厂方法、抽象工厂三兄弟的演进与取舍、开闭原则得失",3741,6415,17,"2023-10-13T21:36:05",{"code":4,"message":5,"data":73},{"prev":74,"next":82},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":36,"publishedAt":81},"2113931625669595138","design-pattern-10","设计模式修炼之路 ⑩ 装饰器模式——不改原类，动态加功能","不改原类动态加功能、\"我是组件+我有组件\"、Java IO 流的套娃设计",1003,7847,"2024-02-10T15:14:16",{"id":83,"slug":84,"title":85,"summary":86,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":87,"likeCount":4,"wordCount":88,"readingMinutes":89,"publishedAt":90},"2113931783249596417","design-pattern-12","设计模式修炼之路 ⑫ 享元模式——共享细粒度对象","共享细粒度对象、内部状态与外部状态的切分、Integer 缓存与字符串常量池",2866,6956,18,"2024-04-15T17:46:59",{"code":4,"message":5,"data":92},[93,99,104,110],{"id":94,"name":95,"slug":96,"description":97,"sortOrder":4,"articleCount":98},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":100,"slug":101,"description":102,"sortOrder":4,"articleCount":103},"设计模式","design-pattern","设计模式系列","34",{"id":105,"name":106,"slug":107,"description":108,"sortOrder":33,"articleCount":109},"1946000000000000002","随笔","essay","生活与思考","0",{"id":111,"name":112,"slug":113,"description":114,"sortOrder":115,"articleCount":116},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":118,"toc":119},"\u003Cp>上一篇装饰器模式给它留了个悬念式的好评：\"GoF 里最厚道的一个\"。今天正主登场：\u003Cstrong>外观模式（Facade Pattern）\u003C\u002Fstrong>，GoF 23 人名单正牌选手、结构型第六位。前面几位多少都带了点\"设计感\"——适配器要换接口、桥接要拆维度、装饰器要玩套娃——唯独它朴素到不像个模式：\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>生活类比是 4S 店销售：你想买辆车，不用自己跑车管所上牌、跑保险公司买保险、跑加油站办油卡、跑银行办贷款——销售一句话：\"包在我身上\"，转头他替你把全流程串起来。上牌、保险、贷款这些\"子系统\"照旧各干各的，销售这个\"门面\"什么新业务都没发明，他只是\u003Cstrong>知道该找谁、按什么顺序找\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>它只有两个角色，GoF 23 种模式里最少的一档：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>Facade（门面）\u003C\u002Fstrong>：唯一的编排处。持有子系统的引用，对外提供\"一句话\"级别的粗粒度方法；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Subsystem（子系统）\u003C\u002Fstrong>：若干个真正干活的类。\u003Cstrong>它们不知道门面的存在\u003C\u002Fstrong>——这条单向依赖是外观模式最鲜明的特征：门面 import 子系统，子系统绝不 import 门面。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>精髓一句话：\u003Cstrong>门面只编排，不干活\u003C\u002Fstrong>。它每个方法的方法体，本质都是\"把子系统的方法按正确顺序各调一遍\"，没有一行新的业务逻辑——这既是它厚道的地方（加它不改变任何既有行为），也是判断一个\"门面\"写得对不对的试金石，第五节专门说。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、外观模式的优缺点\u003C\u002Fh2>\n\u003Ch3 id=\"toc-3\">优点\u003C\u002Fh3>\n\u003Col>\n \u003Cli>\u003Cstrong>客户端与子系统解耦\u003C\u002Fstrong>：客户端只认门面，子系统内部怎么重构、类怎么拆、实现怎么换，客户端一行不改——门面是挡在\"变化\"前面的墙。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>复杂度收口\u003C\u002Fstrong>：五步流程收成一句话，而且这份\"流程知识\"从 N 个客户端收回到 1 个门面里，全局只写一份。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不绑架任何人\u003C\u002Fstrong>：门面是便捷层，不是隔离墙——需要细粒度控制时，客户端随时可以绕过门面直接调子系统，两者共存毫无障碍。对照着想想 API 网关\"只许走网关\"的强制风格，外观这份\"不强制\"格外难得。\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>：什么都觉得\"顺手包一下\"，三年后门面三千行、什么用例都往里塞——门面的边界感，比技术含量更考验人。\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>SLF4J（日志门面）\u003C\u002Fstrong>：全宇宙名字起得最诚实的框架——Simple Logging \u003Cstrong>Facade\u003C\u002Fstrong> for Java。LoggerFactory.getLogger() 背后，Logback、Log4j2、JUL 这些\"子系统\"随你替换，业务代码一行不动。为什么大厂规范都要求\"只用 SLF4J 的 API、不许直接用实现框架\"？就是在守护门面这堵墙。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Spring 的 JdbcTemplate\u003C\u002Fstrong>：JDBC 原生那套——开 Connection、建 Statement、设参数、执行、遍历 ResultSet、关资源、转异常——六步_protocol 藏在 query()\u002Fupdate() 两个门面方法背后。你只管传 SQL 和回调，顺序和善后全是它编排。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>MyBatis 的 SqlSession\u003C\u002Fstrong>：一句 selectOne() 背后，Executor、StatementHandler、ParameterHandler、ResultSetHandler 一长串组件按管线协作，SqlSession 就是它们的总门面——本系列第五次在 MyBatis 里撞见老朋友。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Tomcat 的 RequestFacade\u003C\u002Fstrong>：org.apache.catalina.connector.RequestFacade 包着内部 Request，只暴露 HttpServletRequest 接口，防止应用向下转型篡改容器内部状态——门面的一个变体玩法：保护性门面，重点从\"简化\"变成了\"隔离越权\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>各路 SDK 的 XxClient\u003C\u002Fstrong>：云厂商 SDK 里那个 XxClient，init 几行配置，一个方法发出去，背后签名、鉴权、重试、序列化全包了——每个\"好用的 SDK\"背后都站着一个尽职的门面。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>什么时候该用\u003C\u002Fstrong>：一段\"多步协作流程\"被第二个客户端复制粘贴的那一刻，就是门面该出场的时刻——一个客户端时流程写哪都行，两个客户端就该收口了；或者子系统预期会换实现（日志框架、缓存实现），门面先把依赖钉死在接口层。\u003C\u002Fp>\n\u003Ch2 id=\"toc-6\">四、外观模式的实现方式\u003C\u002Fh2>\n\u003Cp>示例代码在 design-pattern 项目的 facade 包里：\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>└── facade                    \u002F\u002F 外观模式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── DataFetcher.java      \u002F\u002F 子系统：查数据\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── DataAggregator.java   \u002F\u002F 子系统：汇总指标\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── ExcelWriter.java      \u002F\u002F 子系统：写 Excel\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── OssUploader.java      \u002F\u002F 子系统：传对象存储\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── MailNotifier.java     \u002F\u002F 子系统：发邮件\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadController.java    \u002F\u002F 反面教材：Web 客户端自己编排五步\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── BadReportJob.java     \u002F\u002F 反面教材：定时任务复制粘贴同样五步\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── ReportFacade.java     \u002F\u002F 门面：唯一的编排处\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── WebController.java    \u002F\u002F 正解客户端：Web 触发\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    ├── ReportJob.java        \u002F\u002F 正解客户端：定时任务触发\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    └── FacadeTest.java       \u002F\u002F 测试入口\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>场景：导出报表。完整流程五步——查数据、汇总、写 Excel、传 OSS、发邮件，五个类各管一步。这个流程有两个入口都要用：Web 接口（用户点导出按钮）和定时任务（每周一自动跑）。先看\"每个客户端自己编排\"的下场，再看门面怎么收口。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">1. 反面教材：流程散落在客户端\u003C\u002Fh3>\n\u003Cp>五个子系统类先各就各位（实现都只有一行 println，略过），然后 Web 客户端亲自下场当指挥：\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 反面教材：Web 接口触发的导出，五步流程全写在客户端里\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\"> BadController\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\"> DataFetcher fetcher \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\"> DataFetcher\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\"> DataAggregator aggregator \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\"> DataAggregator\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\"> ExcelWriter excelWriter \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\"> ExcelWriter\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\"> OssUploader ossUploader \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\"> OssUploader\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\"> MailNotifier mailNotifier \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\"> MailNotifier\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> exportReport\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">bizId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">email\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\">\"[BadController] 导出 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bizId \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\">        List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> raw \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fetcher.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fetch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bizId);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String summary \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> aggregator.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">aggregate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String file \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> excelWriter.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">write\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bizId, summary);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String url \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ossUploader.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">upload\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(file);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mailNotifier.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">sendMail\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(url, email);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>看起来还行？第二个客户端进来的那一刻，问题显形——定时任务也要导出报表，只好把五步\u003Cstrong>原样复制一遍\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\"> BadReportJob\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\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\"> runWeekly\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\">\"[BadReportJob] 每周一自动导出报表：\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">        \u002F\u002F ↓↓ 和 BadController.exportReport 一模一样的五步，复制粘贴来的\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        DataFetcher fetcher \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\"> DataFetcher\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        DataAggregator aggregator \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\"> DataAggregator\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ExcelWriter excelWriter \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\"> ExcelWriter\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        OssUploader ossUploader \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\"> OssUploader\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        MailNotifier mailNotifier \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\"> MailNotifier\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> raw \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fetcher.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fetch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"WEEK-2026-42\"\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 summary \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> aggregator.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">aggregate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String file \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> excelWriter.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">write\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"WEEK-2026-42\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, summary);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String url \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ossUploader.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">upload\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(file);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mailNotifier.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">sendMail\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(url, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data-team@example.com\"\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>[BadController] 导出 ORDER-1001 的报表：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>查数据：拉取到 ORDER-1001 的 5 条明细\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>汇总：明细 5 条，总金额 1280.00\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>写 Excel：生成文件 ORDER-1001-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>发邮件：报表已发送到 boss@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[BadReportJob] 每周一自动导出报表：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>查数据：拉取到 WEEK-2026-42 的 5 条明细\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>汇总：明细 5 条，总金额 1280.00\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>写 Excel：生成文件 WEEK-2026-42-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>发邮件：报表已发送到 data-team@example.com\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>：BadController 得 import 五个子系统、知道五个类的职责边界和协作顺序——它本该只关心\"导出报表，返回 URL\"这一件事；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>子系统失去重构自由\u003C\u002Fstrong>：哪天想把 DataFetcher 和 DataAggregator 合并成一个类？所有编排过它们的客户端全部遭殃——子系统想动一动，得先数数全世界有几个客户端。\u003C\u002Fli>\n\u003C\u002Fol>\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\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ReportFacade\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\"> DataFetcher fetcher \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\"> DataFetcher\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\"> DataAggregator aggregator \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\"> DataAggregator\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\"> ExcelWriter excelWriter \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\"> ExcelWriter\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\"> OssUploader ossUploader \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\"> OssUploader\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\"> MailNotifier mailNotifier \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\"> MailNotifier\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 客户端只需要一句话，五个子系统在门面背后鱼贯而出\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">exportAndSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">bizId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">email\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\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> raw \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fetcher.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fetch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bizId);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String summary \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> aggregator.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">aggregate\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String file \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> excelWriter.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">write\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bizId, summary);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String url \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ossUploader.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">upload\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(file);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mailNotifier.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">sendMail\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(url, email);\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\"> url;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意方法体：和反面教材一模一样的五步，一个字没多、一个字没少——\u003Cstrong>门面没有发明任何新逻辑，它只是把这段编排从 N 个客户端收回到 1 个类里\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 正解客户端：Web 接口触发的导出，编排细节一概不知\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\"> WebController\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\"> ReportFacade facade \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\"> ReportFacade\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> exportReport\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">bizId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">email\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\">\"[WebController] 导出 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bizId \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\">        String url \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> facade.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">exportAndSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bizId, email);\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\">\"[WebController] 返回给前端：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> url);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 正解客户端：定时任务触发的导出，同样一句话\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\"> ReportJob\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\"> ReportFacade facade \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\"> ReportFacade\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> runWeekly\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\">\"[ReportJob] 每周一自动导出报表：\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        facade.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">exportAndSend\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"WEEK-2026-42\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data-team@example.com\"\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>[WebController] 导出 ORDER-1001 的报表：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>查数据：拉取到 ORDER-1001 的 5 条明细\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>汇总：明细 5 条，总金额 1280.00\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>写 Excel：生成文件 ORDER-1001-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>传 OSS：ORDER-1001-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>发邮件：报表已发送到 boss@example.com\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[WebController] 返回给前端：https:\u002F\u002Foss.example.com\u002FORDER-1001-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[ReportJob] 每周一自动导出报表：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>查数据：拉取到 WEEK-2026-42 的 5 条明细\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>汇总：明细 5 条，总金额 1280.00\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>写 Excel：生成文件 WEEK-2026-42-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>传 OSS：WEEK-2026-42-报表.xlsx 已上传，URL: https:\u002F\u002Foss.example.com\u002FWEEK-2026-42-报表.xlsx\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>发邮件：报表已发送到 data-team@example.com\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>五步编排从两份（N 份）变回一份，明早加\"数据脱敏\"？改 ReportFacade 一个类，两个客户端睡他们的觉；\u003C\u002Fli>\n \u003Cli>客户端只 import 一个 ReportFacade，五个子系统从它的世界里彻底消失——想认识它都不需要；\u003C\u002Fli>\n \u003Cli>子系统重获自由：合并 DataFetcher 和 DataAggregator、给 OssUploader 换供应商，只要门面方法的签名不动，客户端零感知；\u003C\u002Fli>\n \u003Cli>两个客户端的职责也各自归位了——WebController 回去专心管\"参数校验、返回响应\"，ReportJob 专心管\"调度\"，编排这种脏活谁也不该抢。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-9\">五、你天天在写外观，只是没叫它名字\u003C\u002Fh2>\n\u003Cp>外观模式最容易被小看，因为它看起来\"没技术含量\"。但把视角拉高你会发现：\u003Cstrong>三层架构里的 Service 层，本职工作就是 DAO 层的门面\u003C\u002Fstrong>——Controller 调一个 orderService.createOrder()，背后 DAO 存订单、扣库存、记流水，那不就是一个标准的门面方法？再说细一点，工具类里那些把复杂 API 包一层的静态方法、SDK 里那个什么都管的 XxClient，全是门面。你天天在写外观，只是没叫它名字。\u003C\u002Fp>\n\u003Cp>正因为天天在写，才更该有几个判断标准，防它长歪：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>门面只编排，不干活\u003C\u002Fstrong>：门面方法体里若出现了大段业务逻辑——if 分支、循环计算、字符串拼接——它已经开始从\"门面\"堕落成\"业务类\"。一个狠一点的检验法：把门面类删掉，它的每个方法体能否在子系统里原样重演？能，它是门面；不能，它偷偷藏了私货。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>子系统不知道门面存在\u003C\u002Fstrong>：子系统类 import 门面？依赖方向反了。门面可以认识所有子系统，子系统一个门面都不许认识——这条单向依赖一旦破坏，门面就成了网状依赖的中心节点，第五节的标题就该换成\"中介者模式\"了（那个模式㉒ 再聊，先把账记下：门面是\u003Cstrong>单向\u003C\u002Fstrong>的传话筒，中介者是\u003Cstrong>双向\u003C\u002Fstrong>的交通枢纽）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一个门面只管一类用例\u003C\u002Fstrong>：ReportFacade 管导出，就别把\"用户注册\"也塞进来顺手。门面长成三千行上帝类，几乎都是从\"顺手包一下\"开始的——门面的粒度对齐的是\u003Cstrong>用例\u003C\u002Fstrong>，不是\"整个子系统\"。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>门面不是唯一入口\u003C\u002Fstrong>：客户端偶尔需要绕过门面直接调子系统（比如只要查数据、不要导出全程）？完全可以，外观从不拦路。真想拦，那是上一条里 Tomcat RequestFacade 那种保护性门面的活儿，得明确知道自己为什么要拦。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cp>回顾这一篇：外观模式给一组子系统类提供一个一致的入口——门面持有子系统、按序编排、不添一行新逻辑，子系统对门面毫不知情；反面教材\"流程散落在客户端\"三宗罪——编排知识全局重复、客户端被迫认识全世界、子系统失去重构自由；SLF4J、JdbcTemplate、SqlSession、各路 SDK 的 Client，全是它的地盘；而\"只编排不干活、单向依赖、用例粒度、不绑架客户端\"四条标准，守的是门面和上帝类之间那条细线。\u003C\u002Fp>\n\u003Cp>下一篇 ⑫ 享元模式，结构型阵营只剩它和代理压轴。前面几位省的是\"代码的复杂度\"，它省的是\"货真价实的内存\"——同样的对象造一万份，享元说：留一份共享，其余都指过来。Integer.valueOf 里 -128 到 127 的缓存池、字符串常量池里那个\"字面量只存一份\"的规矩，全是你天天踩在脚下却没低头看过的享元。\u003C\u002Fp>",[120,123,126,130,133,136,139,142,145,148],{"id":121,"text":122,"level":115},"toc-1","一、什么是外观模式？",{"id":124,"text":125,"level":115},"toc-2","二、外观模式的优缺点",{"id":127,"text":128,"level":129},"toc-3","优点",3,{"id":131,"text":132,"level":129},"toc-4","缺点",{"id":134,"text":135,"level":115},"toc-5","三、外观模式的应用场景",{"id":137,"text":138,"level":115},"toc-6","四、外观模式的实现方式",{"id":140,"text":141,"level":129},"toc-7","1. 反面教材：流程散落在客户端",{"id":143,"text":144,"level":129},"toc-8","2. 外观版：一个门面，一句点单",{"id":146,"text":147,"level":115},"toc-9","五、你天天在写外观，只是没叫它名字",{"id":149,"text":150,"level":115},"toc-10","写在最后"]