适配器在生活中很常见,例如电源适配器、USB串口转接设备等,它们本质上是完成接口转换的功能。在编程领域上,模拟适配器完成接口转换的一种解决方案叫做适配器模式。
在编程中免不了使用别人的API,如果别人提供的API与自己期待的API存在不同的话,可以使用适配器模式把别人提供的API转换成自己的所期待的API,这样一来别人的代码和自己的代码都不需要变动。再者,日后API有所变动的话,只需要更改或替换适配器即可,这样的解决方案符合开闭原则,能够大大提高代码的可维护性。
在适配器模式中有三个角色,target
即所期待的使用对象,也是适配器所转换的目标,adapter
即适配器,完成接口转换的功能,adaptee
即被转换的对象。具体的实现可以通过继承或对象委派来实现,通过继承实现的叫做类适配器,通过对象委派实现的叫做对象适配器,而在JS中往往通过一个函数就可以完成接口的转换功能。
通过继承实现适配器
下面是一个第三方库:
class Painter {
rect() {
console.log('画矩形')
}
circle() {
console.log('画圆形')
}
}
module.exports = Painter
而使用者MyPainter
期望的API如下:
interface IMyPainter {
rect():void;
circle():void;
line():void; // 希望有一个画直线的方法
}
由于第三方API没有提供画直线方法,但是又想使用第三方库的画矩形和画圆形的方法,所以写一个适配器来扩展第三方的API完成适配:
class PainterAdapter extends Painter {
line() {
console.log('画直线')
}
}
使用时:
const myPainter = new PainterAdapter()
myPainter.line();
myPainter.rect();
通过对象委派实现适配器
假设使用者MyPainter
期望的API如下:
interface IMyPainter {
drawRect():void;
drawCircle():void;
}
可以看到同样的功能在第三方库已有实现,但是API不太一样,所以可以使用对象委派的方式适配第三方库
class PainterAdapter {
constructor() {
this._painter = new Painter()
}
drawRect(...args) {
return this._painter.rect(...args)
}
drawCircle(...args) {
return this._painter.circle(...args)
}
}
使用时
const myPainter = new PainterAdapter()
myPainter.drawRect()
myPainter.drawCircle()
缺省适配器
当你对别人提供的API或数据不太放心的时候,可以使用缺省适配器为别人提供的API或数据设置一个默认值,这样就不怕别人提供的格式不规范导致代码出错。
假设你希望使用的接口还是下面这个
interface IMyPainter {
rect():void;
circle():void;
line():void; // 希望有一个画直线的方法
}
但是你不确定第三方API是否真的提供了相应的方法,你可以写一个缺省适配器使得你使用的时候总有一个默认方法:
class DefaultPainterAdapter extends Painter {
constructor() {
super()
const _defaultImplement = {
line() { console.log('default draw line') }
}
Object.keys(_defaultImplement).forEach((m) => {
this[m] = this[m] || _defaultImplement[m]
})
}
}
接下来就可以放心使用了:
const myPainter = new DefaultPainterAdapter()
myPainter.line()
myPainter.circle()
总结
适配器模式是比较简单也比较常用的设计模式,它的作用是为API的提供者和使用者完成接口转换,但是也增加了代码量。API的提供者与使用者应该尽量保持一致来避免这种额外的开销。
与外观模式的区别
适配器模式关注的是接口转换,使得API的提供者和使用者不必关心对方的实现,体现的是解耦的思想;而外观模式关注的是让API保持统一的接口给API的使用者使用,体现的是封装的思想。