当一个业务流程要穿过一堆子系统、调用顺序还有讲究时,别让每个调用方自己拼这条链路,把它收进一个入口。外观是结构型里最不起眼、却几乎每个项目都在用的模式——它不解决「类怎么组织」,它解决「调用方怎么写」。本文讲清三个角色、外观的三条边界(不禁止直连、不增加功能、可以有很多个),给出可运行的单文件 Java 示例,拆开 SLF4J、Tomcat 的
RequestFacade、Spring 的JdbcTemplate这些真实外观,最后把结构型五个模式的界线用一张总表理清。
一、问题的起点:一个业务流程要穿过五个子系统
先看一个再普通不过的用例:下单。
它看起来是一件事,实际要按顺序穿过一堆子系统:
- 锁定库存;
- 算优惠券抵扣;
- 扣款;
- 发通知。
而且每一步都可能失败,失败要补偿:支付失败得把库存放回去、把券退回去。没有外观的时候,这段逻辑得由每个调用方自己拼:
/** 下单:没有外观时,每个调用点都要自己把这条链路拼一遍 */
OrderResult placeOrder(String userId, String sku, int qty, long amountFen) {
InventoryService inventory = new InventoryService();
CouponService coupon = new CouponService();
PayService pay = new PayService();
NotifyService notify = new NotifyService();
if (!inventory.lock(sku, qty)) {
throw new IllegalStateException("库存不足");
}
long discount = coupon.calcDiscount(userId, amountFen);
long payable = amountFen - discount;
if (!pay.pay(userId, payable)) {
inventory.release(sku, qty); // 补偿逻辑在每个调用点重复一遍
coupon.refund(userId, discount);
throw new IllegalStateException("支付失败");
}
notify.send(userId, "下单成功");
return OrderResult.ok(payable);
}
问题不在「代码长」,而在这三件事:
- 同一条链路会在多个地方重复(下单入口、后台代下单、活动报名下单……);
- 顺序和补偿规则散落在各处,改一处得找齐所有调用点;
- 调用方被迫认识一堆它本不该关心的类(优惠券怎么算、通知走哪个通道)。

二、结构:一个门面 + 一组子系统
外观的结构是结构型里最简单的,只有三个角色(其中两个还常常被忽略):

- Facade(外观):
OrderFacade。只暴露「这个用例需要的那几个方法」,内部负责编排——按什么顺序调、失败怎么补偿。 - Subsystem(子系统):
InventoryService、CouponService、PayService、NotifyService。它们完全不知道外观存在,各自只干自己的事,也能被别处直接使用。 - Client(客户端):只认识
OrderFacade.placeOrder(...)一个方法。
两个容易漏掉的点:
- 外观不等于「唯一的入口」。图里那条虚线
Client ..> InventoryService是刻意画的——外观不禁止你直接调子系统。它提供的是一条便捷通道,不是一堵墙。 - 外观不做业务判断之外的事。它负责「编排」,不负责「实现」:算优惠券折扣仍然是
CouponService的事,外观只是决定什么时候调它。
三、手写一个订单门面
下面是可直接运行的单文件版本(Java 8+):
/** 子系统 1:库存 */
class InventoryService {
boolean lock(String sku, int qty) {
System.out.println(" [库存] 锁定 " + sku + " × " + qty);
return true; // 真实场景这里会查库
}
void release(String sku, int qty) {
System.out.println(" [库存] 释放 " + sku + " × " + qty);
}
}
/** 子系统 2:优惠券 */
class CouponService {
long calcDiscount(String userId, long amountFen) {
long discount = amountFen >= 10000 ? 800 : 0; // 满 100 元减 8 元(单位:分)
System.out.println(" [优惠券] 抵扣 " + discount + " 分");
return discount;
}
void refund(String userId, long discount) {
System.out.println(" [优惠券] 退回抵扣 " + discount + " 分");
}
}
/** 子系统 3:支付 */
class PayService {
boolean pay(String userId, long amountFen) {
System.out.println(" [支付] 扣款 " + amountFen + " 分");
return true;
}
void refund(String userId, long amountFen) {
System.out.println(" [支付] 退款 " + amountFen + " 分");
}
}
/** 子系统 4:通知 */
class NotifyService {
void send(String userId, String msg) {
System.out.println(" [通知] " + userId + " ← " + msg);
}
}
/** 结果对象:外观的返回值应该是一个「业务结果」,而不是一堆子系统的返回值 */
class OrderResult {
final boolean success;
final long payable;
final String reason;
private OrderResult(boolean success, long payable, String reason) {
this.success = success;
this.payable = payable;
this.reason = reason;
}
static OrderResult ok(long payable) {
return new OrderResult(true, payable, "OK");
}
static OrderResult fail(String reason) {
return new OrderResult(false, 0L, reason);
}
@Override
public String toString() {
return success ? ("成功,实付 " + payable + " 分") : ("失败:" + reason);
}
}
/** 外观:把「下单」这条链路收成一个方法 */
class OrderFacade {
private final InventoryService inventory = new InventoryService();
private final CouponService coupon = new CouponService();
private final PayService pay = new PayService();
private final NotifyService notify = new NotifyService();
OrderResult placeOrder(String userId, String sku, int qty, long amountFen) {
if (!inventory.lock(sku, qty)) {
return OrderResult.fail("库存不足");
}
long discount = coupon.calcDiscount(userId, amountFen);
long payable = amountFen - discount;
if (!pay.pay(userId, payable)) {
inventory.release(sku, qty); // 补偿逻辑只写在这里一处
coupon.refund(userId, discount);
return OrderResult.fail("支付失败");
}
notify.send(userId, "下单成功,实付 " + payable + " 分");
return OrderResult.ok(payable);
}
}
public class FacadeDemo {
public static void main(String[] args) {
OrderFacade facade = new OrderFacade();
// 调用方:一行完成下单,内部 4 个子系统 + 补偿逻辑全都藏起来了
System.out.println("下单:" + facade.placeOrder("u-1001", "SKU-9", 2, 12000L));
}
}
输出:
[库存] 锁定 SKU-9 × 2
[优惠券] 抵扣 800 分
[支付] 扣款 11200 分
[通知] u-1001 ← 下单成功,实付 11200 分
下单:成功,实付 11200 分
一次 placeOrder() 在内部是这样编排的(注意失败分支里的补偿):

