Skip to content

适配器模式 (Adapter) —— 它是充当翻译官的

前言

在深入探讨适配器模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 适配器模式是什么?

适配器模式(Adapter Pattern)的作用就是牵线搭桥,让两个原本不兼容的接口能一起工作。

通俗解释: 最典型的例子就是旅行转接头。你去香港旅游,那边是英标插座(三个方头),而你的手机充电器是国标(两个扁头)。插不进去怎么办?你不会把酒店插座拆了,也不会把充电器砸了,而是买个“转接头”:一边插酒店插座,一边连你的充电器。这个转接头就是适配器。

1.1 为什么我们需要它?

在软件开发中,经常遇到这种情况:

  1. 老系统遗留问题:老系统的接口返回 XML 格式数据,但新系统只认 JSON
  2. 第三方库兼容:你想用一个很牛的第三方库,但它的接口定义跟你现在的系统不一样。

痛点:直接修改老系统或第三方库的代码?风险太大,甚至没源码改不了。修改新系统去迁就老接口?那新系统代码就脏了。最好的办法就是中间加个“翻译官”。

2. Java 实战演练:TF 卡转 USB

假设我们有一个老式的 TF卡,但现在的笔记本电脑只有 USB 接口。

2.1 场景角色

  1. Target (目标接口):客户端期待的接口(USB)。
  2. Adaptee (适配者):需要被适配的类(TFCard)。
  3. Adapter (适配器):把 Adaptee 包装成 Target(CardReader)。

2.2 代码实现 (对象适配器 - 推荐)

这是最常用的方式,使用组合关系。

java
// 1. 目标接口:USB
public interface USB {
    void readUSB();
}

// 2. 适配者:TF卡 (老接口)
public class TFCard {
    public void readTF() {
        System.out.println("读取 TF 卡中的数据:Hello World");
    }
}

// 3. 适配器:读卡器 (实现了 USB 接口,持有 TFCard 引用)
public class CardReaderAdapter implements USB {
    private TFCard tfCard;

    public CardReaderAdapter(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    @Override
    public void readUSB() {
        System.out.println("适配器开始工作...");
        // 核心:表面调 USB,实际转调 TF
        tfCard.readTF();
    }
}

// 4. 客户端调用
public class Computer {
    public void read(USB usb) {
        usb.readUSB();
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        TFCard tfCard = new TFCard();
        
        // computer.read(tfCard); // 报错!电脑插不进 TF 卡
        
        // 插上读卡器(适配器),再把读卡器插电脑上
        USB adapter = new CardReaderAdapter(tfCard);
        computer.read(adapter);
    }
}

3. 进阶:适配器的三种形态

3.1 对象适配器 (Object Adapter)

就是上面的例子,使用组合(Composition)方式。

  • 优点:灵活性高,一个适配器可以适配多个 Adaptee(只要它们是子类关系)。
  • 缺点:无法重写 Adaptee 的方法(因为是委托调用)。

3.2 类适配器 (Class Adapter)

使用继承(Inheritance)方式。

java
// 继承 TFCard,同时实现 USB
public class CardReaderInheritAdapter extends TFCard implements USB {
    @Override
    public void readUSB() {
        super.readTF();
    }
}
  • 优点:代码稍微少一点,可以重写 Adaptee 的方法。
  • 缺点:Java 是单继承,如果 TFCard 已经是 final 或者你需要适配多个类,就傻眼了。不推荐使用

3.3 接口适配器 (Interface Adapter / Default Adapter)

当一个接口方法太多(比如 20 个),而你只想实现其中 1 个时,可以使用此模式。

  • 做法:定义一个抽象类实现接口,所有方法都给空实现。子类继承抽象类,只重写需要的方法。
  • 例子:Java AWT 中的 WindowAdapter
java
public interface BigInterface {
    void method1();
    void method2();
    void method3();
    // ... method100
}

public abstract class DefaultAdapter implements BigInterface {
    public void method1() {}
    public void method2() {}
    public void method3() {}
    // ...
}

// 我只关心 method1
public class MyClass extends DefaultAdapter {
    @Override
    public void method1() {
        System.out.println("Do something");
    }
}

4. 源码中的适配器模式

  1. java.util.Arrays#asList(): 把数组适配成 List。

    java
    List<String> list = Arrays.asList("a", "b", "c");

    它返回的 ArrayList 不是 java.util.ArrayList,而是 Arrays 的内部类,它持有一个数组引用,把 List 的 get()set() 操作映射到数组上。

  2. java.io.InputStreamReader: 字节流转字符流的桥梁。

    java
    // System.in 是 InputStream (字节流)
    // BufferedReader 需要 Reader (字符流)
    // InputStreamReader 就是适配器
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  3. java.util.concurrent.FutureTask: Callable 有返回值,Runnable 没有。怎么让 Thread 运行 CallableFutureTask 实现了 Runnable 接口,同时构造函数接收 Callable。它就是个适配器。

  4. Spring MVC 的 HandlerAdapter: Spring MVC 需要调用各种类型的 Controller(有的实现 Controller 接口,有的用 @RequestMapping)。HandlerAdapter 用于屏蔽这些差异,统一调用流程。

5. 优缺点与适用场景总结

优点

  • 复用性好:不用改动现有的代码,就能复用老的类。
  • 解耦:将目标接口和适配者类解耦,引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  • 符合开闭原则:想换个适配器随时换,支持多态。

缺点

  • 系统复杂度增加:明明调用的是 A 接口,内部却被转成了 B 接口,如果适配器太多,代码读起来会有点晕。滥用适配器会让系统变得像个“大杂烩”。

适用场景

  1. 救火队员:系统维护后期,需要接入新功能或第三方接口,但接口定义不一致时。
  2. 封装遗留代码:把古老的、丑陋的接口封装成现代的、整洁的接口。
  3. 统一多个类的接口:希望复用一些现存的类,但是接口又与复用环境要求不一致。

6. 结语

适配器模式是亡羊补牢的模式。如果系统在设计之初就能统一接口,那就不需要适配器。它通常用于系统扩展、第三方库集成或遗留系统重构阶段。记住:不要为了用模式而用模式,如果能修改原有代码,最好直接重构,而不是加个适配器层。

7. 实验实操

design-patterns-web 项目中,你可以看到适配器模式如何解决接口不兼容问题。 场景中墙上的插座是欧标(EU)的。当你选择“US Plug”(美标插头)时,它无法插入插座,设备无法通电。此时点击“Add Adapter”(添加适配器),一个绿色的适配器组件会出现在插头和插座之间,成功连接了两端,设备通电工作。这生动展示了适配器如何“包装”一个对象,使其适应另一个接口。 截图建议:截取美标插头加上绿色适配器后成功连接欧标插座的画面。