趁着这段时间的 618 活动,我在某宝入手了一台 iphone13 手机。不得不说,现在的手机厂商真是太精了,买了手机都不给配充电头,当时也没怎么在意,想着毕竟目前也是用着 iphone 手机,不是还有一个旧的充电头嘛。到货的时候就傻眼了,新手机只有一根数据线,接入充电头的那一头采用的是 usb-c 接口,而我的旧充电头只能接入 type-a 的排口,数据线没有办法插入旧的充电头。。 无奈准备再买一个合适的充电头回来,结果一看价格,149,这什么充电头,这么贵。。转念一想,没有买充电头的肯定不止我一个,有问题就一定有解决办法,果然,我翻到了有商家卖这样的一个东西。 无标题.png 价格相比充电头完全可以接受,果断买回来,一头接上手机数据线,一头插上旧的充电头,诶这不就 OK 了嘛。

一、什么是适配器模式

事实上,上例图片中的转接头在整个结构中就充当了适配的作用,而它就是适配器。所谓适配器,正如上例中的转换头一样,一头连接 type-c 类型的手机数据线,另一头连接着 usb-a 类型的充电头,这使得原本因为类型不匹配的两个零件能正常的工作。这就是适配器模式的核心思想。

1.1 模式意图

将一个类的接口转换成客户希望的另外一个接口,适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

适配器模式的意图非常明显,尽管如此,为了帮助理解,我们还是结合着上面的例子对意图进行说明:

  • 一个类的接口转换成客户希望的另外一个接口:在充电时,我的数据线插入充电头的那一端是 type-c 类型的,所以,我希望充电头是 usb-c 类型的;
  • 使得原本由于接口不兼容而不能一起工作的那些类可以一起工作:因为充电头是 usb-a 类型和我的 type-c 类型不兼容,没有办法接入,但因为有了转接头的存在,让他们又可以一起完成充电。

    1.2 如何实现

    分析上面的例子,在充电行为中,至少有三个角色参与其中,他们分别是充电头、装接头、手机数据线。在很多情况下,我们不得不为同一个充电头适配多个数据线(或者为一个数据线适配多个充电头),所以我们可以为该模式引入一个适配器的抽象。

    比如:现在我有一台游戏机也需要充电,该游戏机的数据线输入端同样不是 type-c 类型的,无法插入充电头,不得已我又买了一个对应的转接头。

现在,我们就得到如下的关系图:
结构型 - 适配器模式(Adapter) - 图2
在这个关系图中,我们拥有两根数据线,还有两个对应的转接头,但只有一个充电头。如果站在充电头的角度来说:

  1. 与充电头直接接入的是转接头,所以,只要接入不同的转接头就能为不同的设备提供充电服务;
  2. 对于不同的转接头来说,他们仅仅只是完成适配的工作,转接头本身并不提供充电服务,充电头给设备充电其实是将工作交给转接头,而转接头同样把工作委派给另一头的设备数据线,所以,我们可以将所有的转接头进行抽象;

    1.3 类图分析

    适配器模式的类图则如下所示:
    结构型 - 适配器模式(Adapter) - 图3
    在适配器模式中,包含有以下主要角色,我们可以将各个角色与上面的例子进行类比分析:
  • Client:使用放 Target 接口的用户。与充电头的作用一致,通过相应的转接头给不同的设备提供充电服务;
  • Target:定义给 Client 使用的接口。定义一个将其他类型的接线口转换成 type-a 类型的方法,由具体的适配器实现,不同的适配器可针对不同的接线口类型进行适配;
  • Adapter: 对 Adaptee 的接口与 Target 接口进行适配 。等效于手机数据线转接头、游戏机数据线转接头,负责将其他类型的接口转换成 type-a 类型,接入充电头;
  • Adaptee:定义一个已经存在的接口,这个接口需要适配。比如手机数据线、游戏机数据线,需要适配器来适配,否则无法接入充电头。

    二、案例实现

    2.1 Adaptee(被适配者)

    ```java public class PhoneUsbCable {

    public void accessTypeC (){

    1. System.out.println(" 手机数据线接入 type-c 类型接口");

    }

}

  1. ```java
  2. public class GameConsoleUsbCable {
  3. public void accessTypeB (){
  4. System.out.println(" 游戏机数据线接入 type-b 类型接口");
  5. }
  6. }