三点值得注意:
- 外观的返回值是「业务结果」。如果
placeOrder把boolean、long、String一堆子系统的返回值原样传出来,那只是把复杂度搬了个地方,调用方还得自己组合。 - 子系统之间不互相认识。
PayService不知道库存的存在,补偿是外观编排的——这条边界守住了,子系统才能被别处复用。 - 调用方一行搞定,而顺序、补偿、失败语义都只有一处定义。
四、外观的三条边界
外观是被滥用得最厉害的结构型模式,因为它「加一个类就能少写几行」的诱惑太大。三条边界能挡住大部分滥用:
- 不禁止直连:外观提供便利通道,不是守卫。确实需要细粒度控制时,调用方可以绕过外观直接调子系统——不要为了「统一入口」把子系统的方法设成私有。
- 不增加功能:外观只做编排与转译(把多个调用串起来、把多个返回值组合成一个业务结果)。如果开始在外观里写「优惠券怎么算」「库存怎么扣」这种实现逻辑,它就在变成上帝类了。
- 可以有很多个:一个外观干所有事,是另一种上帝类。按用例拆——下单一个、退款一个、查询一个,各自只认识自己用到的那几个子系统。
第 3 条最容易被忽略。下面这样是不推荐的:
// ✗ 反例:一个「万能外观」包住所有子系统的所有方法
class SystemFacade {
void placeOrder() { /* ... */ }
void refund() { /* ... */ }
void addCoupon() { /* ... */ }
void queryStock() { /* ... */ }
void exportStock() { /* ... */ }
void sendSms() { /* ... */ }
// …… 二十个方法,谁都往里加
}
五、外观 vs 适配器 vs 中介者
这三个(加上代理、装饰者、桥接)是结构型里最容易被混着用的。外观和适配器的区别是最常考、也最容易说错的:
| 外观 Facade | 适配器 Adapter | 中介者 Mediator | |
|---|---|---|---|
| 目的 | 简化:多个变一个 | 转换:让对不上的接口能协作 | 解耦:避免对象间互相引用 |
| 数量关系 | 多对一(多个子系统 → 一个入口) | 通常一对一(两个接口之间) | 多对多(都要通过中枢) |
| 方向 | 单向:调用方 → 子系统 | 单向:调用方 → 被适配者 | 双向:同事之间互相通信 |
| 子系统是否知道 | 不知道外观存在 | 不知道适配器存在 | 知道中介者(要向它注册) |
| 接口复杂度 | 变简单 | 变成对方认识的(不一定简单) | 不变,只是不再直连 |
一句话记忆:
- 外观:接口变简单,包住一堆子系统,调用方只看一个门;
- 适配器:接口要转换,让原本对不上的双方能协作;
- 中介者:接口不变,但把「谁调谁」改成「都经过我」。

