3. 外观模式:把复杂接口藏到背后

外观模式,说白了就是给一堆乱七八糟的子系统套个「门面」。你想想看,一个复杂的接口适配流程里,可能有密钥协商、签名验证、数据加密、会话管理……每个环节都暴露出来,调用方得疯掉。

我早年做支付网关的时候,对接过一家银行的接口。对方给了十几个 API,每个都有不同的签名算法、不同的超时策略。我当时就想——能不能只暴露一个方法给业务方?这就是外观模式的直觉来源。

3.1 外观模式的核心原理

外观模式(Facade Pattern)属于结构型设计模式。它的核心就一句话:为子系统的一组接口提供一个统一的、更高级的接口。

它不封装子系统,也不限制你直接访问子系统。它只是在你和复杂系统之间,加了一层「简化层」。

关键点:外观模式不是「封装」,而是「编排」。它把多个子系统的调用顺序、参数转换、异常处理集中到一个地方。

我习惯用三个角色来理解它:

  • 外观(Facade):对外暴露的简化接口,内部协调子系统
  • 子系统(Subsystem):真正干活的模块,彼此可能耦合
  • 客户端(Client):只跟外观打交道,不关心内部细节

举个例子,一个密钥协商流程:

// 子系统:密钥生成
class KeyGenerator {
    public byte[] generateKey() { /* ... */ }
}

// 子系统:证书验证
class CertValidator {
    public boolean validate(byte[] cert) { /* ... */ }
}

// 子系统:会话建立
class SessionManager {
    public String createSession(byte[] key) { /* ... */ }
}

// 外观:统一入口
class KeyExchangeFacade {
    private KeyGenerator kg = new KeyGenerator();
    private CertValidator cv = new CertValidator();
    private SessionManager sm = new SessionManager();

    public String doKeyExchange(byte[] cert) {
        if (!cv.validate(cert)) {
            throw new SecurityException("证书无效");
        }
        byte[] key = kg.generateKey();
        return sm.createSession(key);
    }
}

// 客户端只用一行
new KeyExchangeFacade().doKeyExchange(certData);

你看,客户端根本不需要知道证书怎么验、密钥怎么生成、会话怎么建。它只需要一个方法调用。

3.2 统一接口设计:别让调用方猜

统一接口设计,是外观模式落地的关键。我见过太多「伪外观」——外观方法里塞了十几个参数,调用方还得传一堆 null。这跟直接调子系统有什么区别?

好的统一接口,应该满足三个条件:

  1. 参数最少化:能推导的参数,不要暴露给调用方
  2. 返回值稳定:不管内部怎么变,返回结构不变
  3. 异常统一化:把子系统的各种异常,转成业务异常

我的习惯:外观方法的参数不超过 3 个。如果超过,说明外观粒度不对,或者应该用参数对象。

举个例子,一个接口适配的外观:

// 不好的设计:参数太多
public String doAdapter(String url, String appId, String secret, 
                        String certPath, int timeout, boolean retry) {
    // ...
}

// 好的设计:用配置对象
public class AdapterConfig {
    private String url;
    private String appId;
    private String secret;
    // getter/setter
}

public String doAdapter(AdapterConfig config) {
    // 内部读取配置,调用子系统
}

嗯,这里要注意:外观不是万能胶。如果调用方需要频繁调整子系统的行为,那说明外观太死板了。这时候可以考虑加一个策略参数,或者干脆让调用方绕过外观。

3.3 子系统封装:该藏的藏,该露的露

子系统封装,不是把所有东西都藏起来。我见过有人把外观做成「上帝类」,几百行代码,什么逻辑都往里塞。这反而破坏了可维护性。

我的原则是:

  • 隐藏实现细节:算法选择、缓存策略、重试机制,这些调用方不需要知道
  • 暴露配置点:比如超时时间、密钥长度,这些调用方可能需要调整
  • 保留逃生口:如果调用方确实需要直接操作子系统,不要禁止,但要在文档里说明

我曾经踩过的坑:在一个密钥管理系统中,我把所有子系统的构造方法都设成了包级私有,结果测试团队没法 mock 子系统。后来我加了一个「测试外观」,专门暴露内部接口给测试用。教训是:封装要考虑可测试性。

子系统封装还有一个常见问题——循环依赖。外观调用了子系统 A,子系统 A 又调用了子系统 B,子系统 B 又回调了外观。这种设计一旦出现,维护成本直线上升。

我建议用依赖倒置来解决:外观依赖子系统的接口,而不是具体实现。子系统之间也通过接口交互,不要直接依赖外观。

3.4 外观模式在复杂接口简化中的应用

讲个真实案例。我之前做一个跨平台的接口适配框架,需要对接三种不同的密钥协议:RSA、ECDH、SM2。每种协议的参数、流程、错误码都不一样。

如果不做外观,业务方得写这样的代码:

if (protocol.equals("RSA")) {
    // 20 行 RSA 逻辑
} else if (protocol.equals("ECDH")) {
    // 25 行 ECDH 逻辑
} else if (protocol.equals("SM2")) {
    // 30 行 SM2 逻辑
}

这代码,你想想看,每个业务方都得写一遍。而且一旦协议升级,所有调用方都得改。

用了外观模式之后:

// 外观
class KeyProtocolFacade {
    public KeyExchangeResult exchange(ProtocolType type, byte[] data) {
        KeyProtocol protocol = ProtocolFactory.create(type);
        return protocol.execute(data);
    }
}

// 客户端
KeyExchangeResult result = facade.exchange(ProtocolType.SM2, data);

业务方只需要知道协议类型和数据,剩下的交给外观。协议增加时,只需要新增一个子系统和工厂分支,外观接口不变。

下面这张图展示了外观模式在接口适配中的整体结构:

外观模式在接口适配中的结构 客户端 KeyProtocolFacade(外观) exchange(ProtocolType, byte[]) RSA 子系统 密钥生成/签名/验签 ECDH 子系统 密钥协商/派生 SM2 子系统 国密密钥/证书 外观模式的关键收益 • 客户端只依赖外观,不依赖具体协议子系统 • 新增协议时,外观接口不变,只需新增子系统和工厂分支 • 子系统的异常、参数变化被隔离在外观内部

你看,外观模式在接口适配中的价值,说白了就是解耦。客户端和子系统之间,隔了一层外观。这一层,就是你的「接口契约」。

我的建议:在设计外观时,先画一张子系统依赖图。如果外观需要调用的子系统超过 5 个,考虑拆分成多个外观,或者用「外观的外观」——也就是元外观。

最后说一个容易被忽略的点:外观的性能开销。外观本身是一个额外的调用层,如果子系统调用非常频繁(比如每秒上万次),外观的编排逻辑可能会成为瓶颈。我遇到过这种情况,解决方案是:在外观内部加缓存,或者把外观的逻辑内联到调用方。

但大多数场景下,外观带来的维护收益远大于那点性能损耗。别为了省几个纳秒,让业务方去面对一堆乱七八糟的子系统。