2.2 Target(适配接口)

  1. public interface AccessUsbCable {
  2. /**
  3. * 接入数据线
  4. */
  5. void access();
  6. }

2.3 Adapter(具体适配器)

  1. public class PhoneUsbCableAdapter implements AccessUsbCable {
  2. private final PhoneUsbCable phoneUsbCable;
  3. public PhoneUsbCableAdapter(PhoneUsbCable phoneUsbCable) {
  4. this.phoneUsbCable = phoneUsbCable;
  5. }
  6. @Override
  7. public void access() {
  8. System.out.println(" 手机数据线适配器接入 type-a 类型的接口,接出 usb-c 类型的接口");
  9. this.phoneUsbCable.accessTypeC();
  10. }
  11. }
  1. public class GameConsoleUsbCableAdapter implements AccessUsbCable {
  2. private final GameConsoleUsbCable gameConsoleUsbCable;
  3. public GameConsoleUsbCableAdapter(GameConsoleUsbCable gameConsoleUsbCable) {
  4. this.gameConsoleUsbCable = gameConsoleUsbCable;
  5. }
  6. @Override
  7. public void access() {
  8. System.out.println(" 游戏机数据线适配器接入 type-a 类型的接口,接出 usb-b 类型的接口");
  9. this.gameConsoleUsbCable.accessTypeB();
  10. }
  11. }

2.4 Client(适配器的调用方)

  1. public class Plug {
  2. public static void main(String[] args) {
  3. System.out.println("手机充电时接线,该插座接出 usb-a 类型的接口:");
  4. AccessUsbCable phone = new PhoneUsbCableAdapter(new PhoneUsbCable());
  5. phone.access();
  6. System.out.println("游戏机充电时接线,该插座接出 usb-a 类型的接口:");
  7. AccessUsbCable gameConsole = new GameConsoleUsbCableAdapter(new GameConsoleUsbCable());
  8. gameConsole.access();
  9. }
  10. }
  1. 手机充电时接线,该插座接出 usb-a 类型的接口:
  2. 手机数据线适配器接入 type-a 类型的接口,接出 usb-c 类型的接口
  3. 手机数据线接入 type-c 类型接口
  4. 游戏机充电时接线,该插座接出 usb-a 类型的接口:
  5. 游戏机数据线适配器接入 type-a 类型的接口,接出 usb-b 类型的接口
  6. 游戏机数据线接入 type-b 类型接口

三、关于更多

3.1 适用场景

适配器有一个比较独特的使用场景,如果用一个词描述的话,就是“亡羊补牢”。

回想一开始的例子,如果我在一开始就知道 iphone13 数据线与现有的充电头无法适配时,我或许会在下单手机的同时就买一个对应的充电头回来,这样就能完全匹配。但事实是,我发现了数据线与现有的充电头无法直连,才出此下策,买了一个转接头回来进行适配。

由此可以看出,由于前期设计上出现问题,导致实际产品在对接时出现偏差的时候我们就可以使用适配器模式来挽救这个局面,这样做的好处是:已经出现偏差的两方产品不需要改动。实际上,出于挽救这一初衷而采用适配器模式的场景有很多。比如说:

a). 现有几个类,拥有相似的功能,但是没有统一的规范(没有接口约束,方法名,参数均不一样),可以用适配器模式分别适配这几个类,使调用方可以按照统一的方式进行调用; b). 我给别人提供了 sdk,在新的版本中我发现将某个 api 的参数类型调整一下,能让整个方法更加高效,但是这样就意味着使用旧版本的用户升级到新版本时,不得不改动调用。此时可以不改 api 的方法签名,只需要将旧的 api 方法委托给新的接口实现。

3.2 从源码中看适配器模式

