1. 适配器模式

适配器模式(Adapter Pattern)又称包装器模式,将一个类(对象)的接口(方法、属性)转化为用户需要的另一个接口,解决类(对象)之间接口不兼容的问题。

主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口。也就是说,访问者需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要是负责把不兼容的接口转换成访问者期望的格式而已。

在生活中我们会遇到形形色色的适配器,最常见的就是转接头了,比如不同规格电源接口的转接头、3.5 毫米耳机插口转接头、DP/miniDP/HDMI/DVI/VGA 等视频转接头、电脑、手机、ipad 的电源适配器,都是属于适配器的范畴。

在类似场景中,这些例子有以下特点:

  1. 旧有接口格式已经不满足现在的需要;
  2. 通过增加适配器来更好地使用旧有接口;

    2. 适配器模式的实现

    下面来实现一下电源适配器的例子,使用中国插头标准:
    1. var chinaPlug = {
    2. type: '中国插头',
    3. chinaInPlug() {
    4. console.log('开始供电')
    5. }
    6. }
    7. chinaPlug.chinaInPlug()
    8. // 输出:开始供电
    但是出国旅游,到了日本,需要增加一个日本插头到中国插头的电源适配器,来将原来的电源线用起来:
    1. var chinaPlug = {
    2. type: '中国插头',
    3. chinaInPlug() {
    4. console.log('开始供电')
    5. }
    6. }
    7. var japanPlug = {
    8. type: '日本插头',
    9. japanInPlug() {
    10. console.log('开始供电')
    11. }
    12. }
    13. /* 日本插头电源适配器 */
    14. function japanPlugAdapter(plug) {
    15. return {
    16. chinaInPlug() {
    17. return plug.japanInPlug()
    18. }
    19. }
    20. }
    21. japanPlugAdapter(japanPlug).chinaInPlug()
    22. // 输出:开始供电
    适配器模式的原理大概如下图:
    适配器模式 - 图1
    访问者需要目标对象的某个功能,但是这个对象的接口不是自己期望的,那么通过适配器模式对现有对象的接口进行包装,来获得自己需要的接口格式。

    3. 适配器模式的应用

    适配器模式在日常开发中还是比较频繁的,其实可能已经使用了,但却不知道原来这就是适配器模式。适配器可以将新的软件实体适配到老的接口,也可以将老的软件实体适配到新的接口,具体如何来进行适配,可以根据具体使用场景来灵活使用。

    (1)业务数据适配

    在实际项目中,我们经常会遇到树形数据结构和表形数据结构的转换,比如全国省市区结构、公司组织结构、军队编制结构等等。以公司组织结构为例,在历史代码中,后端给了公司组织结构的树形数据,在以后的业务迭代中,会增加一些要求非树形结构的场景。比如增加了将组织维护起来的功能,因此就需要在新增组织的时候选择上级组织,在某个下拉菜单中选择这个新增组织的上级菜单。或者增加了将人员归属到某一级组织的需求,需要在某个下拉菜单中选择任一级组织。

在这些业务场景中,都需要将树形结构平铺开,但是又不能直接将旧有的树形结构状态进行修改,因为在项目别的地方已经使用了老的树形结构状态,这时可以引入适配器来将老的数据结构进行适配:

  1. /* 原来的树形结构 */
  2. const oldTreeData = [
  3. {
  4. name: '总部',
  5. place: '一楼',
  6. children: [
  7. { name: '财务部', place: '二楼' },
  8. { name: '生产部', place: '三楼' },
  9. {
  10. name: '开发部', place: '三楼', children: [
  11. {
  12. name: '软件部', place: '四楼', children: [
  13. { name: '后端部', place: '五楼' },
  14. { name: '前端部', place: '七楼' },
  15. { name: '技术支持部', place: '六楼' }]
  16. }, {
  17. name: '硬件部', place: '四楼', children: [
  18. { name: 'DSP部', place: '八楼' },
  19. { name: 'ARM部', place: '二楼' },
  20. { name: '调试部', place: '三楼' }]
  21. }]
  22. }
  23. ]
  24. }
  25. ]
  26. /* 树形结构平铺 */
  27. function treeDataAdapter(treeData, lastArrayData = []) {
  28. treeData.forEach(item => {
  29. if (item.children) {
  30. treeDataAdapter(item.children, lastArrayData)
  31. }
  32. const { name, place } = item
  33. lastArrayData.push({ name, place })
  34. })
  35. return lastArrayData
  36. }
  37. treeDataAdapter(oldTreeData)
  38. // 返回平铺的组织结构

增加适配器后,就可以将原先状态的树形结构转化为所需的结构,而并不改动原先的数据,也不对原来使用旧数据结构的代码有所影响。

