# 什么是适配器模式?
适配器模式:为多个不兼容接口之间提供“转化器”。即解决两个接口之间不匹配的问题。
它的实现非常简单,检查接口的数据,进行过滤、重组等操作,使另一接口可以使用数据即可。不考虑这些接口与是怎样实现的,也不考虑的将来是如何演化,不需要改变已有的接口,就能使其协同作用。
# 应用场景
当数据不符合使用规则,就可以借助此种模式进行格式转化。
# 代码实现案例1(API对外暴露统一数据格式)
假设编写了不同平台的音乐爬虫,破解音乐数据。而对外向用户暴露的数据应该是具有一致性。
下面,adapter
函数的作用就是转化数据格式。
事实上,在我开发的音乐爬虫库--music-api-next就采用了下面的处理方法。
因为,网易、QQ、虾米等平台的音乐数据不同,需要处理成一致的数据返回给用户,方便用户调用。
const API = {
qq() {
return {
n: '告白气球',
a: '周杰伦'
}
},
netease() {
return {
name: '告白气球',
author: '周杰伦'
}
},
kugou() {
return {
song_name: '告白气球',
author_name: '周杰伦',
}
}
}
const adapter = (obj) => {
return {
name: obj.n || obj.name || obj.song_name,
author: obj.a || obj.author || obj.author_name
}
}
let adapterQQ = adapter(API.qq());
let adapterNetease = adapter(API.netease());
let adapterKugou = adapter(API.kugou());
console.log(adapterQQ, adapterNetease, adapterKugou);
虽然三个音乐平台的API对外接口不一样,但是通过适配器adapter,入参都是一个包含歌曲名字和作者的对象,经过适配器的包装,就可以将出参处理为一个包括author和name的统一对象,然后将其暴露出来,这样调用调用者就不用关注如何处理不同平台API接口的兼容性,只需要调用即可。
实战案例2:通过适配器渲染同类功能的函数
var googleMap = {
show: function(){
console.log( '开始渲染谷歌地图' );
}
};
var baiduMap = {
display: function(){
console.log( '开始渲染百度地图' );
}
};
var baiduMapAdapter = {
show: function(){
return baiduMap.display();
}
};
renderMap( googleMap ); // 开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 开始渲染百度地图
在这段代码中适配器做的事情其实很简单,就是创建了一个对象,添加了一个同名的show()方法,然后在适配器里面调用了baiduMap.display()方法,这样我们只需要在调用baiduMap的时候调用我们的适配器即可达到预期效果;
实战案例3:通过适配器处理新老接口的兼容性
let oldCityList = (() => {
return [{
name: 'beijing',
id: '1'
},
{
name: 'shanghai',
id: '2'
},
{
name: 'guangzhou',
id: '3'
},
{
name: 'shenzhen',
id: '4'
},
]
})()
// 这时候需求更改,但是老接口的数据格式又不能更改,那就只能通过适配器来实现新老接口兼容处理
// 例如新需求需要这样的数据格式
let newCityList = [{
'beijing': 1,
'shanghai': 2,
'guangzhou': 2,
'shenzhen': 2,
}]
let adapter = (oldCityList) => {
let res = {};
oldCityList.forEach(city => {
res[city.name] = city.id;
});
return res;
}
console.log(adapter(oldCityList))
通过adapter适配器将老的接口格式更改为我们需要的数据格式,这就是适配器的强大之处
实战案例4:接口转换
适配器模式的实现非常简单,就是在client对target进行调用时, target内部adptee类进行了调用
UML类图:
class Adaptee{
specificRequest() {
return '德国标准插头'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
request() {
let info = this.adaptee.specificRequest()
return `${info}->中国标准插头`
}
}
class Client {
constructor() {
this.target = new Target()
}
transform() {
return this.target.request()
}
}
const client = new Client()
console.log(client.transform()) // 德国标准插头->中国标准插头
&前段时间在做Echarts的图表柱状图的X轴渲染时就遇到一个使用适配器模式解决的问题
/*我们都知道很多UI组件或者工具库会按指定的数据格式进行渲染, 但是这个时候后端是不知道的;
所以可能接口出来的数据我们是不能直接正常的在页面上渲染的, 而此时老板催促我们赶紧上线, 而后端坚持认为数据格式没问题, 坚决不修改;
这个时候我们可以通过适配器模式来前端格式化数据;*/
// 后端返回的json数据格式:
[{
"day": "周一",
"uv": 6300
}, {
"day": "周二",
"uv": 7100
}, {
"day": "周三",
"uv": 4300
}, {
"day": "周四",
"uv": 3300
}, {
"day": "周五",
"uv": 8300
}, {
"day": "周六",
"uv": 9300
}, {
"day": "周日",
"uv": 11300
}]
// Echarts图表图形需要的数据格式:
//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);
}
// 创建两个函数分别对数据按照echarts所需要的数据格式进行格式化处理即可解决问题;这两个方法其实就是一个适配器,
// 把指定的数据丢进去即可按照指定规则输出我们期待得到的数据格式;
Axios 是比较热门的网络请求库,在浏览器中使用的时候,Axios 的用来发送请求的 adapter 本质上是封装浏览器提供的 API XMLHttpRequest,我们可以看看源码中是如何封装这个 API 的,为了方便观看,进行了一些省略:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data
var requestHeaders = config.headers
var request = new XMLHttpRequest()
// 初始化一个请求
request.open(config.method.toUpperCase(),
buildURL(config.url, config.params, config.paramsSerializer), true)
// 设置最大超时时间
request.timeout = config.timeout
// readyState 属性发生变化时的回调
request.onreadystatechange = function handleLoad() { ... }
// 浏览器请求退出时的回调
request.onabort = function handleAbort() { ... }
// 当请求报错时的回调
request.onerror = function handleError() { ... }
// 当请求超时调用的回调
request.ontimeout = function handleTimeout() { ... }
// 设置HTTP请求头的值
if ('setRequestHeader' in request) {
request.setRequestHeader(key, val)
}
// 跨域的请求是否应该使用证书
if (config.withCredentials) {
request.withCredentials = true
}
// 响应类型
if (config.responseType) {
request.responseType = config.responseType
}
// 发送请求
request.send(requestData)
})
}
可以看到这个模块主要是对请求头、请求配置和一些回调的设置,并没有对原生的 API 有改动,所以也可以在其他地方正常使用。这个适配器可以看作是对 XMLHttpRequest 的适配,是用户对 Axios 调用层到原生 XMLHttpRequest 这个 API 之间的适配层。
源码可以参见 Github 仓库: axios/lib/adapters/xhr.js
总结
如果有以下情况出现时,建议使用适配器模式:
1、使用一个已经存在的对象,但其方法或属性接口不符合你的要求。
2、你想创建一个可复用的对象,该对象可以与其它不相关的对象或不可见对象(即接口方法或属性不兼容的对象)协同工作。
3、想使用已经存在的对象,但是不能对每一个都进行原型继承以匹配它的接口。对象适配器可以适配它的父对象接口方法或属性。
4、需要一个统一的输出接口,但是输入类型却不可预知。
适配器模式与其他设计模式的区别
适配器模式不同于装饰者模式和代理模式,装饰者模式是为了个对象增加功能,代理模式是为了控制对象的访问,也不同于外观模式,外观模式是定义了一个新的接口。
适配器模式与代理模式
适配器模式: 提供一个不一样的接口,由于原来的接口格式不能用了,提供新的接口以满足新场景下的需求;
代理模式: 提供一模一样的接口,由于不能直接访问目标对象,找个代理来帮忙访问,使用者可以就像访问目标对象一样来访问代理对象;
适配器模式、装饰者模式与代理模式
适配器模式: 功能不变,只转换了原有接口访问格式;
装饰者模式: 扩展功能,原有功能不变且可直接使用;
代理模式: 原有功能不变,但一般是经过限制访问的;
今天的学习就到这里,你可以使用今天学习的技巧来改善一下你曾经的代码,如果想继续提高,欢迎关注我,每天学习进步一点点,就是领先的开始。如果觉得本文对你有帮助的话,可以点个红心哟!么么哒
本文主要参考《JavaScript 设计模式与开发实践》