适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:
当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决办法,第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
现实中的适配器
Mac book电池支持的电压是20V,我们日常生活中的交流电压一般是220V。除了我们了解的220V交流电压,日本和韩国的交流电压大多是100V,而英国和澳大利亚的是240V。笔记本电脑的电源适配器就承担了转换电压的作用,电源适配器使笔记本电脑在100V~240V的电压之内都能正常工作,这也是它为什么被称为电源“适配器”的原因。

适配器模式的应用
如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。因为没有人可以完全预料到未来的事情,也许现在好好工作的接口,未来的某天却不再适用于新系统,那么我们可以用适配器模式把旧接口包装成一个新的接口,使它继续保持生命力。
假如有一个旧项目需要重构,其中封装的网络请求库是基于 XMLHttpRequest的:
function Ajax(type, url, data, success, failed){// 创建ajax对象var xhr = null;if(window.XMLHttpRequest){xhr = new XMLHttpRequest();} else {xhr = new ActiveXObject('Microsoft.XMLHTTP')}...(此处省略一系列的业务逻辑细节)var type = type.toUpperCase();// 识别请求类型if(type == 'GET'){if(data){xhr.open('GET', url + '?' + data, true); //如果有数据就拼接}// 发送get请求xhr.send();} else if(type == 'POST'){xhr.open('POST', url, true);// 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");// 发送post请求xhr.send(data);}// 处理返回数据xhr.onreadystatechange = function(){if(xhr.readyState == 4){if(xhr.status == 200){success(xhr.responseText);} else {if(failed){failed(xhr.status);}}}}}
大家知道我们现在有一个非常好用异步方案叫fetch,它的写法比ajax优雅很多。因此在不考虑兼容性的情况下,我们更愿意使用fetch、而不是使用ajax来发起异步请求。为了能更好地使用fetch,可能会封装一个基于fetch的http方法库:
class HttpUtils {// get方法static get(url) {return new Promise((resolve, reject) => {// 调用fetchfetch(url).then(response => response.json()).then(result => {resolve(result)}).catch(error => {reject(error)})})}// post方法,data以object形式传入static post(url, data) {return new Promise((resolve, reject) => {// 调用fetchfetch(url, {method: 'POST',headers: {Accept: 'application/json','Content-Type': 'application/x-www-form-urlencoded'},// 将object类型的数据格式化为合法的body参数body: HttpUtils.changeData(data)}).then(response => response.json()).then(result => {resolve(result)}).catch(error => {reject(error)})})}// body请求体的格式化方法static changeData(obj) {var prop,str = ''var i = 0for (prop in obj) {if (!prop) {return}if (i == 0) {str += prop + '=' + obj[prop]} else {str += '&' + prop + '=' + obj[prop]}i++}return str}}
当想使用 fetch 发起请求时,只需要这样轻松地调用,而不必再操心繁琐的数据配置和数据格式化:
// 定义目标url地址const URL = "xxxxx"// 定义post入参const params = {...}// 发起post请求const postResponse = await HttpUtils.post(URL,params) || {}// 发起get请求const getResponse = await HttpUtils.get(URL) || {}
但遗憾的是不仅接口名不同,入参方式也不一样。除了大动干戈地改写前端代码之外,另外一种更轻便的解决方式就是新增一个抹平差异的适配器,要把老代码迁移到新接口,不一定要挨个儿去修改每一次的接口调用,我们只需要在引入接口时进行一次适配便可轻松地 cover 掉业务里可能会有的多次调用。
// Ajax适配器函数,入参与旧接口保持一致async function AjaxAdapter(type, url, data, success, failed) {const type = type.toUpperCase()let resulttry {// 实际的请求全部由新接口发起if(type === 'GET') {result = await HttpUtils.get(url) || {}} else if(type === 'POST') {result = await HttpUtils.post(url, data) || {}}// 假设请求成功对应的状态码是1result.statusCode === 1 && success ? success(result) : failed(result.statusCode)} catch(error) {// 捕捉网络错误if(failed){failed(error.statusCode);}}}// 用适配器适配旧的Ajax方法async function Ajax(type, url, data, success, failed) {await AjaxAdapter(type, url, data, success, failed)}
如此一来,我们只需要编写一个适配器函数AjaxAdapter,并用适配器去承接旧接口的参数,就可以实现新旧接口的无缝衔接了。
总结
适配器模式是一对相对简单的模式,适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。
