JS设计模式之适配器模式

# 什么是适配器模式?

适配器模式:为多个不兼容接口之间提供“转化器”。即解决两个接口之间不匹配的问题。

它的实现非常简单,检查接口的数据,进行过滤、重组等操作,使另一接口可以使用数据即可。不考虑这些接口与是怎样实现的,也不考虑的将来是如何演化,不需要改变已有的接口,就能使其协同作用。

# 应用场景

当数据不符合使用规则,就可以借助此种模式进行格式转化。

# 代码实现案例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);
image.png

虽然三个音乐平台的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))
image.png

通过adapter适配器将老的接口格式更改为我们需要的数据格式,这就是适配器的强大之处

实战案例4:接口转换

适配器模式的实现非常简单,就是在client对target进行调用时, target内部adptee类进行了调用
UML类图:


image.png
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 设计模式与开发实践》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357