43

1 什么是适配器模式

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

在生活中,我们有许多的适配器,例如iPhone7以后的耳机接口从3.5mm圆孔接口更改成为了苹果专属的 lightning接口。许多人以前的圆孔耳机就需要下面的一个适配器,才能够在自个儿新买的iPhone上面听歌。

image_1chh3hfii6f51tbbs71cc7cnqm.png-64.6kB

在前端开发中,我们可能会遇见这样的场景:当我们试图调用某个模块或者对象的接口时,却发现这个接口的格式不符合我们的需求。这时有两种解决办法:第一种是修改原来的接口实现,但如果原来的代码很复杂,例如是一个库或框架,更改原代码就显得很不现实了。所以这时就需要使用今天所讲的第二种办法:创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要使用适配器即可。

对于只有JavaScript这一门语言经验的前端开发来说,可能对于接口的概念比较陌生。建议参考阅读TypeScript-接口的文档来更好的理解接口。

2 ES6中的适配器模式

在前端项目中,适配器模式的使用场景一般有以下三种情况:库的适配、参数的适配和数据的适配。下面我将以我在项目中的实际例子来说明。

2.1 库的适配

项目上线前通常会要求前端开发者在页面中会接入统计网页数据用的SDK,这些SDK能够采集用户的信息和网页行生成可视化的图表和表格,来帮助网站运营人员和产品经理更好的根据用户行为来提升网页质量。我们来看一下适配器在接入采集数据的库时的使用场景:

Who-is-a-Data-Analyst-Digital-nest-www.digitalnest.in_.png-230kB

目前国内做得比较好的数据分析网站有百度统计神策数据友盟等。在一个你做的电商类网站 项目上线前,你的产品经理要求你接入了百度的代码用于数据采集,并在几十个涉及用户操作的地方进行了埋点。百度统计提供的埋点接口格式如下:

 _hmt.push(['_trackEvent', category, action, opt_label,opt_value]);

按照产品经理的要求,你根据上面的格式将埋点代码写到了页面的多个地方:

//index.html
_hmt.push(['_trackEvent', 'web', 'page_enter', 'position', 'index.html']);

//product-detail.html
_hmt.push(['_trackEvent', 'web', 'page_enter', 'position', 'product-detail.html']);

_hmt.push(['_trackEvent', 'web', 'product_detail_view', 'product_id', productId]);

_hmt.push(['_trackEvent', 'web', 'add-product-chart', 'product_id', productId]);

//...还有几十个页面

过了几个月之后,该电商网站发展速度很快,运营人员感觉到百度统计提供的采集数据在已经无法满足当前网站的规模。运营人员和产品经理商量后决定,数据采集平台需要从百度统计切换到神策数据,神策数据提供的埋点接口格式如下:

sa.track(eventName, {
  attrName: value 
})

接口的规则不同,就意味着你需要将几十个百度统计的_htm.push接口更改成为神策提供的sa.track接口。其实不用这么麻烦,写一个适配器就可以完成所有埋点事件的迁移:

//app.js

let _hmt = {
  push: (arr) {
    
    const [eventName, attrName, value] = [...arr.splice(2)];
    
    let attrObj = {
      [attrName]: value
    };

    sa.track(eventName, attrObj);
  }
}

通过分析比较百度统计的接口和神策的接口,可以知道在神策中只需要传入三个参数,eventName对应的是百度统计接口中的action, attrName对应的是百度统计接口中的opt_labelvalue对应的是百度统计接口中的opt_value; 删除了百度统计的SDK后,SDK所提供的_htm这个全局变量也就不存在了,我们可以利用该变量名做适配器,在push方法获取sa.track所需要的三个参数并调用sa.track即可。

2.2 参数的适配

有的情况下一个方法可能需要传入多个参数,例如在SDK这个类中有一个phoneStatus,需要传入五个参数用于接收手机的相关信息:

class SDK {
  phoneStatus(brand, os, carrier, language, network) {

    //dosomething.....
  }
}

通常在传入的参数大于3的时候,我们就可以考虑将参数合并为一个对象的形式,就像我们$.ajax的做法一样。下面我们可以将phoneStatus的参数接口定义如下(String代表参数类型,?: 代表可选项)

{
  brand: String
  os: String
  carrier:? String
  language:? String
  network:? String
}

可以看出,carrierlanguagenetwork这三个属性不是必须传入的,它们在方法内部可能被设置一些默认值。所以这个时候我们就可以在方法内部采用适配器来适配这个参数对象。

class SDK {
  phoneStatus(config) {
    
    let defaultConfig = {
      brand: null,  //手机品牌
      os: null, //系统类型: Andoird或 iOS
      carrier: 'china-mobile', //运营商,默认中国移动
      language: 'zh', //语言类型,默认中文
      network: 'wifi', //网络类型,默认wifi
    }
    
    //参数适配
    for( let i in config) {
      defaultConfig[i] = config[i] || defaultConfig[i];
    }
    
    //dosomething.....
  }
}

2.3 数据的适配

数据的适配在前端中是最为常见的场景,这时适配器在解决前后端的数据依赖上有着重要的意义。通常服务器端传递的数据和我们前端需要使用的数据格式是不一致的,特别是在在使用一些UI框架时,框架所规定的数据有着固定的格式。所以,这个时候我们就需要对后端的数据格式进行适配。

例如网页中有一个使用Echarts折线图对网站每周的uv,通常后端返回的数据格式如下所示:

[
  {
    "day": "周一",
    "uv": 6300
  },
  {
    "day": "周二",
    "uv": 7100
  },  {
    "day": "周三",
    "uv": 4300
  },  {
    "day": "周四",
    "uv": 3300
  },  {
    "day": "周五",
    "uv": 8300
  },  {
    "day": "周六",
    "uv": 9300
  }, {
    "day": "周日",
    "uv": 11300
  }
]

但是Echarts需要的x轴的数据格式和坐标点的数据是长下面这样的:

["周二", "周二", "周三", "周四", "周五", "周六", "周日"] //x轴的数据

[6300. 7100, 4300, 3300, 8300, 9300, 11300] //坐标点的数据

所以这是我们就可以使用一个适配器,将后端的返回数据做适配:

//x轴适配器
function echartXAxisAdapter(res) {
  return res.map(item => item.day);
}

//坐标点适配器
function echartDataAdapter(res) {
  return res.map(item => item.uv);
}

3 总结

适配器模式在JS中的使用场景很多,在参数的适配上,有许多库和框架都使用适配器模式;数据的适配在解决前后端数据依赖上十分重要。但是适配器模式本质上是一个亡羊补牢的模式,它解决的是现存的两个接口之间不兼容的问题,你不应该在软件的初期开发阶段就使用该模式;如果在设计之初我们就能够统筹的规划好接口的一致性,那么适配器就应该尽量减少使用。


参考文献:

  • [1] 张荣铭 JavaScript设计模式 [M].人民邮电出版社
  • [2] 程杰 大话设计模式 [M].清华大学出版社
  • [3] 曾探 JavaScript设计模式与开发实践 [M].r人民邮电出版社

yihui_indie
2.5k 声望938 粉丝

前端拼装工程师