少女祈祷中...
残云的学习笔记

外观模式:把五个子系统收成一个入口,以及它和适配器、中介者到底差在哪

当一个业务流程要穿过一堆子系统、调用顺序还有讲究时,别让每个调用方自己拼这条链路,把它收进一个入口。外观是结构型里最不起眼、却几乎每个项目都在用的模式——它不解决「类怎么组织」,它解决「调用方怎么写」。本文讲清三个角色、外观的三条边界(不禁止直连、不增加功能、可以有很多个),给出可运行的单文件 Java 示例,拆开 SLF4J、Tomcat 的 RequestFacade、Spring 的 JdbcTemplate 这些真实外观,最后把结构型五个模式的界线用一张总表理清。

一、问题的起点:一个业务流程要穿过五个子系统

先看一个再普通不过的用例:下单。

它看起来是一件事,实际要按顺序穿过一堆子系统:

  1. 锁定库存;
  2. 算优惠券抵扣;
  3. 扣款;
  4. 发通知。

而且每一步都可能失败,失败要补偿:支付失败得把库存放回去、把券退回去。没有外观的时候,这段逻辑得由每个调用方自己拼:

/** 下单:没有外观时,每个调用点都要自己把这条链路拼一遍 */
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-1-two-ways

二、结构:一个门面 + 一组子系统

外观的结构是结构型里最简单的,只有三个角色(其中两个还常常被忽略):

facade-2-structure

  • Facade(外观):OrderFacade。只暴露「这个用例需要的那几个方法」,内部负责编排——按什么顺序调、失败怎么补偿。
  • Subsystem(子系统):InventoryService、CouponService、PayService、NotifyService。它们完全不知道外观存在,各自只干自己的事,也能被别处直接使用。
  • Client(客户端):只认识 OrderFacade.placeOrder(...) 一个方法。

两个容易漏掉的点:

  1. 外观不等于「唯一的入口」。图里那条虚线 Client ..> InventoryService 是刻意画的——外观不禁止你直接调子系统。它提供的是一条便捷通道,不是一堵墙。
  2. 外观不做业务判断之外的事。它负责「编排」,不负责「实现」:算优惠券折扣仍然是 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() 在内部是这样编排的(注意失败分支里的补偿):

facade-3-sequence

三点值得注意:

  1. 外观的返回值是「业务结果」。如果 placeOrder 把 boolean、long、String 一堆子系统的返回值原样传出来,那只是把复杂度搬了个地方,调用方还得自己组合。
  2. 子系统之间不互相认识。PayService 不知道库存的存在,补偿是外观编排的——这条边界守住了,子系统才能被别处复用。
  3. 调用方一行搞定,而顺序、补偿、失败语义都只有一处定义。

四、外观的三条边界

外观是被滥用得最厉害的结构型模式,因为它「加一个类就能少写几行」的诱惑太大。三条边界能挡住大部分滥用:

  1. 不禁止直连:外观提供便利通道,不是守卫。确实需要细粒度控制时,调用方可以绕过外观直接调子系统——不要为了「统一入口」把子系统的方法设成私有。
  2. 不增加功能:外观只做编排与转译(把多个调用串起来、把多个返回值组合成一个业务结果)。如果开始在外观里写「优惠券怎么算」「库存怎么扣」这种实现逻辑,它就在变成上帝类了。
  3. 可以有很多个:一个外观干所有事,是另一种上帝类。按用例拆——下单一个、退款一个、查询一个,各自只认识自己用到的那几个子系统。

第 3 条最容易被忽略。下面这样是不推荐的:

// ✗ 反例:一个「万能外观」包住所有子系统的所有方法
class SystemFacade {
    void placeOrder()  { /* ... */ }
    void refund()      { /* ... */ }
    void addCoupon()   { /* ... */ }
    void queryStock()  { /* ... */ }
    void exportStock() { /* ... */ }
    void sendSms()     { /* ... */ }
    // …… 二十个方法,谁都往里加
}

五、外观 vs 适配器 vs 中介者

这三个(加上代理、装饰者、桥接)是结构型里最容易被混着用的。外观和适配器的区别是最常考、也最容易说错的:

外观 Facade适配器 Adapter中介者 Mediator
目的简化:多个变一个转换:让对不上的接口能协作解耦:避免对象间互相引用
数量关系多对一(多个子系统 → 一个入口)通常一对一(两个接口之间)多对多(都要通过中枢)
方向单向:调用方 → 子系统单向:调用方 → 被适配者双向:同事之间互相通信
子系统是否知道不知道外观存在不知道适配器存在知道中介者(要向它注册)
接口复杂度变简单变成对方认识的(不一定简单)不变,只是不再直连

一句话记忆:

  • 外观:接口变简单,包住一堆子系统,调用方只看一个门;
  • 适配器:接口要转换,让原本对不上的双方能协作;
  • 中介者:接口不变,但把「谁调谁」改成「都经过我」。

facade-4-vs-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 个以内,新人一眼能读完。

facade-5-multi-facade

八、什么时候不要用外观

  1. 子系统只有一个类:那叫「多包了一层」,直接调就行。
  2. 调用方本来就需要细粒度控制:比如后台管理要单独改库存、单独退券,硬套外观反而要开一堆穿透方法。
  3. 想用外观掩盖糟糕的子系统设计:外观能让调用方看不见乱,但乱还在,而且会多出一层要维护。先修子系统,再谈外观。
  4. 把外观当业务层:外观是「编排」,不是「领域服务」。真正的业务规则应该落在领域对象/领域服务里,外观只负责把它们串起来。

九、结构型五兄弟:一张表理清

模式一句话意图接口数量关系典型场景
代理 Proxy控制对单个对象的访问保持不变一对一权限校验、远程调用、延迟加载
适配器 Adapter让接口对不上的双方能协作转成对方认识的通常一对一接第三方 SDK、老系统兼容
装饰者 Decorator不改源码叠加职责保持不变一层层套IO 流、加缓存/日志/加密
桥接 Bridge分离两个独立变化的维度两边接口不同多对多(两棵树)JDBC、跨平台 GUI
外观 Facade简化一组子系统的调用变简单多对一下单编排、日志门面、SDK 客户端

facade-6-five-patterns

十、参考资料

  • GoF《设计模式:可复用面向对象软件的基础》——外观模式章节
  • 《Head First 设计模式》第 7 章——外观模式与「最少知识原则」
  • SLF4J 官方文档(门面 + 绑定)、Tomcat 源码 org.apache.catalina.connector.RequestFacade
  • Spring Framework 文档:JdbcTemplate、ApplicationContext
  • 软考《软件设计师教程》——结构型设计模式章节
  • 本系列相关篇目:桥接模式(拆维度)、适配器模式(改接口)、装饰者模式(叠职责)、代理模式(控访问)
分享到

评论