# Adapter（适配器模式）

Adapter（适配器模式）属于结构型模式，别名 `wrapper`，结构性模式关注的是如何组合类与对象，以获得更大的结构，我们平常工作大部分时间都在与这种设计模式打交道。

**意图：将一个类的接口转换成客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能在一起工作的那些类可以一起工作。**

这个设计模式的意图很好懂，就是把接口不兼容问题抹平。注意，也仅仅能解决接口不一致的问题，而不能解决功能不一致的问题。

## 举例子

如果看不懂上面的意图介绍，没有关系，设计模式需要在日常工作里用起来，结合例子可以加深你的理解，下面我准备了三个例子，让你体会什么场景下会用到这种设计模式。

### 接口转换器

插座的种类很多，我们都用过许多适配器，将不同的插头进行转换，可以在不替换插座的情况下正常使用。

USB 接口转换也同样精彩，有将 TypeC 接口转换为 TypeA 的，也有将 TypeA 接口转换为 TypeC 的，支持双向转换。

接口转换器就是我们在生活中使用到的适配器模式，因为厂商并没有生产一个新的插座，我们也没有因为接口不适配而换一个手机，一切只需要一个接口转换器即可，这就是运用设计模式的收益。

### 数据库 ORM

ORM 屏蔽了 SQL 这一层，带来的好处是不需要理解不同 SQL 语法之间的区别，对于通用功能，ORM 会根据不同的平台，比如 Postgresql、Mysql 进行 SQL 的转换。

对 ORM 来说，屏蔽不同平台的差异，就是利用适配器模式做到的。

### API Deprecated

当一个广泛使用的库进行了含有 break change 的升级时，往往要留给开发者足够的时间去升级，而不能升级后就直接挂掉，因此被废弃的 API 要标记为 `deprecated`，而这种被废弃标记的 API 的实际实现，往往是使用新的 API 替代，这种场景正是使用了适配器模式，将新的 API 适配到旧的 API，实现 API Deprecated。

## 意图解释

上面三个例子都满足下面两个条件：

1. API 不兼容：因为接口的不同；数据库 SQL 语法的不同；框架 API 的不同。
2. 但能力已支持：插座都拥有充电或读取能力；不同的 SQL 都拥有查询数据库能力；新 API 覆盖了旧 API 的能力。

这样就可以通过适配器满足 Adapter 的意图：

**意图：将一个类的接口转换成客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能在一起工作的那些类可以一起工作。**

## 结构图

适配器的实现分为继承与组合模式。

下面是名词解释：

- `Adapter` 适配器，把 `Adeptee` 适配成 `Target`。
- `Adaptee` 被适配的内容，比如不兼容的接口。
- `Target` 适配为的内容，比如需要用的接口。

继承：

<img width=400 src="https://img.alicdn.com/tfs/TB1iy7Gk4vbeK8jSZPfXXariXXa-1590-518.png">

适配器继承 `Adaptee` 并实现 `Target`，适用场景是 `Adaptee` 与 `Target` 结构类似的情况，因为这样只需要实现部分差异化即可。

组合：

<img width=400 src="https://img.alicdn.com/tfs/TB1SrW21EY1gK0jSZFMXXaWcVXa-1524-500.png">

组合的拓展性更强，但工作量更大，如果 `Target` 与 `Adaptee` 结构差异较大，适合用组合模式。

## 代码例子

下面例子使用 typescript 编写。

继承：

```typescript
interface ITarget {
  // 标准方式是 hello
  hello: () => void
}

class Adaptee {
  // 要被适配的类方法叫 sayHello
  sayHello() {
    console.log('hello')
  }
}

// 适配器继承 Adaptee 并实现 ITarget
class Adapter extends Adaptee implements ITarget {
  hello() {
    // 用 sayHello 对接到 hello
    super.sayHello()
  }
}
```

组合：

```typescript
interface ITarget {
  // 标准方式是 hello
  hello: () => void
}

class Adaptee {
  // 要被适配的类方法叫 sayHello
  sayHello() {
    console.log('hello')
  }
}

// 适配器继承 Adaptee 并实现 ITarget
class Adapter implements ITarget {
  private adaptee: Adaptee 

  constructor(adaptee: Adaptee) {
    this.adaptee = adaptee
  }

  hello() {
    // 用 adaptee.sayHello 对接到 hello
    this.adaptee.sayHello()
  }
}
```

## 弊端

**使用适配器模式本身就可能是个问题**，因为一个好的系统内部不应该做任何侨界，模型应该保持一致性。只有在如下情况才考虑使用适配器模式：

1. 新老系统接替，改造成本非常高。
2. 三方包适配。
3. 新旧 API 兼容。
4. 统一多个类的接口。一般可以结合工厂方法使用。

## 总结

适配器模式也符合开闭原则，在不对原有对象改造的前提下，构造一个适配器就能完成模块衔接。

适配器模式的实现分为类与对象模式，类模式用继承，对象模式用组合，分别适用于 `Adaptee` 与 `Target` 结构相似与结构差异较大的场景，在任何情况下，组合模式都是灵活性最高的。

最后用一张图概括一下适配器模式的思维：

<img width=400 src="https://img.alicdn.com/tfs/TB16L2n1AY2gK0jSZFgXXc5OFXa-1254-630.png">

> 讨论地址是：[精读《设计模式 - Adapter 适配器模式》· Issue #279 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/279)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
