设计模式 11 · 外观模式
上一篇的组合模式,把一群对象组织成了树。这一篇的外观模式(Facade),换个角度处理"一群对象"——它不组织它们,而是给它们盖一个统一的"门面",让外界只需和这个门面打交道,不必直接面对背后那一堆复杂的子系统。
外观模式的思想,可能是所有设计模式里最朴素、最符合直觉的一个,你甚至可能早就在不知不觉中用了它。打个比方:你去餐厅吃饭,只需要跟服务员说"来一份宫保鸡丁",就能吃到菜。你不需要自己跑进后厨,分别去指挥采购员买菜、洗菜工洗菜、厨师炒菜、传菜员上菜——这一整套复杂的子系统协作,都被"服务员"这一个门面挡在了后面。服务员就是厨房这套复杂子系统的"外观":他给你一个极简的接口(“点菜”),内部替你协调了所有细节。
在代码里,外观模式解决的正是这个问题:当完成一件事需要协调一大堆子系统、按特定顺序调用一长串方法时,把这套复杂流程封装进一个"外观类",对外只暴露一个简单的方法。 我们的下单场景就是绝佳例子——"下一个单"这件事,背后要协调库存、支付、物流、通知好几个子系统,顺序还不能乱。外观模式让调用方一句 facade.placeOrder(...) 就搞定,完全不用知道背后的复杂编排。
这篇文章按这条线索展开:先看"没有门面"时,调用方被迫直接面对一堆子系统的窘境;再引出外观模式如何用一个门面类收拢这套复杂性;然后讲清它和迪米特法则的深刻关联;接着看它在框架里的身影;最后辨析它与其他模式的区别,并给出适用边界。贯穿例是下单流程的编排。
目录
一、没有门面:调用方被迫指挥一切
先看"下单"这件事背后有多复杂。它至少要协调这么几个子系统:
public class InventoryService { // 库存子系统
public boolean deduct(long skuId, int count) { /* 扣减库存 */ return true; }
}
public class PaymentService { // 支付子系统
public boolean pay(long userId, double amount) { /* 发起支付 */ return true; }
}
public class LogisticsService { // 物流子系统
public String ship(String orderNo, String address) { /* 创建物流单 */ return "SF123"; }
}
public class NotifyService { // 通知子系统
public void sendSms(long userId, String msg) { /* 发短信 */ }
}
如果没有外观,那么"下单"的完整流程,就得由调用方(比如 Controller)自己一步步指挥:
public class OrderController {
// 调用方被迫认识所有子系统,还得懂它们的调用顺序
public void createOrder(long userId, long skuId, int count, double amount, String address) {
// 1. 先扣库存
if (!inventoryService.deduct(skuId, count)) {
throw new RuntimeException("库存不足");
}
// 2. 再支付
if (!paymentService.pay(userId, amount)) {
inventoryService.rollback(skuId, count); // 支付失败还得记得回滚库存
throw new RuntimeException("支付失败");
}
// 3. 创建物流单
String logisticsNo = logisticsService.ship("NO123", address);
// 4. 发通知
notifyService.sendSms(userId, "您的订单已创建");
}
}
这段代码的问题一目了然:
- 调用方知道得太多:
OrderController被迫认识了库存、支付、物流、通知四个子系统,还得懂它们的调用顺序、参数、以及失败后怎么回滚。它和这四个子系统全都紧耦合了。 - 复杂流程到处复制:除了下单,可能还有"预订单""秒杀下单"等地方也要走类似流程,这套编排逻辑就得抄来抄去。
- 子系统一变,调用方全遭殃:哪天支付前要多加一步"风控校验",所有写过这套流程的调用方都得改。
问题的根源是:"下单"这套复杂的编排逻辑,本该是一个内聚的整体,却散落、暴露给了每一个调用方。 调用方被迫成了"总指挥",指挥一堆它本不该关心的细节。我们需要一个"服务员",把这套编排收进去。
二、外观模式:盖一个统一的门面
外观模式的做法极其简单:创建一个"外观类",它内部持有所有子系统,把那套复杂的编排流程封装进一个方法,对外只暴露这一个简单入口。
// 外观类:下单的"服务员"
public class OrderFacade {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final LogisticsService logisticsService;
private final NotifyService notifyService;
// 构造时把子系统都注入进来(通常由 Spring 管理)
public OrderFacade(InventoryService inv, PaymentService pay,
LogisticsService logi, NotifyService notify) {
this.inventoryService = inv;
this.paymentService = pay;
this.logisticsService = logi;
this.notifyService = notify;
}
// 对外只暴露这一个方法,内部编排全部子系统
public String placeOrder(long userId, long skuId, int count,
double amount, String address) {
if (!inventoryService.deduct(skuId, count)) {
throw new RuntimeException("库存不足");
}
if (!paymentService.pay(userId, amount)) {
inventoryService.rollback(skuId, count);
throw new RuntimeException("支付失败");
}
String logisticsNo = logisticsService.ship("NO123", address);
notifyService.sendSms(userId, "您的订单已创建");
return logisticsNo;
}
}
现在调用方清爽得不像话——它只认识 OrderFacade 一个类,一句话下单:
public class OrderController {
private final OrderFacade orderFacade; // 只依赖一个门面
public void createOrder(...) {
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 都是它的经典身影。下一篇我们讲结构型的最后一个——享元模式:当系统里存在海量重复的相似对象、内存吃紧时,享元教你把这些对象的"公共部分"提取出来共享,用少量对象扛住大量场景。
更多推荐
所有评论(0)