适配器模式:把第三方SDK的“怪脾气”拧成统一接口
做过SDK封装的同学都知道,第三方SDK的接口风格千奇百怪。有的用静态方法,有的用链式调用,有的参数顺序还反人类。你想想看,要是每个SDK都按自己的路子来,上层业务代码得疯掉。适配器模式就是解决这个问题的——把不兼容的接口,包一层,变成你想要的统一样子。
我个人习惯把适配器分成两类:类适配器(继承实现)和对象适配器(组合实现)。做Java封装时,我几乎只用对象适配器,因为组合比继承灵活,也不破坏被适配者的功能。下面我会结合一个真实的支付SDK改造案例,把思路说透。
什么时候需要适配器
说白了就三种情况:
- 接口名称对不上:比如第三方叫
execute(),你期望叫invoke() - 参数结构不匹配:它要一个Map,你传的是DTO;或者它要拆开传,你习惯打包传
- 返回类型不统一:有人返回JSON字符串,有人返回自定义对象,你希望统一返回
Result
我在项目中遇到过最典型的案例:对接三家海外支付SDK,A家要传 amount 和 currency 两个参数,B家要求合并成 price 字符串,C家更离谱,直接让你在XML请求体里拼。嗯,这时候不搞适配器,业务代码里全是if-else,维护成本直接爆炸。
对象适配器的标准结构
先看一张结构图,让你对整体有感觉:
这张图里,Client 只认识 Target 接口,Adapter 实现了 Target,并在内部调用 Adaptee。这样,SDK 自己该怎么写还怎么写,外层业务代码也统一了,两边都不受伤。
实战案例:支付SDK统一封装
假设我们有三家支付SDK,各自的支付接口长这样:
| SDK名称 | 原始调用方式 | 参数风格 | 返回值类型 |
|---|---|---|---|
| AlipaySDK | AlipayClient.pay(amount, currency) |
两个单独参数 | PayResponse |
| WechatSDK | WechatPay.payByJson(requestJson) |
JSON字符串 | Map |
| StripeSDK | Stripe.charge().setAmount(amount).execute() |
链式调用 | String (含错误码) |
我们想给业务层统一的接口:
public interface UnifiedPayment {
PaymentResult pay(PaymentRequest request);
}
下面看看怎么用适配器把StripeSDK包装起来——这算是我觉得最折腾的一个,链式调用加String返回,坑不少。
Step 1:定义统一接口和目标DTO
public class PaymentRequest {
private String orderId;
private BigDecimal amount;
private String currency;
private Map<String, String> extra;
// getters & setters
}
public class PaymentResult {
private boolean success;
private String transactionId;
private String errorMessage;
// getters & setters
}
Step 2:实现Stripe适配器
public class StripePaymentAdapter implements UnifiedPayment {
// 持有被适配的Stripe对象(注入或创建)
private final Stripe stripe;
public StripePaymentAdapter(Stripe stripe) {
this.stripe = stripe;
}
@Override
public PaymentResult pay(PaymentRequest request) {
try {
// 1. 参数转换
String amountStr = request.getAmount()
.multiply(BigDecimal.valueOf(100))
.toBigInteger().toString();
// 2. 调用原始链式API
String response = stripe.charge()
.setAmount(amountStr)
.setCurrency(request.getCurrency())
.setDescription(request.getOrderId())
.execute();
// 3. 返回值解析(坑:Stripe返回的String格式不固定)
PaymentResult result = parseStripeResponse(response);
return result;
} catch (Exception e) {
// 4. 统一异常处理
return new PaymentResult(false, null, e.getMessage());
}
}
private PaymentResult parseStripeResponse(String raw) {
// 假设raw格式为 "ok_交易流水号" 或 "err_错误描述"
if (raw.startsWith("ok_")) {
return new PaymentResult(true, raw.substring(3), null);
} else {
return new PaymentResult(false, null, raw.substring(4));
}
}
}
Step 3:适配器注册与使用
我建议用工厂模式或者Spring注入来管理适配器列表:
public class PaymentAdapterRegistry {
private Map<String, UnifiedPayment> adapterMap = new HashMap<>();
public void register(String channel, UnifiedPayment adapter) {
adapterMap.put(channel, adapter);
}
public UnifiedPayment getAdapter(String channel) {
UnifiedPayment adapter = adapterMap.get(channel);
if (adapter == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
return adapter;
}
}
业务层使用时只需一行:
PaymentRequest req = new PaymentRequest(/* ... */);
UnifiedPayment adapter = registry.getAdapter("stripe");
PaymentResult result = adapter.pay(req);
避坑指南:适配器设计的三个“不要”
适配器只负责格式转换和调用编排。金额校验、订单状态检查这些业务,请放在Service层。我曾经看到一个适配器里塞了十几行if-else判断支付限额,结果换SDK时这些逻辑全得重写。
2. 不要对Adaptee做缓存或状态管理
适配器应该是无状态的。如果某个SDK需要保持登录会话,请在外部用连接池或单例处理,别让适配器自己维护。
3. 不要忽略适配器的单元测试
很多同学觉得适配器只是做转换,随便测测就行。其实适配器最容易出边界问题——比如金额精度丢失、空指针、字符串截断。我自己习惯用Mockito模拟Adaptee,单独写一套适配器单元测试。
适配器 vs 外观模式:别搞混
有同学问过:“这不就是外观模式吗?” 嗯,两者确实有点像,但目的不同:
| 对比维度 | 适配器模式 | 外观模式 |
|---|---|---|
| 核心目标 | 接口转换,解决不兼容 | 简化复杂子系统,提供统一入口 |
| 关注点 | 单一接口的一对一适配 | 多接口的一对多聚合 |
| 典型场景 | SDK封装、第三方库集成 | 模块层提供门面,隐藏内部复杂度 |
| 对Adaptee的修改 | 不改Adaptee,只包一层 | 可能封装多个子模块,也不改内部 |
说白了,适配器是“换个说法让它能干”,外观是“你不想操这么多心,我帮你代劳”。做SDK封装时,我通常两个结合着用:外观把多个适配器组织成一个统一入口,适配器处理单个SDK的接口不匹配。
动手练习
给你一个真实的遗留系统改造任务:老项目中有一个 OldSmsSender.send(String phone, String content),新规范要求统一调用 SmsService.send(SmsMessage msg),其中 SmsMessage 包含手机号、内容、签名、优先级等字段。请写一个适配器类 OldSmsAdapter 实现新接口,并在适配器内做参数填充(签名默认“【服务通知】”,优先级默认 NORMAL)。
动手写一遍,适配器模式的“转换”思路会记得更牢。