适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(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) => {
// 调用fetch
fetch(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) => {
// 调用fetch
fetch(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 = 0
for (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 result
try {
// 实际的请求全部由新接口发起
if(type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if(type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.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,并用适配器去承接旧接口的参数,就可以实现新旧接口的无缝衔接了。
总结
适配器模式是一对相对简单的模式,适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。