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。这跟直接调子系统有什么区别?
好的统一接口,应该满足三个条件:
- 参数最少化:能推导的参数,不要暴露给调用方
- 返回值稳定:不管内部怎么变,返回结构不变
- 异常统一化:把子系统的各种异常,转成业务异常
我的习惯:外观方法的参数不超过 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);
业务方只需要知道协议类型和数据,剩下的交给外观。协议增加时,只需要新增一个子系统和工厂分支,外观接口不变。
下面这张图展示了外观模式在接口适配中的整体结构:
你看,外观模式在接口适配中的价值,说白了就是解耦。客户端和子系统之间,隔了一层外观。这一层,就是你的「接口契约」。
我的建议:在设计外观时,先画一张子系统依赖图。如果外观需要调用的子系统超过 5 个,考虑拆分成多个外观,或者用「外观的外观」——也就是元外观。
最后说一个容易被忽略的点:外观的性能开销。外观本身是一个额外的调用层,如果子系统调用非常频繁(比如每秒上万次),外观的编排逻辑可能会成为瓶颈。我遇到过这种情况,解决方案是:在外观内部加缓存,或者把外观的逻辑内联到调用方。
但大多数场景下,外观带来的维护收益远大于那点性能损耗。别为了省几个纳秒,让业务方去面对一堆乱七八糟的子系统。