(2)Vue 计算属性

Vue 中的计算属性也是一个适配器模式的实例,以官网的例子为例:

  1. <template>
  2. <div id="example">
  3. <p>Original message: "{{ message }}"</p> <!-- Hello -->
  4. <p>Computed reversed message: "{{ reversedMessage }}"</p> <!-- olleH -->
  5. </div>
  6. </template>
  7. <script type='text/javascript'>
  8. export default {
  9. name: 'demo',
  10. data() {
  11. return {
  12. message: 'Hello'
  13. }
  14. },
  15. computed: {
  16. reversedMessage: function() {
  17. return this.message.split('').reverse().join('')
  18. }
  19. }
  20. }
  21. </script>

旧有 data 中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式。

4. 源码中的适配器模式

Axios 是比较热门的网络请求库,在浏览器中使用的时候,Axios 的用来发送请求的 adapter 本质上是封装浏览器提供的 API XMLHttpRequest,可以看看源码中是如何封装这个 API 的,为了方便观看,进行了一些省略:

  1. module.exports = function xhrAdapter(config) {
  2. return new Promise(function dispatchXhrRequest(resolve, reject) {
  3. var requestData = config.data
  4. var requestHeaders = config.headers
  5. var request = new XMLHttpRequest()
  6. // 初始化一个请求
  7. request.open(config.method.toUpperCase(),
  8. buildURL(config.url, config.params, config.paramsSerializer), true)
  9. // 设置最大超时时间
  10. request.timeout = config.timeout
  11. // readyState 属性发生变化时的回调
  12. request.onreadystatechange = function handleLoad() { ... }
  13. // 浏览器请求退出时的回调
  14. request.onabort = function handleAbort() { ... }
  15. // 当请求报错时的回调
  16. request.onerror = function handleError() { ... }
  17. // 当请求超时调用的回调
  18. request.ontimeout = function handleTimeout() { ... }
  19. // 设置HTTP请求头的值
  20. if ('setRequestHeader' in request) {
  21. request.setRequestHeader(key, val)
  22. }
  23. // 跨域的请求是否应该使用证书
  24. if (config.withCredentials) {
  25. request.withCredentials = true
  26. }
  27. // 响应类型
  28. if (config.responseType) {
  29. request.responseType = config.responseType
  30. }
  31. // 发送请求
  32. request.send(requestData)
  33. })
  34. }

可以看到这个模块主要是对请求头、请求配置和一些回调的设置,并没有对原生的 API 有改动,所以也可以在其他地方正常使用。这个适配器可以看作是对 XMLHttpRequest 的适配,是用户对 Axios 调用层到原生 XMLHttpRequest 这个 API 之间的适配层。

5. 适配器模式的优缺点

适配器模式的优点

  1. 已有的功能如果只是接口不兼容,使用适配器适配已有功能,可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码;
  2. 可扩展性良好,在实现适配器功能的时候,可以调用自己开发的功能,从而方便地扩展系统的功能;
  3. 灵活性好,因为适配器并没有对原有对象的功能有所影响,如果不想使用适配器了,那么直接删掉即可,不会对使用原有对象的代码有影响;

适配器模式的缺点:会让系统变得零乱,明明调用 A,却被适配到了 B,如果系统中这样的情况很多,那么对可阅读性不太友好。如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,可以考虑尽量把文档完善。

6. 适配器模式的适用场景

当你想用已有对象的功能,却想修改它的接口时,一般可以考虑一下是不是可以应用适配器模式。

  • 如果你想要使用一个已经存在的对象,但是它的接口不满足需求,那么可以使用适配器模式,把已有的实现转换成你需要的接口;
  • 如果你想创建一个可以复用的对象,而且确定需要和一些不兼容的对象一起工作,这种情况可以使用适配器模式,然后需要什么就适配什么;

    7. 适配器模式与其他模式的区别

    适配器模式和代理模式、装饰者模式看起来比较类似,都是属于包装模式,也就是用一个对象来包装另一个对象的模式,下面来看一下他们的异同。

    (1)适配器模式与代理模式

  • 适配器模式: 提供一个不一样的接口,由于原来的接口格式不能用了,提供新的接口以满足新场景下的需求;

  • 代理模式: 提供一模一样的接口,由于不能直接访问目标对象,找个代理来帮忙访问,使用者可以就像访问目标对象一样来访问代理对象;

    (2)适配器模式、装饰者模式与代理模式

  • 适配器模式: 功能不变,只转换了原有接口访问格式;

  • 装饰者模式: 扩展功能,原有功能不变且可直接使用;
  • 代理模式: 原有功能不变,但一般是经过限制访问的;