适配器模式可以用来在现在接口和不兼容的类之间进行适配。
使用这种模式的对象又叫包装器,因为他们是在用一个新接口包装另一个对象。
在设计类的时候往往遇到有些接口不能与现有api一同使用的情况,借助于适配器,你可以不用直接修改这些类也能使用他们。
适配器的特点:
适配器可以被添加到现有代码中以协调俩个不同的接口。从表面上来看,适配器模式很像门面模式,他们都要对别的对象进行包装并改变其呈现的接口,二者之间的差别在于他们如何改变接口。门面元素展现的是一个简化接口,它并不提供额外的选择,而且有时是为了方便完成常见任务它还会做一些假定。而适配器模式则要把一个接口转换为另一个接口,它并不会滤除某些能力,也不会简化接口,如果客户系统期待的api不可用,那么就需要用到适配器模式。
适配器可被实现为不兼容的方法调用之间的一个代码薄层,如果你有一个具有三个字符串参数的函数,但客户系统拥有的却是一个包含三个字符串元素的数组,这时候就可以用一个适配器来衔接二者。
现在看看Prototype库和YUI的get方法转换,这俩个函数的功能比较相似,不过先看看二者之间接口的差别。
//Prototype $ function function $(){ var elements = new Array(); for(var i=0;i<arguments.length;i++){ var element = arguments[i]; if(typeof element == 'string'){ element = document.getElementById(element); } if(arguments.length==1){ return element; } elements.push(element); } return elements; } //YUI get method YAHOO.util.Dom.get = function(el){ if(YAHOO.lang.isString(el)){ return document.getElementById("el"); } if(YAHOO.lang.isArray(el)){ var c = []; for(var i=0,len=el.length;i<len;++i){ c[c.length]=YAHOO.util.Dom.get(el[i]); } return c; } if(el){ return el; } return null; }
而适配器的实现十分简单:
function PrototypeToYUIAdapter(){ return YAHOO.util.Dom.get(arguments); } function YUIToPrototypeAdapter(){ return $.apply(window,el instanceof Array?el:[el]); }
Prototype 想用YUI的话,只需要 $ = PrototypeToYUIAdapter即可;相反,则YAHOO.util.Dom.get = YUIToPrototypeAdapter 即可。
适配电子邮件API:
下面这是一个替换函数,很多HTML模板的实现也是这个原理:
var DED = {}; DED.util = { substitute:function(s,o){ return s.replace(/{([^{}]*)}/g,function(a,b){ var r = o[b]; return typeof r==='string' || typeof r==='number'?r:a; }); } }
具体实现:
var deMail = (function(){ function request(id,type,callback){ DED.util.asyncRequest( "GET", "mail.php", function(o){ callback(o.responseText); } ); } return { getMail:function(id,callback){ request(id,'all',callback); }, sendMail:function(id){ //..... }, save:function(id){ //... }, move:function(id,des){ //.... }, archive:function(id){ //... }, trash:function(id){ //... }, resportSpam:function(id){ //... }, formatMessage:function(e){ var e = e || window.event; try{ e.preventDefault() }catch(e){ e.returnValue = false; } var targrtEl = e.target || e.srcElement; var id = targrtEl.id.toString().split('-')[1]; deMail.getMail(id,function(msgObject){ var resp = eval('('+msgObject+')'); var details = '<p>{from}'; details+='{date}'; details+='{message}</p>'; $('message-pane').innerHTML = DED.util.substitute(details,resp); }) } } })() addEvent(widow,'load',function(){ var threads = getElementsByClass('thread','a'); for (var i=0,len=threads.length;i<len;++i) { addEvent(threads[i],'click',formatMessage) } })
程序创建完成,但是别的小组那边的人已经使用原来的fooMail系统实现了他们的代码,问题是,他们的方法要求提供的是HTML片段,其构造函数也只接受一个ID,而且他们的getMail只有回调函数这一个参数,而且,他们不想改写全部代码,于是我们决定,要有适配器。
下面我们要从fooMail转到dedMail。
在充分料及了提供方和接收方的情况后,你可以截取来自提供方的逻辑,然后用接收方能够理解的方式对其转换。
先看下fooMail这个api的代码:
fooMail.getMail(function(text){ $('message-pane').innerHTML = text; });
注意,getMail方法以一个回调方法为参数,这个回调函数被调用时得到的参数是包含着发信人姓名,日期和内容的一段文本。这不算理想,但是fooMail的工程师不想冒着破换系统的危险进行修改,我们就可以为他们写一个简单的适配器,这样他们就不必改变自己的原有代码。
var deMailtoFooMailAdapter = {}; deMailtoFooMailAdapter.getMail = function(id,callback){ deMail.getMail(id,function(resp){ var resp = eval('('+resp+')'); var details = '<p>{from}'; details+='{date}'; details+='{message}</p>'; callback(DED.util.substitute(details,resp)); }) } fooMail = deMailtoFooMailAdapter;
这段代码中用 deMailtoFooMailAdapter 这个单体对象改写了fooMail对象,该单体对象实现了一个getMail方法,这个方法内部在调用哪个回调函数时会把一段HTML文本作为参数正确的传给它。
适配器适用于客户系统期待的接口与现有API提供的接口不兼容这种场合。它只能用来协调语法上的差异。适配器所适应的俩个方法执行的应该是类似的任务,否则的话它就无法解决问题。如果客户想要一个不同的接口,比如说一个他们用起来更容易一些的接口,那么也可以为此而使用适配器。就像桥接元素和门面元素一样,通过创建适配器,就可以把抽象与实现隔离开来,以便二者独立变化。
适配器的好处有助于避免大规模改写现有客户端代码,其工作机制就是用一个新的接口对现有类的接口进行包装,这个客户程序就能使用这个并非为其量身打造的类而又无需为此大动手术。