(1)java.util.concurrent.Executors 类中的静态内部类 RunnableAdapter

  1. public class Executors {
  2. /**
  3. * 运行给定任务并返回给定结果的可调用对象
  4. */
  5. static final class RunnableAdapter<T> implements Callable<T> {
  6. final Runnable task;
  7. final T result;
  8. RunnableAdapter(Runnable task, T result) {
  9. this.task = task;
  10. this.result = result;
  11. }
  12. public T call() {
  13. task.run();
  14. return result;
  15. }
  16. }
  17. }

我们看到:在 RunnableAdapter 这个类的定义中,实现了 Callable 接口,内部维护了一个 Runnable 类型的 task ,在实现的 call 方法中,调用了 Runnable 的 run() 方法。所以,RunnableAdapter 类本身就是一个适配器,目的就是将 Runnable 类型的对象包装成一个 Callable 类型的对象。在这段源码中,Target 是 Callable 接口,Adapter 是RunnableAdapter ,Adaptee 是 Runable 的实例 task。

这样看来 RunnableAdapter 其实是将 Runnable 包装成 Callable,这正是适配器模式的特点。那这样做对 jdk 来说有什么实际的意义?要想知道这样封装有什么作用,得看哪里在使用这个类。我们跟踪到 Executors 类中使用了这个类,而正好 Executors 类我们比较熟悉。

  1. public class Executors {
  2. /**
  3. * 运行给定任务并返回给定结果的可调用对象
  4. */
  5. static final class RunnableAdapter<T> implements Callable<T> {
  6. // ...
  7. }
  8. /**
  9. * 返回一个 {@link Callable} 对象,该对象在被调用时运行给定任务并返回给定结果。
  10. * 这在将需要 {@code Callable} 的方法应用于其他没有结果的操作时很有用。
  11. * @param task 要运行的任务
  12. * @param result 返回的结果
  13. * @param <T> 结果的类型
  14. * @return 可调用对象
  15. * @throws NullPointerException 如果任务为空
  16. */
  17. public static <T> Callable<T> callable(Runnable task, T result) {
  18. if (task == null)
  19. throw new NullPointerException();
  20. return new RunnableAdapter<T>(task, result);
  21. }
  22. public static Callable<Object> callable(Runnable task) {
  23. if (task == null)
  24. throw new NullPointerException();
  25. return new RunnableAdapter<Object>(task, null);
  26. }
  27. }

callable() 方法的文档注释中说:_当将需要 Callable 的方法应用于其他无结果的操作时,这个方法会很有用_。这说了个啥??既然看不懂,那就再往上找,看看哪里在调用这个方法。然后就跟踪到了 FutureTask 这个类。

  1. public class FutureTask<V> implements RunnableFuture<V> {
  2. private Callable<V> callable;
  3. private volatile Thread runner;
  4. public FutureTask(Callable<V> callable) {
  5. if (callable == null)
  6. throw new NullPointerException();
  7. this.callable = callable;
  8. this.state = NEW; // ensure visibility of callable
  9. }
  10. public FutureTask(Runnable runnable, V result) {
  11. this.callable = Executors.callable(runnable, result);
  12. this.state = NEW; // ensure visibility of callable
  13. }
  14. }

看到这里,一下就明白了。FutureTask 类中只维护了一个 Callable 类型的任务对象,但是 FutureTask 需要支持提交 Runnable 类型的任务。如果不将 Runnable 类型包装成 Callable 类型,就意味着 FutureTask 类还需要再维护一个 Runnable 类型的任务,而 FutureTask 只能维持一个任务,也就是说 Callable 类型和 Runnable 类型必然有一个任务是 null 。这对于 FutureTask 来说,在交给线程执行的时候就很麻烦了,需要找到不为空的那一个任务,且需根据任务的类型进行具体的处理。
梳理一下整个逻辑,不难得出下面这样的链路关系:

  • Runnable:worker thread —> futureTask.run() —> callable.call() —> task.run()
  • Callable:worker thread —> futureTask.run() —> callable.call()

所以,RunnableAdapter 类采用适配器模式主要是为了使上层应用只需要统一处理 Callable 类型的接口,以便上层应用只需处理一套逻辑。

附录

案例代码:…/adapter