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

适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:
当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。

现实中的适配器

Mac book电池支持的电压是20V,我们日常生活中的交流电压一般是220V。除了我们了解的220V交流电压,日本和韩国的交流电压大多是100V,而英国和澳大利亚的是240V。笔记本电脑的电源适配器就承担了转换电压的作用,电源适配器使笔记本电脑在100V~240V的电压之内都能正常工作,这也是它为什么被称为电源“适配器”的原因。

适配器模式:重构封装的网络请求库 - 图1

适配器模式的应用

如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。因为没有人可以完全预料到未来的事情,也许现在好好工作的接口,未来的某天却不再适用于新系统,那么我们可以用适配器模式把旧接口包装成一个新的接口,使它继续保持生命力。

假如有一个旧项目需要重构,其中封装的网络请求库是基于 XMLHttpRequest的:

  1. function Ajax(type, url, data, success, failed){
  2. // 创建ajax对象
  3. var xhr = null;
  4. if(window.XMLHttpRequest){
  5. xhr = new XMLHttpRequest();
  6. } else {
  7. xhr = new ActiveXObject('Microsoft.XMLHTTP')
  8. }
  9. ...(此处省略一系列的业务逻辑细节)
  10. var type = type.toUpperCase();
  11. // 识别请求类型
  12. if(type == 'GET'){
  13. if(data){
  14. xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
  15. }
  16. // 发送get请求
  17. xhr.send();
  18. } else if(type == 'POST'){
  19. xhr.open('POST', url, true);
  20. // 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
  21. xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  22. // 发送post请求
  23. xhr.send(data);
  24. }
  25. // 处理返回数据
  26. xhr.onreadystatechange = function(){
  27. if(xhr.readyState == 4){
  28. if(xhr.status == 200){
  29. success(xhr.responseText);
  30. } else {
  31. if(failed){
  32. failed(xhr.status);
  33. }
  34. }
  35. }
  36. }
  37. }

大家知道我们现在有一个非常好用异步方案叫fetch,它的写法比ajax优雅很多。因此在不考虑兼容性的情况下,我们更愿意使用fetch、而不是使用ajax来发起异步请求。为了能更好地使用fetch,可能会封装一个基于fetch的http方法库:

  1. class HttpUtils {
  2. // get方法
  3. static get(url) {
  4. return new Promise((resolve, reject) => {
  5. // 调用fetch
  6. fetch(url)
  7. .then(response => response.json())
  8. .then(result => {
  9. resolve(result)
  10. })
  11. .catch(error => {
  12. reject(error)
  13. })
  14. })
  15. }
  16. // post方法,data以object形式传入
  17. static post(url, data) {
  18. return new Promise((resolve, reject) => {
  19. // 调用fetch
  20. fetch(url, {
  21. method: 'POST',
  22. headers: {
  23. Accept: 'application/json',
  24. 'Content-Type': 'application/x-www-form-urlencoded'
  25. },
  26. // 将object类型的数据格式化为合法的body参数
  27. body: HttpUtils.changeData(data)
  28. })
  29. .then(response => response.json())
  30. .then(result => {
  31. resolve(result)
  32. })
  33. .catch(error => {
  34. reject(error)
  35. })
  36. })
  37. }
  38. // body请求体的格式化方法
  39. static changeData(obj) {
  40. var prop,
  41. str = ''
  42. var i = 0
  43. for (prop in obj) {
  44. if (!prop) {
  45. return
  46. }
  47. if (i == 0) {
  48. str += prop + '=' + obj[prop]
  49. } else {
  50. str += '&' + prop + '=' + obj[prop]
  51. }
  52. i++
  53. }
  54. return str
  55. }
  56. }

当想使用 fetch 发起请求时,只需要这样轻松地调用,而不必再操心繁琐的数据配置和数据格式化:

  1. // 定义目标url地址
  2. const URL = "xxxxx"
  3. // 定义post入参
  4. const params = {
  5. ...
  6. }
  7. // 发起post请求
  8. const postResponse = await HttpUtils.post(URL,params) || {}
  9. // 发起get请求
  10. const getResponse = await HttpUtils.get(URL) || {}

但遗憾的是不仅接口名不同,入参方式也不一样。除了大动干戈地改写前端代码之外,另外一种更轻便的解决方式就是新增一个抹平差异的适配器,要把老代码迁移到新接口,不一定要挨个儿去修改每一次的接口调用,我们只需要在引入接口时进行一次适配便可轻松地 cover 掉业务里可能会有的多次调用。

  1. // Ajax适配器函数,入参与旧接口保持一致
  2. async function AjaxAdapter(type, url, data, success, failed) {
  3. const type = type.toUpperCase()
  4. let result
  5. try {
  6. // 实际的请求全部由新接口发起
  7. if(type === 'GET') {
  8. result = await HttpUtils.get(url) || {}
  9. } else if(type === 'POST') {
  10. result = await HttpUtils.post(url, data) || {}
  11. }
  12. // 假设请求成功对应的状态码是1
  13. result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
  14. } catch(error) {
  15. // 捕捉网络错误
  16. if(failed){
  17. failed(error.statusCode);
  18. }
  19. }
  20. }
  21. // 用适配器适配旧的Ajax方法
  22. async function Ajax(type, url, data, success, failed) {
  23. await AjaxAdapter(type, url, data, success, failed)
  24. }

如此一来,我们只需要编写一个适配器函数AjaxAdapter,并用适配器去承接旧接口的参数,就可以实现新旧接口的无缝衔接了。

总结

适配器模式是一对相对简单的模式,适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。