第十七章 适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能正常工作的两个软件实体可以一起工作。

适配器的别名是包装起(wrapper),这是一种相对简单的模式。在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候就有两种解决方法,第一种就是修改原有的接口实现。但是如果原来的模块非常复杂,修改原接口就显得不太现实了。

还有一种方法是创建一个适配器,将原接口转化为客户希望的另一个接口,客户只需要跟适配器打交道。

17.1 现实中的适配器

港式插头转换器:港式的电器插头比大陆的插头体积要大一点。如果从香港买了一个 Mac book,我们会发现充电器无法插在家里的插座上,为此我们不可能改造家里的插座,所以我们需要一个适配器

img

17.2 适配器模式的应用

如果现有的接口可以正常工作,那我们就永远不会用上适配器模式。适配器模式是一种亡羊补牢的模式,没有人会在程序的设计之初就使用它。因为我们不能预料未来会发生的事情,也许今天还好好工作的接口,明天就不适用于新系统。所以我们需要用适配器模式把原来的旧接口包装成一个新接口。

比如现在我们需要使用到地图的插件,现在我们有两个来自谷歌地图和百度地图的插件,它们内部都有一个 show 方法,我们只需要调用它就可以渲染到页面上。我们的代码是这样的:

js
1var googleMap = {
2  show: function() {
3    console.log(’开始渲染谷歌地图’);
4  }
5};
6
7var baiduMap = {
8  show: function() {
9    console.log(’开始渲染百度地图’);
10  }
11};
12
13var renderMap = function(map) {
14  if (map.show instanceof Function) {
15    map.show();
16  }
17};
18
19renderMap(googleMap); // 输出:开始渲染谷歌地图
20renderMap(baiduMap); // 输出:开始渲染百度地图

假设有一天,百度地图插件内部的 show 方法变掉了呢?

js
1var baiduMap = {
2  display: function() {
3    console.log(’开始渲染百度地图’);
4  }
5};

这时候,我们的程序遍地都写了 renderMap 方法,而这个方法非常复杂,我们没办法对其修改。

那么我们可以写一个适配器,然后把 renderMap 的参数换掉就可以了

js
1var baiduMapAdapter = {
2  show: function () {
3    baiduMap.display()
4  },
5}
6
7renderMap(baiduMapAdapter)

假设我们正在编写一个渲染广东省地图的页面。目前从第三方资源里获得了广东省的所有城市和它们所对应的 ID,并且成功渲染到页面上:

js
1var getGuangdongCity = function() {
2  var guangdongCity = [{
3    name: 'shenzhen',
4    id: 11,
5  }, {
6    name: 'guangzhou',
7    id: 12,
8  }];
9
10  return guangdongCity;
11};
12
13var render = function(fn) {
14  console.log(’开始渲染广东省地图’);
15  document.write(JSON.stringify(fn()));
16};
17
18render(getGuangdongCity);

目前这个版本的地图已经上线了,但是经过一段时间后发现,render 地图这个插件提供的方法修改了数据接口,最新的数据应该是这样的:

js
1var guangdongCity = {
2  shenzhen: 11,
3  guangzhou: 12,
4}

于是为了让老版本的数据也适配,我们可以写一个适配器

js
1var getGuangdongCity = function () {
2  var guangdongCity = [
3    {
4      name: 'shenzhen',
5      id: 11,
6    },
7    {
8      name: 'guangzhou',
9      id: 12,
10    },
11  ]
12  return guangdongCity
13}
14
15var render = function (fn) {
16  console.log('开始渲染广东省地图')
17  document.write(JSON.stringify(fn()))
18}
19// 把老版本的数据处理成新的数据
20var addressAdapter = function (oldAddressfn) {
21  var address = {}
22  var oldAddress = oldAddressfn()
23
24  for (var i = 0; i < oldAddress.length; i++) {
25    const c = oldAddress[i]
26    address[c.name] = c.id
27  }
28
29  return function () {
30    return address
31  }
32}
33
34render(addressAdapter(getGuangdongCity))

然后就可以在调用getGuangdongCity的地方用经过 addressAdapter 适配器转换之后的新函数来代替。

17.3 小结

适配器模式是一种相对简单的模式,它的结构和代理模式、装饰者模式等非常相似,都属于包装模式,由一个对象来包装另一个对象。

区别它们的方式是模式的意图

  • 适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑接口的实现,也不考虑它们未来如何演化。适配器模式不需要改变已有的接口,就可以使它们协同作用。
  • 装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。
  • 外观模式的作用和适配器相似,有人把外观者模式看成一组对象的适配器,但外观模式最显著的特点是定义一个新的接口。