一、定义
适配器模式可用来在现有接口和不兼容的类之间进行匹配。使用这种模式的对象又叫包装器(wrapper),因为它们是在用一个新的接口包装另一个对象。在设计类的时候旺旺会遇到有些接口不能与现有API一同使用的情况。借助于适配器,你不用直接修改这些类也能使用它们。
适配器可以被添加到现有代码中以协调两个不同的接口。如果现有代码的接口能很好的满足需要,那就可能没有必要使用适配器。但要是现有接口对于手头的工作来说不够直观或实用,那么可以使用适配器来提供一个更简化或更丰富的接口。
var clientObject = { string1: 'foo', string2: 'bar', string3: 'baz' }; function interfaceMethod(str1, str2, str3) { // ... } // 为了把clientObject作为参数传递给interfaceMethod,需要用到适配器。 function clientToInterfaceAdapter(o) { interfaceMethod(o.string1, o.string2, o.string3) } // 现在就可以把整个对象传递给这个函数 clientToInterfaceAdapter(clientObject);
clientToInterfaceAdapter函数的作用就在于对interfaceMethod函数进行包装,并把传递给它的参数转换为后者需要的形式。
在这里,嵌入进大叔的鸭子与火鸡的例子。
鸭子(Dock)有飞(fly)和嘎嘎叫(quack)的行为,而火鸡虽然也有飞(fly)的行为,但是其叫声是咯咯的(gobble)。如果你非要火鸡也要实现嘎嘎叫(quack)这个动作,那我们可以复用鸭子的quack方法,但是具体的叫还应该是咯咯的,此时,我们就可以创建一个火鸡的适配器,以便让火鸡也支持quack方法,其内部还是要调用gobble。
//鸭子 var Duck = function(){ }; Duck.prototype.fly = function(){ throw new Error("该方法必须被重写!"); }; Duck.prototype.quack = function(){ throw new Error("该方法必须被重写!"); }//火鸡 var Turkey = function(){ }; Turkey.prototype.fly = function(){ throw new Error(" 该方法必须被重写 !"); }; // 火鸡——咯咯地叫 Turkey.prototype.gobble = function(){ throw new Error(" 该方法必须被重写 !"); };//具体的鸭子 var MallardDuck = function () { Duck.apply(this); }; MallardDuck.prototype = new Duck(); //原型是Duck MallardDuck.prototype.fly = function () { console.log("可以飞翔很长的距离!"); }; // 鸭子——嘎嘎叫 MallardDuck.prototype.quack = function () { console.log("嘎嘎!嘎嘎!"); };// 具体的火鸡 var WildTurkey = function () { Turkey.apply(this); }; WildTurkey.prototype = new Turkey(); //原型是Turkey WildTurkey.prototype.fly = function () { console.log("飞翔的距离貌似有点短!"); }; WildTurkey.prototype.gobble = function () { console.log("咯咯!咯咯!"); };// 让火鸡也支持quack方法的适配器 var TurkeyAdapter = function(oTurkey) { Duck.apply(this); this.oTurkey = oTurkey; }; TurkeyAdapter.prototype = new Duck(); TurkeyAdapter.prototype.fly = function() { // 调用火鸡的飞方法 this.oTurkey.fly(); }; // 让火鸡也有鸭子的方法 TurkeyAdapter.prototype.quack = function() { this.oTurkey.gobble(); }// 调用 var oMallardDuck = new MallardDuck(); oMallardDuck.fly(); // 可以飞翔很长的距离! oMallardDuck.quack(); // 咯咯!咯咯! var oWildTurkey = new WildTurkey(); oWildTurkey.fly(); // 飞翔的距离貌似有点短! oWildTurkey.gobble(); // 咯咯!咯咯! var oTurkeyAdapter = new TurkeyAdapter(oWildTurkey); // 调用原有的火鸡的飞方法 oTurkeyAdapter.fly(); // 飞翔的距离貌似有点短! // 调用鸭子特有的嘎嘎叫的方法 - 从而达到适配的目的 oTurkeyAdapter.quack(); // 咯咯!咯咯!这样,有了TurkeyAdapter适配器,使得具体的实例对象oWildTurkey同样具有了Duck具有的方法quack。
二、适用场景
适配器适用于客户系统期待的接口与现有API提供的接口不兼容这种场合。它只能用来协调语法上的差异问题。适配器所适配的两个方法执行的应该是类似的任务,否则的话它就解决不了问题。如果客户想要的是一个不同的接口,比如说一个他们用起来更容易一些的接口,那么也可以为此设计适配器。就像桥接元素和门面元素一样,通过创建适配器,可以把抽象与其实现隔离开来,以便两者独立变化。
在这里,引入进大叔博客深入理解JavaScript系列(39):设计模式之适配器模式中适用场景的概括——
1.使用一个已经存在的对象,但其方法或属性接口不符合你的要求;
2.你想创建一个可复用的对象,该对象可以与其它不相关的对象或不可见对象(即接口方法或属性不兼容的对象)协同工作;
3.想使用已经存在的对象,但是不能对每一个都进行原型继承以匹配它的接口。对象适配器可以适配它的父对象接口方法或属性。
三、优势
适配器有助于避免大规模改写现有客户代码。其工作机制是:用一个新的接口独享对现有类的接口进行包装,这样客户程序就能使用这个并非为其量身打造的类而又勿需为此大动手脚。
四、劣势
1.可能有些工程师不想使用适配器,其原因主要在于他们实际上需要彻底重写代码。有人认为适配器是一种不必要的开销,完全可以通过重写现有代码避免。
2.适配器模式也会引入一批需要支持的新工具
3.如果现有API还未定形,或者新接口还未定型(这更有可能),那么适配器可能不会一直管用。
五、与其他模式的区别
1.与门面模式的区别:从表面上看,适配器模式很像门面模式。它们都要对别的对象进行包装并改变其呈现的接口。二者的差别在于如何改变接口。门面元素展现的是一个简化的接口,它并不提供额外的选择,而且有时为了方便完成常见任务它还会做出一些假设。而适配器则要把一个接口转换为另一个接口,它并不会滤除某些能力,也不会简化接口。如果客户端系统期待的接口不可用,那就需要用到适配器。
2.与桥接模式的区别:适配器和桥接模式虽然类似,但桥接的出发点不同,桥接的目的是将接口部分和实现部分分离,从而对他们可以更为容易也相对独立的加以改变。而适配器则意味着改变一个已有对象的接口。
3.与装饰者模式的区别:装饰者模式增强了其它对象的功能而同时又不改变它的接口,因此它对应程序的透明性比适配器要好,其结果是装饰者支持递归组合,而纯粹使用适配器则是不可能的。
4.与代理模式的区别:代理模式在不改变它的接口的条件下,为另外一个对象定义了一个代理。
六、小结
适配器模式是一种很有用的技术,它可以用来对类和对象进行包装,以便向客户代码提供其期待的接口。应用这种技术,你可以在不影响现有实现的前提下利用新的更好的接口。作为一个实现者,你可以根据自己的需要定制接口。这种模式的确会引入一些新代码,不过,在涉及大型系统和遗留框架的情况下,它的优点往往比缺点更突出。
源自:JavaScript设计模式(人民邮电出版社)——第十一章,适配器模式