
上一篇的组合模式,把一群对象组织成了树。这一篇的外观模式(Facade),换个角度处理一群对象——它不组织它们,而是给它们盖一个统一的门面,让外界只需和这个门面打交道,不必直接面对背后那一堆复杂的子系统。外观模式的思想,可能是所有设计模式里最朴素、最符合直觉的一个,你甚至可能早就在不知不觉中用了它。打个比方:你去餐厅吃饭,只需要跟服务员说来一份宫保鸡丁,就能吃到菜。你不需要自己跑进后厨,分别去指挥采购员买菜、洗菜工洗菜、厨师炒菜、传菜员上菜——这一整套复杂的子系统协作,都被服务员这一个门面挡在了后面。服务员就是厨房这套复杂子系统的外观:他给你一个极简的接口(“点菜”),内部替你协调了所有细节。在代码里,外观模式解决的正是这个问题:当完成一件事需要协调一大堆子系统、按特定顺序调用一长串方法时,把这套复杂流程封装进一个外观类,对外只暴露一个简单的方法。我们的下单场景就是绝佳例子——下一个单这件事,背后要协调库存、支付、物流、通知好几个子系统,顺序还不能乱。外观模式让调用方一句facade.placeOrder(...)就搞定,完全不用知道背后的复杂编排。这篇文章按这条线索展开:先看没有门面时,调用方被迫直接面对一堆子系统的窘境;再引出外观模式如何用一个门面类收拢这套复杂性;然后讲清它和迪米特法则的深刻关联;接着看它在框架里的身影;最后辨析它与其他模式的区别,并给出适用边界。贯穿例是下单流程的编排。目录没有门面:调用方被迫指挥一切外观模式:盖一个统一的门面外观与迪米特法则:最少知识原则的实践外观不是封印:它不阻止你直接访问子系统现实身影,与相近模式的辨析什么时候用外观模式一、没有门面:调用方被迫指挥一切先看下单这件事背后有多复杂。它至少要协调这么几个子系统:publicclassInventoryService{// 库存子系统publicbooleandeduct(longskuId,intcount){/* 扣减库存 */returntrue;}}publicclassPaymentService{// 支付子系统publicbooleanpay(longuserId,doubleamount){/* 发起支付 */returntrue;}}publicclassLogisticsService{// 物流子系统publicStringship(StringorderNo,Stringaddress){/* 创建物流单 */returnSF123;}}publicclassNotifyService{// 通知子系统publicvoidsendSms(longuserId,Stringmsg){/* 发短信 */}}如果没有外观,那么下单的完整流程,就得由调用方(比如 Controller)自己一步步指挥:publicclassOrderController{// 调用方被迫认识所有子系统,还得懂它们的调用顺序publicvoidcreateOrder(longuserId,longskuId,intcount,doubleamount,Stringaddress){// 1. 先扣库存if(!inventoryService.deduct(skuId,count)){thrownewRuntimeException(库存不足);}// 2. 再支付if(!paymentService.pay(userId,amount)){inventoryService.rollback(skuId,count);// 支付失败还得记得回滚库存thrownewRuntimeException(支付失败);}// 3. 创建物流单StringlogisticsNologisticsService.ship(NO123,address);// 4. 发通知notifyService.sendSms(userId,您的订单已创建);}}这段代码的问题一目了然:调用方知道得太多:OrderController被迫认识了库存、支付、物流、通知四个子系统,还得懂它们的调用顺序、参数、以及失败后怎么回滚。它和这四个子系统全都紧耦合了。复杂流程到处复制:除了下单,可能还有预订单秒杀下单等地方也要走类似流程,这套编排逻辑就得抄来抄去。子系统一变,调用方全遭殃:哪天支付前要多加一步风控校验,所有写过这套流程的调用方都得改。问题的根源是:下单这套复杂的编排逻辑,本该是一个内聚的整体,却散落、暴露给了每一个调用方。调用方被迫成了总指挥,指挥一堆它本不该关心的细节。我们需要一个服务员,把这套编排收进去。二、外观模式:盖一个统一的门面外观模式的做法极其简单:创建一个外观类,它内部持有所有子系统,把那套复杂的编排流程封装进一个方法,对外只暴露这一个简单入口。// 外观类:下单的服务员publicclassOrderFacade{privatefinalInventoryServiceinventoryService;privatefinalPaymentServicepaymentService;privatefinalLogisticsServicelogisticsService;privatefinalNotifyServicenotifyService;// 构造时把子系统都注入进来(通常由 Spring 管理)publicOrderFacade(InventoryServiceinv,PaymentServicepay,LogisticsServicelogi,NotifyServicenotify){this.inventoryServiceinv;this.paymentServicepay;this.logisticsServicelogi;this.notifyServicenotify;}// 对外只暴露这一个方法,内部编排全部子系统publicStringplaceOrder(longuserId,longskuId,intcount,doubleamount,Stringaddress){if(!inventoryService.deduct(skuId,count)){thrownewRuntimeException(库存不足);}if(!paymentService.pay(userId,amount)){inventoryService.rollback(skuId,count);thrownewRuntimeException(支付失败);}StringlogisticsNologisticsService.ship(NO123,address);notifyService.sendSms(userId,您的订单已创建);returnlogisticsNo;}}现在调用方清爽得不像话——它只认识OrderFacade一个类,一句话下单:publicclassOrderController{privatefinalOrderFacadeorderFacade;// 只依赖一个门面publicvoidcreateOrder(...){orderFacade.placeOrder(userId,skuId,count,amount,address);// 一句话搞定}}对比第一节,升级点非常清晰:调用方从认识 4 个子系统 懂编排顺序 管回滚,退化成只认识 1 个门面 调 1 个方法。那套复杂的编排逻辑被收进了OrderFacade,只写一次,所有需要下单的地方共享。子系统怎么变、顺序怎么调、失败怎么回滚,全是门面内部的事,调用方一概不用管。用一张图看这个门面挡在中间的效果最直观:图里最直观的就是那束从杂乱到清爽的连线:没有门面时,是多对多的蛛网;有了门面,变成调用方多对一连门面、门面一对多连子系统。外观模式的本质,就是用一个中间层,把网状的耦合,梳理成星型的结构。三、外观与迪米特法则:最少知识原则的实践外观模式和第一篇讲的迪米特法则(最少知识原则)是深度绑定的——可以说,外观模式就是迪米特法则最直接的一种落地。回忆迪米特法则那句话:“一个对象应该只和它的’直接朋友’打交道,别去碰朋友的朋友。” 在第一节没有门面的版本里,OrderController为了下单,直接和库存、支付、物流、通知四个子系统打交道——它认识了太多陌生人,知道了太多本不该知道的内部细节。这正是迪米特法则要批评的过度耦合。而外观模式的做法,恰恰是迪米特法则给出的标准解药:引入一个中间的朋友(门面),让调用方只和这一个直接朋友说话,由这个朋友去和背后那一堆子系统打交道。OrderController现在只认识OrderFacade一个直接朋友,它对库存、支付这些子系统的存在一无所知——知识被最小化了。所以你可以这样理解两者的关系:迪米特法则是原则(应该减少对象间的了解),外观模式是手段(用一个门面来实现这种减少)。当你发现某个类为了完成一件事,不得不认识一大堆子对象、调用一长串方法时,那就是迪米特法则在报警,而外观模式往往就是那个把警报消掉的答案。这也再次印证了全系列的主线:模式是原则的具体落地。四、外观不是封印:它不阻止你直接访问子系统有一个关于外观模式的常见误解必须澄清:外观模式提供了一个简化的入口,但它并不禁止你在需要时,仍然直接访问背后的子系统。外观是推荐路径,不是唯一路径。它的定位是:为绝大多数常规需求提供一个傻瓜式的简单接口(80% 的场景用门面一句话搞定);但对于那 20% 需要精细控制、门面没有覆盖的特殊需求,你依然可以绕过门面,直接去调用某个子系统。举个例子:99% 的下单都走orderFacade.placeOrder()。但有个后台运维工具,只想单独测试一下库存扣减、不想触发支付和物流,那它完全可以绕过门面,直接调用inventoryService.deduct()。外观模式不阻拦这种做法。这一点区分了外观和另一种包装的意图:外观的目的是方便(给你一个简单入口),不是封锁(不让你碰里面);如果你的目的是必须、强制所有访问都经过某个中间层做控制(比如权限、限流),那你要的其实是代理或者一个更严格的封装,而不是外观。一句话:外观是一扇方便门,不是一道防盗门。它降低使用的复杂度,但把要不要走捷径的选择权留给你。五、现实身影,与相近模式的辨析外观模式在框架里极其常见,因为框架的核心使命之一就是把复杂的东西变简单:SLF4J 日志门面:名字里直接带门面(Facade)。它给你一个统一简单的日志接口(Logger.info()),背后可以对接 Logback、Log4j2、JUL 等各种复杂的日志实现。你的代码只面向 SLF4J 这个门面,换底层实现时业务代码一行不改——这是外观模式教科书级的应用。Spring 的JdbcTemplate:原生 JDBC 操作要经历获取连接 → 创建 Statement → 执行 → 遍历 ResultSet → 关闭连接 → 处理异常一长串繁琐步骤。JdbcTemplate把这套复杂流程封装成query()、update()几个简单方法,就是给 JDBC 这套复杂子系统盖的门面。类似地,RestTemplate、JmsTemplate也是同样的思路。各种 SDK 的 Client 类:比如一个云服务的XxxClient,把背后复杂的鉴权、签名、网络请求、重试全封装了,你只需调client.doSomething()。辨析一下外观和几个相近模式的区别,避免混淆:外观 vs 代理:代理和真实对象接口相同(它是替身),目的是控制访问;外观定义了一个全新的、更简单的接口(它不是任何子系统的替身),目的是简化。而且外观通常面对多个子系统,代理通常只包一个对象。外观 vs 适配器:适配器是把一个已有的接口转换成另一个已有的、期望的接口(接口对接口);外观是给一堆子系统发明一个全新的简单接口(无中生有一个门面)。适配器是转换,外观是简化。外观 vs 中介者(后面行为型会讲):外观是单向的(调用方 → 门面 → 子系统,子系统不反过来调门面);中介者是双向的(各同事对象通过中介者互相通信)。六、什么时候用外观模式老规矩,泼冷水。外观模式几乎没有什么技术含量,也正因如此,它容易被两个极端误用。适合用的信号:一个操作需要协调多个子系统 / 一长串调用,且这套流程会被多处复用;你想给一个复杂的模块 / 子系统,提供一个简单的、面向外部的统一入口,降低使用门槛;你想给子系统和调用方之间解耦——调用方不该知道子系统的内部结构。分层架构里,常用外观作为每一层对外的门面。要警惕的两个误区:别把外观变成上帝类:如果你把所有业务逻辑都往一个OrderFacade里塞,它会膨胀成一个几千行、什么都管的巨无霸,重新违反了单一职责。门面应该只做编排、转发,不该把子系统的业务逻辑也搬进来。一个系统可以有多个各司其职的门面。别为一个简单的子系统硬造门面:如果背后就一个子系统、一个方法,那直接调用就好,套个门面纯属多此一举——这又是过度设计。外观的价值建立在背后确实复杂这个前提上。判断的核心还是那句话:先确认背后真的存在需要协调多个子系统的复杂性,外观才有意义;而且门面只负责编排、不越界抢子系统的活。小结。外观模式是最朴素的设计模式之一:给一堆复杂的子系统盖一个统一的门面,对外只暴露一个简单入口,把复杂的编排流程收进门面内部。它让调用方从认识一堆子系统 懂全部编排,退化成只认识一个门面 调一个方法,本质是用一个中间层把网状耦合梳理成星型结构——这正是迪米特法则最直接的落地。要记住它是方便门而非防盗门(不禁止你直接访问子系统),也要警惕它膨胀成上帝类。SLF4J、JdbcTemplate都是它的经典身影。下一篇我们讲结构型的最后一个——享元模式:当系统里存在海量重复的相似对象、内存吃紧时,享元教你把这些对象的公共部分提取出来共享,用少量对象扛住大量场景。