六、JDK 与框架里的外观
① SLF4J 是标准的外观。 业务代码只依赖 slf4j-api 这一个门面,底层用 logback 还是 log4j2 完全由构建时决定:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PayService {
// 只认识门面:LoggerFactory,不认识任何具体日志实现
private static final Logger log = LoggerFactory.getLogger(PayService.class);
public void pay(String userId, long amountFen) {
log.info("扣款成功 userId={} amount={}", userId, amountFen);
}
}
换日志实现:换依赖 + 换配置,业务代码一行不改。
② Tomcat 的 RequestFacade / ResponseFacade 是外观。Tomcat 内部真正的 Request 对象有很多容器自己的方法(recycle()、setRequest() 等),如果直接交给应用的 Servlet,应用就可能把它们用坏。于是它给应用一个门面——只暴露 Servlet 规范里的方法,内部方法全部隐藏。这是「外观 = 简化 + 收窄」的典型:它保护的不是调用方,而是子系统。
③ Spring 的 JdbcTemplate 与 ApplicationContext。
JdbcTemplate把 JDBC 那一整套「拿连接 → 创建 Statement → 绑参数 → 遍历 ResultSet → 逐层关闭 + 异常翻译」收成queryForList(sql)这样的入口,调用方不用再写 try-finally;ApplicationContext把 Bean 工厂、资源加载、事件发布、国际化这些子系统收成一个getBean()背后的门面。
判断标准都一样:调用方是不是只面对一个简化入口,而子系统仍然可以被别人独立使用。
七、多重外观:按用例拆,而不是按子系统拆
正确做法是按用例拆门面——同一个子系统会被多个外观复用:
/** 下单门面:只关心「下单」这条链路(完整实现见第三节) */
class OrderFacade {
private final InventoryService inventory = new InventoryService();
private final CouponService coupon = new CouponService();
private final PayService pay = new PayService();
private final NotifyService notify = new NotifyService();
OrderResult placeOrder(String userId, String sku, int qty, long amountFen) {
// 完整实现见第三节
throw new UnsupportedOperationException("完整实现见第三节");
}
}
/** 退款门面:只关心「退款」这条链路,复用同样的子系统 */
class RefundFacade {
private final InventoryService inventory = new InventoryService();
private final PayService pay = new PayService();
private final NotifyService notify = new NotifyService();
void refund(String userId, String orderNo, String sku, int qty, long amountFen) {
pay.refund(userId, amountFen);
inventory.release(sku, qty);
notify.send(userId, "订单 " + orderNo + " 已退款");
}
}
拆分之后:InventoryService 同时被两个门面使用(子系统不需要为门面做任何改动);下单规则的改动只影响 OrderFacade;每个门面的方法数都在 3~5 个以内,新人一眼能读完。

八、什么时候不要用外观
- 子系统只有一个类:那叫「多包了一层」,直接调就行。
- 调用方本来就需要细粒度控制:比如后台管理要单独改库存、单独退券,硬套外观反而要开一堆穿透方法。
- 想用外观掩盖糟糕的子系统设计:外观能让调用方看不见乱,但乱还在,而且会多出一层要维护。先修子系统,再谈外观。
- 把外观当业务层:外观是「编排」,不是「领域服务」。真正的业务规则应该落在领域对象/领域服务里,外观只负责把它们串起来。
九、结构型五兄弟:一张表理清
| 模式 | 一句话意图 | 接口 | 数量关系 | 典型场景 |
|---|---|---|---|---|
| 代理 Proxy | 控制对单个对象的访问 | 保持不变 | 一对一 | 权限校验、远程调用、延迟加载 |
| 适配器 Adapter | 让接口对不上的双方能协作 | 转成对方认识的 | 通常一对一 | 接第三方 SDK、老系统兼容 |
| 装饰者 Decorator | 不改源码叠加职责 | 保持不变 | 一层层套 | IO 流、加缓存/日志/加密 |
| 桥接 Bridge | 分离两个独立变化的维度 | 两边接口不同 | 多对多(两棵树) | JDBC、跨平台 GUI |
| 外观 Facade | 简化一组子系统的调用 | 变简单 | 多对一 | 下单编排、日志门面、SDK 客户端 |

评论