第三章:适配器模式详解——GoF适配器模式在接口适配中的实际应用
说到接口适配,我第一个想到的就是适配器模式。
这玩意儿在真实项目中太常见了。你想想看,我们做系统集成时,经常遇到“接口对不上”的尴尬。A系统输出的是XML,B系统只认JSON;老系统返回的是List,新框架要的是Map……这时候怎么办?
硬改?不现实。重写?成本太高。
适配器模式就是专门解决这个问题的。它不改变原有接口,而是在中间加一层“转换层”。说白了,就是给不兼容的接口“搭个桥”。
3.1 适配器模式的核心思想
GoF(Gang of Four,四人帮)把适配器模式定义为:将一个类的接口转换成客户期望的另一个接口。
我个人习惯把它理解成“电源转换头”。你去国外出差,插座是两孔的,你的充电器是三孔的。你不会把充电器拆了重焊,而是买个转换头插上。适配器就是这个转换头。
核心角色:
- 目标接口(Target):客户期望的接口
- 被适配者(Adaptee):现有的、需要被适配的接口
- 适配器(Adapter):中间转换层,把Adaptee转换成Target
GoF给出了两种实现方式:类适配器和对象适配器。这两种方式各有千秋,我在项目中都踩过坑,下面详细说说。
3.2 类适配器:基于继承的实现
类适配器使用多重继承(或单继承+接口实现)来适配。适配器同时继承目标接口和被适配者。
先看一个场景:
我们有个老系统,提供的是List<String>格式的数据。新系统要求的是Map<String, String>格式。我需要写个适配器,把List转成Map。
// 被适配者:老系统的接口
public class OldDataProvider {
public List<String> getData() {
return Arrays.asList("key1:value1", "key2:value2", "key3:value3");
}
}
// 目标接口:新系统期望的接口
public interface NewDataProvider {
Map<String, String> getDataAsMap();
}
// 类适配器:继承被适配者,实现目标接口
public class ClassAdapter extends OldDataProvider implements NewDataProvider {
@Override
public Map<String, String> getDataAsMap() {
List<String> oldData = getData(); // 调用父类方法
Map<String, String> result = new HashMap<>();
for (String item : oldData) {
String[] parts = item.split(":");
result.put(parts[0], parts[1]);
}
return result;
}
}
嗯,这里要注意:类适配器代码很简洁,但有个硬伤——它要求被适配者不能是final类。我在项目中遇到过这种情况:老系统的核心类被标记为final,类适配器直接废了。
避坑指南:
我曾经在一个金融项目中用类适配器适配第三方SDK。结果SDK升级后,核心类加了final修饰符,我的适配器直接编译不过。那次教训让我明白:类适配器对继承关系有强依赖,一旦父类变化,适配器就得重写。
3.3 对象适配器:基于组合的实现
对象适配器使用组合(持有被适配者的引用)来实现适配。适配器实现目标接口,内部调用被适配者的方法。
同样的场景,用对象适配器实现:
// 对象适配器:持有被适配者的引用
public class ObjectAdapter implements NewDataProvider {
private OldDataProvider oldProvider; // 组合关系
public ObjectAdapter(OldDataProvider oldProvider) {
this.oldProvider = oldProvider;
}
@Override
public Map<String, String> getDataAsMap() {
List<String> oldData = oldProvider.getData(); // 调用被适配者
Map<String, String> result = new HashMap<>();
for (String item : oldData) {
String[] parts = item.split(":");
result.put(parts[0], parts[1]);
}
return result;
}
}
// 使用方式
OldDataProvider oldProvider = new OldDataProvider();
NewDataProvider adapter = new ObjectAdapter(oldProvider);
Map<String, String> data = adapter.getDataAsMap();
对象适配器更灵活。它不依赖继承,所以被适配者可以是任何类。而且一个适配器可以适配多个被适配者——只要它们实现了相同的接口。
我的建议:
除非你确定被适配者永远不会变(比如是你们自己团队维护的基础类),否则优先使用对象适配器。我在实际项目中,90%的情况都用对象适配器。类适配器只用在性能敏感、且继承关系稳定的场景。
3.4 类适配器 vs 对象适配器:对比表格
| 对比维度 | 类适配器 | 对象适配器 |
|---|---|---|
| 实现方式 | 继承(extends) | 组合(持有引用) |
| 灵活性 | 低,被适配者不能是final类 | 高,任何类都可以 |
| 代码量 | 较少 | 稍多(需要构造函数注入) |
| 性能 | 略高(无间接调用) | 略低(多一层对象引用) |
| 适用场景 | 被适配者稳定、性能敏感 | 大多数通用场景 |
3.5 适配器模式在接口适配中的实际应用
我参与过的一个电商项目,需要对接三个不同的支付网关:支付宝、支付、银联。每个网关的接口签名方式、参数格式、回调机制都不一样。
如果业务代码直接调用各个网关的API,那代码里会充满if-else判断。而且换网关时,业务代码也得改。
我们用适配器模式解决了这个问题:
- 定义统一的目标接口:
PaymentAdapter,包含pay()、refund()、query()方法 - 为每个网关写一个适配器:
AlipayAdapter、WechatAdapter、UnionPayAdapter - 业务代码只依赖目标接口:通过工厂或DI注入具体适配器
// 统一的目标接口
public interface PaymentAdapter {
boolean pay(String orderId, double amount);
boolean refund(String orderId, double amount);
String query(String orderId);
}
// 支付宝适配器
public class AlipayAdapter implements PaymentAdapter {
private AlipaySDK alipaySDK; // 支付宝原生SDK
public AlipayAdapter(AlipaySDK alipaySDK) {
this.alipaySDK = alipaySDK;
}
@Override
public boolean pay(String orderId, double amount) {
// 将统一参数转换成支付宝SDK需要的格式
AlipayRequest request = new AlipayRequest();
request.setOutTradeNo(orderId);
request.setTotalAmount(String.valueOf(amount));
AlipayResponse response = alipaySDK.tradePay(request);
return response.isSuccess();
}
// 其他方法类似...
}
这样做的好处很明显:
- 新增支付网关时,只需写一个新的适配器类,业务代码不用动
- 某个网关的SDK升级了,只改对应的适配器
- 测试时可以轻松mock适配器,不用真的调用支付网关
核心要点:
适配器模式不是让你“改接口”,而是让你“包接口”。不改动现有代码,通过中间层实现兼容。这是软件设计中“开闭原则”的典型应用——对扩展开放,对修改关闭。
3.6 适配器模式的知识体系图
下面我用一张SVG图来展示适配器模式的核心逻辑和两种实现方式的关系:
3.7 什么时候该用适配器模式?
我总结了几条经验,供你参考:
- 系统集成时:两个系统接口不匹配,又不想改任何一方的代码
- 第三方SDK替换:比如从阿里云OSS换成腾讯云COS,适配器可以屏蔽差异
- 老系统改造:新框架要求新接口,老系统不能动,加一层适配器过渡
- 统一多个相似接口:比如上面说的支付网关场景
注意:
适配器模式不是万能的。如果接口差异太大(比如一个同步一个异步,一个阻塞一个非阻塞),适配器会变得很复杂。这时候可能需要考虑更重的方案,比如消息队列或事件驱动架构。
好了,适配器模式就讲到这里。记住一句话:适配器是“粘合剂”,不是“万能胶”。用对了地方,它能帮你省下大量重构时间;用错了,反而增加系统复杂度。