适配器设计模式在JavaScript中非常有用。在处理跨浏览器兼容问题和集成多个第三方SDK调用上可以看到。其实在日常开发中,很多时候你会不经意地写出符合某种设计模式的代码。设计模式毕竟是一些可以帮助提高开发效率的模板,来源于日常开发。其实适配器在JavaScript中应该是比较常见的。在维基百科中,适配器模式的定义是:在软件工程中,适配器模式是一种软件设计模式,允许从另一个接口使用现有类的接口。它通常用于使现有类与其他类一起工作,而无需修改它们的源代码。日常生活中的例子日常生活中最常见的就是电源插头的适配器。世界各国的插座标准各不相同。如果你需要按照每个国家的标准购买相应的电源插头,那就太浪费钱了。如果自己带插座的话,去砸别人家的墙重新接线肯定是不现实的。所以就会有插头的适配器,用来将某种类型的插头转换成另一种类型的插头。在插座和电源之间传输的是适配器。体现在代码中,转向编程。我个人的理解是这样的:把不想看到的脏代码隐藏起来,可以说这是一个对接多个第三方SDK的适配器。以日常开发为例,我们正在开发一个微信♂开发,其中使用了微信支付模块。经过长时间的联调,我们终于跑通了整个流程。就在你准备愉快的打包上线代码的时候,你又得到了一个新的需求:我们需要接入支付宝的SDK公众号,还要有一个支付流程。为了复用代码,我们可以在脚本中写这样的逻辑:if(platform==='wechat'){wx.pay(config)}elseif(platform==='alipay'){config)}//做一些后续的逻辑处理但是一般来说,各个工厂的SDK提供的接口调用方法或多或少都会有一些差异,虽然有时候可能会用到同一个文档,向朋友们致敬.所以上面的代码可能是这样的://不是真正的参数配置,只是示例constconfig={price:10,goodsId:1}//返回值也可能有不同的处理方式if(platform==='wechat'){config.appId='XXX'config.secretKey='XXX'wx.pay(config).then((err,data)=>{if(err)//错误//成功})}elseif(platform==='alipay'){config.token='XXX'alipay.pay(config,data=>{//success},err=>{//error})}现在,代码界面还是很清晰的,只要写注释,这样的代码还算不错。但是生活总是充满惊喜,我们接到要求添加QQ的SDK,美团的SDK,小米的SDK,或者某家银行的SDK。此时你的代码可能是这样的:switch(platform){case'wechat'//微信break的处理逻辑case'QQ'//QQbreak的处理逻辑case'alipay'//支付宝的处理逻辑breakcase'meituan'://美团的处理逻辑breakcase'xiaomi'://小米的处理逻辑break}这已经不是一些注释可以弥补的问题了,这样的代码会越来越难维护,各种SDK调用方式奇怪。如果其他人有类似的需求,需要重写这样的代码,那一定是一种资源的浪费。所以,为了保证我们业务逻辑的清晰,也为了避免后人重复踩这个陷阱,我们将其拆分为一个publicfunction:找到其中一个SDK或者我们约定好的SDK的调用方法规则作为基准。让我们告诉调用者怎么做,如何获取返回的数据,然后我们在函数内部进行各种肮脏的判断:functionpay({price,goodsId}){returnnewPromise((resolve,reject)=>{constconfig={}switch(platform){case'wechat'://微信处理逻辑config.price=priceconfig.goodsId=goodsIdconfig.appId='XXX'config.secretKey='XXX'wx.pay(config).then((err,data)=>{if(err)returnreject(err)resolve(data)})breakcase'QQ'://QQ处理逻辑config.price=price*100config.gid=goodsIdconfig.appId='XXX'config.secretKey='XXX'config.success=resolveconfig.error=rejectqq.pay(config)breakcase'alipay'://支付宝的处理逻辑config.payment=priceconfig.id=goodsIdconfig.token='XXX'alipay.pay(config,resolve,reject)break}})}这样无论我们在什么环境下,只要我们的adapter支持,我们就可以按照我们的约定来了调用的一般规则,以及SDK的具体实现,是Adapter需要关心的事情://runanywhereawaitpay({price:10,goodsId:1})对于SDK提供者来说,它只需要知道它需要的一些参数,然后按照自己的方式返回数据即可。对于SDK呼叫室,我们只需要约定好的通用参数,按照约定的方式进行监听和回调处理即可。集成多个第三方SDK的任务就交给了适配器,然后我们把适配器的代码压缩混淆,放在看不见的角落,这样代码逻辑就会变得很清晰:)。适配器的作用大致就是这样,有一点必须明确,适配器不是灵丹妙药,__那些繁琐的代码总是存在的,只是你写业务的时候看不出来__,看不见的心别烦。其他的一些例子我个人认为jQuery里面有很多适配器的例子,包括最基本的$('selector').on。这不是一个明显的适配器模式吗?一步步降级,抹平部分浏览器的差异,让我们可以在主流浏览器中使用simpleon监听事件://一个简单的伪代码示例functionon(target,event,callback){if(target.addEventListener){//标准事件监听方法target.addEventListener(event,callback)}elseif(target.attachEvent){//IE低版本监听方法target.attachEvent(event,callback))}else{//一些低版本级别的浏览器监听事件target[`on${event}`]=callback}}或者Node中这样的例子比较多,因为早年没有Promise,所以大部分的异步都是通过回调来完成的,而有一个约定的规则,Error-firstcallback:constfs=require('fs')fs.readFile('test.txt',(err,data)=>{if(err)//Handleexceptions//Handlecorrect结果})而我们的新功能都是通过async/await的方式实现的。当我们需要复用旧项目中的某些功能时,必须直接修改旧项目的代码是不可行的。这样的兼容性处理需要调用者来完成,所以为了让逻辑代码看起来不会太混乱,我们可以把这样的回调转换成Promise版本供我们调用:constfs=require('fs')functionreadFile(fileName){returnnewPromise((resolve,reject)=>{fs.readFile(fileName,(err,data)=>{if(err)reject(err)resolve(data)})})}awaitreadFile('test.txt')正如前面所说,这个Error-first回调是一种约定形式,因此我们可以很容易地实现一个通用的适配器:functionpromisify(func){return(...args)=>newPromise((resolve,reject)=>{func(...args,(err,data)=>{if(err)reject(err)resolve(data)})})}然后对应使用前的转换即可执行代码中我们期望的方式:constfs=require('fs')constreadFile=promisify(fs.readFile)awaitreadFile('test.txt')在Node8中,官方实现是这样实现的工具函数:util.promisify个人总结观点:所有的设计模式都不是凭空想象出来的。他们一定是在开发过程中总结和提炼一些高效的方法,这意味着你可能不需要他们。最开始只是啃这些各种名字高大上的设计模式。因为书中提到的场景可能并不全面,某些语言可能有更好的解决方案,所以死记硬背未必能写出深情的代码:)纸上写来总是肤浅的,但从来不知道这些,你必须做它。————《冬夜读书示子聿》,陆游
