适配器模式:把第三方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 (适配器) Adaptee (SDK) 关键点: • Client 只依赖 Target 接口,不知道底层是哪个适配器 • Adapter 内部持有 Adaptee 的引用,做参数转换和逻辑编排 • 适配器不修改 Adaptee,符合开闭原则

这张图里,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));
        }
    }
}
我曾经踩过的坑:Stripe的链式API在部分版本里会抛出未检查异常,而很多人忘记在适配器里catch,导致上层直接500。所以适配器内部一定要做异常兜底,返回带错误码的业务对象,别把异常透传出去。

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);

避坑指南:适配器设计的三个“不要”

1. 不要在适配器里做业务逻辑
适配器只负责格式转换和调用编排。金额校验、订单状态检查这些业务,请放在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)。

动手写一遍,适配器模式的“转换”思路会记得更牢。