登录
  • 欢迎访问悠扬的技术博客,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站😉

设计模式-结构型模式(一)适配器模式

设计模式 悠扬 1265次浏览 已收录

聊聊适配器

适配器模式是Adapter,也称Wrapper,是指如果一个接口需要B接口,但是待传入的对象却是A接口,怎么办?

我们举个例子。如果去美国,我们随身带的电器是无法直接使用的,因为美国的插座标准和中国不同,所以,我们需要一个适配器:

设计模式-结构型模式(一)适配器模式

模式的定义与特点

适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

该模式的主要优点如下。

  • 客户端通过适配器可以透明地调用目标接口。
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 在很多业务场景中符合开闭原则。

其缺点是:

  • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

模式的结构与实现

类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。现在来介绍它们的基本结构。

模式的结构

适配器模式(Adapter)包含以下主要角色。

  1. 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  2. 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  3. 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

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

类适配器模式的结构图如图所示。

设计模式-结构型模式(一)适配器模式

对象适配器模式的结构图如图 所示。

设计模式-结构型模式(一)适配器模式

 

在程序设计中,适配器也是类似的。我们已经有一个Task类,实现了Callable接口:

public class Task implements Callable<Long> {
    private long num;
    public Task(long num) {
        this.num = num;
    }

    public Long call() throws Exception {
        long r = 0;
        for (long n = 1; n <= this.num; n++) {
            r = r + n;
        }
        System.out.println("Result: " + r);
        return r;
    }
}

现在,我们想通过一个线程去执行它:

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(callable); // compile error!
thread.start();

发现编译不过!因为Thread接收Runnable接口,但不接收Callable接口,肿么办?

一个办法是改写Task类,把实现的Callable改为Runnable,但这样做不好,因为Task很可能在其他地方作为Callable被引用,改写Task的接口,会导致其他正常工作的代码无法编译。

另一个办法不用改写Task类,而是用一个Adapter,把这个Callable接口“变成”Runnable接口,这样,就可以正常编译:

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();

这个RunnableAdapter类就是Adapter,它接收一个Callable,输出一个Runnable。怎么实现这个RunnableAdapter呢?我们先看完整的代码:

public class RunnableAdapter implements Runnable {
    // 引用待转换接口:
    private Callable<?> callable;

    public RunnableAdapter(Callable<?> callable) {
        this.callable = callable;
    }

    // 实现指定接口:
    public void run() {
        // 将指定接口调用委托给转换接口调用:
        try {
            callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

编写一个Adapter的步骤如下:

  1. 实现目标接口,这里是Runnable
  2. 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
  3. 在目标接口的实现方法内部,调用待转换接口的方法。

这样一来,Thread就可以接收这个RunnableAdapter,因为它实现了Runnable接口。Thread作为调用方,它会调用RunnableAdapterrun()方法,在这个run()方法内部,又调用了Callablecall()方法,相当于Thread通过一层转换,间接调用了Callablecall()方法。

适配器模式在Java标准库中有广泛应用。比如我们持有数据类型是String[],但是需要List接口时,可以用一个Adapter:

String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));

注意到List<T> Arrays.asList(T[])就相当于一个转换器,它可以把数组转换为List

我们再看一个例子:假设我们持有一个InputStream,希望调用readText(Reader)方法,但它的参数类型是Reader而不是InputStream,怎么办?

当然是使用适配器,把InputStream“变成”Reader

InputStream input = Files.newInputStream(Paths.get("/path/to/file"));
Reader reader = new InputStreamReader(input, "UTF-8");
readText(reader);

InputStreamReader就是Java标准库提供的Adapter,它负责把一个InputStream适配为Reader。类似的还有OutputStreamWriter

如果我们把readText(Reader)方法参数从Reader改为FileReader,会有什么问题?这个时候,因为我们需要一个FileReader类型,就必须把InputStream适配为FileReader

FileReader reader = new InputStreamReader(input, "UTF-8"); // compile error!

直接使用InputStreamReader这个Adapter是不行的,因为它只能转换出Reader接口。事实上,要把InputStream转换为FileReader也不是不可能,但需要花费十倍以上的功夫。这时,面向抽象编程这一原则就体现出了威力:持有高层接口不但代码更灵活,而且把各种接口组合起来也更容易。一旦持有某个具体的子类类型,要想做一些改动就非常困难。


版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明设计模式-结构型模式(一)适配器模式
喜欢 (0)
支付宝[]
分享 (0)
悠扬
关于作者:
10年以上工作经验:6年以上微服务架构设计搭建经验。 曾任岗位:项目经理、架构师。 擅长领域:大数据、数据库,架构设计,资源优化。 获得业绩: 1.实用新型发明专利1个,修改Apache Sharding源码设计实现分库分表程序增强方案。 2.开源项目一个:https://gitee.com/zsiyang/ruoyi-vue-atomikos (加入开源生态圈)。 3.个人技术博客地址:https://www.nxhz1688.com