适配器模式 Adapter Design Pattern

1,386 阅读7分钟

definition

适配器模式,有时候也成为包装样式或者包装 (wrapper)。将一个类的接口转接成用户所期待的,使得因接口不兼容而不能在一起工作的类可以在一起工作。 — wikipedia

适配器模式属于结构型模式。这一类型的模式主要是为了解决如何组织现有的类,设计他们的交互方式,从而达到一定的目的。包括了外观模式、代理模式、装饰模式、桥接模式、组合模式、享元模式以及今天要说的适配器模式。

那么问题来了,适配器模式到底是为了解决什么问题呢?

problems to solve

在软件开发领域,我们经常面临的一个问题就是你需要把一个方形的木头楔进一个圆形的洞里面!这个问题在现实生活中可能除了『削它』之外没别的什么好办法,不过软件世界中,因为是『软的』,我们可以非常方便的进行各种改造来满足我们的需求。

如上图所示,通过在 Client 和 Service 两个对象之间加入一个 Adapter 对象,两个本来无法在一起工作的类现在可以合作了。从这个图也很容易看出来,adapter 的其实就是在 client 和 service 之间创建了一个中间层,这又应验了一句老话『计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决』。

Adapter 模式的概念非常容易理解,在现实世界中我们也很容易找到类似概念的东西。

real world analogy (类比)

比如我们在做演示的时候经常需要使用一个视频数据线来连接笔记本和投影仪。从笔记本的角度看(作为 client),它只能通过 usb-c 向外输出视频信号,但我们的投影仪(作为 service)的接口是固定的,只接受 HDMI 的信号,无法更改(更改成本比较高)。一个 usb-c to HDMI 的数据线就起到了 adapter 的作用。

类似的还有很多,比如手机的充电器,苹果的雷电口转 3.5mm 耳机口转换器,比如我们出国旅行都需要带的插头转换器,三通阀门,等等。

structure

根据适配器的实现方式,可以分对象适配器和类适配器两个类型。

object adapter

对象适配器使用了对象组合的方式,adapter 实现一个类的接口,并内部容纳了另外一个类对象。

  • Client 是系统内部的现有代码
  • Client Interface 描述了与 Client 代码交互必须遵循的协议。
  • Service 包含了一些有用的类(通常是第三方库或者遗留代码),但 Client 不能直接使用,因为接口不兼容
  • Adapter 实现了 Client 的接口,内部包含了一个 Service 对象。Adapter 对象通过接口接受 Client 的调用,并将调用转换为 Service 对象能够理解的格式
  • client 侧的代码只依赖接口,与 adapter 代码完全解耦,因此你可以创建多种多样的 adapter 而不用担心对现有的 client/service 代码产生任何的影响

class adapter

类适配器使用了多继承的方式。 adapter 同时从两个对象类继承,所以这种方式通常在支持多继承的编程语言出现,比如 C++。

在这种模式下,adapter 不需要 wrap service 对象了,因为它通过继承,既是 client 又是 sercie,在 client 调用的方法中直接调用自己继承的 service 方法就可以了。

Java 因为不支持多继承,通常无法实现这种模式,但也可以实现类似的形式:让 adapter 来继承 Service 类并实现 Client Interface。与多继承的差别就是 adapter 还是需要自己来实现 client 接口定义的方法。

which one to choose?

  1. 从实现上:对象适配器使用组合的方式,属于动态组合;而类适配器使用继承的方式,属于静态定义
  2. 从工作模式上:对象适配器允许一个 adapter 组合多个 service,并可以兼容 service 的子类;而类适配器直接继承了 service 类,因此不能对 service 的子类进行适配
  3. 从定义的角度:对象适配器很难重定义 service 的行为,而类适配器因为使用了继承的方式,可以通过覆写重定义 service 的部分行为

总体来说,建议使用对象适配器的方式。

hands-on

我们来找一个实际的例子来看看适配器到底应该怎么用。

在 Java 标准库中,Arrays.asList() 就实现了适配器模式。

/**
 * Returns a fixed-size list backed by the specified array.  (Changes to
 * the returned list "write through" to the array.)  This method acts
 * as bridge between array-based and collection-based APIs, in
 * combination with {@link Collection#toArray}.
...
 */
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

这个方法接受变长参数的元素,返回了一个 ArrayList 对象,此 ArrayList 非彼 ArrayList,它是在 Arrays 类的一个 private 内部类,充当的是适配器的角色。

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public int size() {
        return a.length;
    }
...

这个适配器类的会将 List 接口方法的调用代理给内部的数组对象。这个适配器类主要作用是为了协调以数组为基础的 API 和以集合为基础的 API。在这个例子中,service 就是数组,client interface 就是 List,而依赖 List 的 API 就是 client。

pros & cons

  • 优势 适配器也是一种包装模式,还有委托的意思在里面,它适合在系统后期扩展、修改时候使用,因为只是新引入的 adapter 类,不需要对 client 和 service 做任何修改,非常的灵活。 适配器模式也符合 SOLID 中的单一职责原则开闭原则。前者主要体现在它将接口转换的工作从 client 分离出来,由适配器来承担;后者主要体现在适配器不需要 client 和 service 做任何的改动,而是通过继承来实现需要的功能。

  • 劣势 有得必有失。灵活性的增加通常也意味着复杂度的增加。 因为引入了新的类,应用的整体复杂度增加了。如果不加限制的过度使用适配器,会让系统变的非常混乱,因此如果条件允许的话,也许更好的方案是直接对系统进行重构。

comparison / relations with others

  • 适配器是让东西在设计实现之后能够适应新需求;桥接模式则是在他们设计的时候就想好了,可以让两部分的开发独立的进行
  • Adapter 提供了不同的接口(对于被 adapter 的对象,比如 usb-c 转 hdmi),而 Proxy 则提供是同样的接口,Decorator 提供的是增强的接口
  • Adapter 目的是为了改变已经存在的某个对象的接口,Decorator 则是为了不改变原来接口的前提下提供增强的接口。另外 Decorator 支持嵌套的组合,而 Adapter 则不行
  • Facade 为已经存在的对象增加了一个新的接口,而 Adapter 则是让已有的接口变得可用。Adapter 通常只包含一个对象,而 Facade 则通常是定义了一个子系统所有对象的接口
  • Bridge, State, Strategy, Adapter 有非常像的结构,实际上,这些模式都是基于 composition,也就是说将工作 delegate 给其他对象完成,但是这些模式解决的是不同的问题。一个模式并不仅仅是代码组合的特殊方式,它也能帮助其他的开发者理解这个模式解决的问题。

references

Adapter | refactoring.guru

适配器模式原理及实例介绍 | ibm.com

适配器模式 | runoob.com

@monkeyM