第三章:适配器模式详解——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判断。而且换网关时,业务代码也得改。

我们用适配器模式解决了这个问题:

  1. 定义统一的目标接口:PaymentAdapter,包含pay()、refund()、query()方法
  2. 为每个网关写一个适配器:AlipayAdapter、WechatAdapter、UnionPayAdapter
  3. 业务代码只依赖目标接口:通过工厂或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图来展示适配器模式的核心逻辑和两种实现方式的关系:

适配器模式核心结构 目标接口 (Target) request() 被适配者 (Adaptee) specificRequest() 类适配器 extends Adaptee implements Target 对象适配器 implements Target 持有Adaptee引用 客户端 (Client) 实现 继承 实现 组合 调用 目标接口 被适配者 适配器 客户端

3.7 什么时候该用适配器模式?

我总结了几条经验,供你参考:

  • 系统集成时:两个系统接口不匹配,又不想改任何一方的代码
  • 第三方SDK替换:比如从阿里云OSS换成腾讯云COS,适配器可以屏蔽差异
  • 老系统改造:新框架要求新接口,老系统不能动,加一层适配器过渡
  • 统一多个相似接口:比如上面说的支付网关场景

注意:

适配器模式不是万能的。如果接口差异太大(比如一个同步一个异步,一个阻塞一个非阻塞),适配器会变得很复杂。这时候可能需要考虑更重的方案,比如消息队列或事件驱动架构。

好了,适配器模式就讲到这里。记住一句话:适配器是“粘合剂”,不是“万能胶”。用对了地方,它能帮你省下大量重构时间;用错了,反而增加系统复杂度。

可可精品资料库