1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title></title> 5 <meta charset="utf-8"> 6 </head> 7 <body> 8 <script> 9 /** 10 * 适配器模式 11 * 12 * 定义: 13 * 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 14 * 15 * 本质: 16 * 转换匹配,复用功能 17 * 18 * 适配器模式可用来在现有接口和不兼容的类之间进行适配。使用这种模式的对象又叫包装器,因为它们是在用一个新的接口包装另一个对象。许多时候创建适配器对程序员和接口的设计人员都有好处。在设计类的时候往往会遇到有些接口不能与现有API一同使用的情况。借助于适配器,你不用直接修改这些类也能使用它们。在设计大型系统和遗留框架的情况下,他的优点往往比缺点更突出。 19 * 20 * 适配器的特点 21 * 22 * 适配器可以被添加到现有代码中以协调两个不同的接口。如果现有代码的接口能很好地满足需要,那就可能没有必要使用适配器。但要是现有接口对于手头的工作来说不够直观或实用,那么可以使用适配器来提供一个更简洁或更丰富的接口。 23 * 从表面上看,适配器模式很像门面模式。它们都要对别的对象进行包装并改变其呈现的接口。二者的差别在于它们如何改变接口。门面元素展现的是一个简化的接口,它并不提供额外的选择,而且有时为了方便完成常见任务它还会做出一些假定。而适配器则要把一个接口转换为另一个接口,它并不会滤除某些能力,也不会简化接口。如果客户系统期待的API不可用,那就需要用到适配器。 24 * 25 * 应用适配器模式来解决问题的思路 26 * 一般问题的根源在于接口的不兼容,功能是基本实现了,也就是说,只要让两边的接口匹配起来,就可以复用第一版的功能了。 27 * 按照适配器模式的实现方式,可以定义一个类来实现第二版的接口,然后在内部实现的时候,转调第一版已经实现了的功能,这样就可以通过对象组合的方式,既复用了第一版已有的功能,同时又在接口上满足了第二版调用的需求, 28 */ 29 30 // 例子 31 // 已经存在的接口,这个接口需要被适配 32 var Adaptee = function(){}; 33 Adaptee.prototype.specificRequest = function(){ 34 // 具体功能实现 35 }; 36 37 // 适配器 38 // 构造器,传入需要被适配的对象 39 var Adapter = function(adaptee){ 40 this.adaptee = adaptee; 41 }; 42 Adapter.prototype = { 43 request: function(){ 44 // do sth 45 // 可能转调已经实现了的方法,进行适配 46 this.adaptee.specificRequest(); 47 // other thing 48 } 49 }; 50 51 var adaptee = new Adaptee(); 52 var target = new Adapter(adaptee); 53 target.request(); 54 55 /* 56 Adaptee和Target的关系 57 58 适配器模式中被适配的接口Adaptee和适配成为的接口Target是没有关联的,也就是说,Adaptee和Target中的方法既可以相同,也可以不同。极端情况下两个接口里面的方法可能是完全不同的,也可能完全相同。 59 60 对象组合 61 62 适配器的实现方式其实是依靠对象组合的方式。通过给适配器对象组合被适配的对象,然后当客户端调用Target的时候,适配器会把相应的功能委托给被适配对象去完成。 63 64 适配器模式的调用顺序 65 66 target--Adapter--Adaptee 67 */ 68 69 /* 70 适配器的常见实现 71 72 在实现适配器的时候,适配器通常是一个类,一般会让适配器类去实现Target接口,然后在适配器的具体实现里面调用Adaptee。也就是说适配器通常是一个Target类型,而不是Adaptee类型。 73 74 智能适配器 75 76 适配器也可以实现一些Adaptee没有实现,但是在Target中定义的的功能。这种情况就需要在适配器的实现里面,加入新功能的实现。这种适配器被称为智能适配器 77 如果要使用智能适配器,一般新加入功能的实现会用到很多Adaptee的功能,相当于利用Adaptee的功能来实现更高层的功能。当然也可以完全实现新加入的功能,和已有的功能都不相关,变相地扩展了功能。 78 79 适配多个Adaptee 80 81 适配器在适配的时候,可以适配多个Adaptee,也就是说实现某个新的Target的功能的时候,需要调用多个模块的功能,适配多个模块的功能才能满足新接口的要求。 82 83 适配器Adapter实现的复杂程度 84 85 取决于Target和Adaptee的相似程度。 86 87 缺省适配 88 89 为一个接口提供缺省实现。有了它,就不用直接去实现接口,而是采用集成这个缺省适配对象,从而让子类可以有选择地去覆盖实现需要的方法,对于不需要的方法,使用缺省适配的方法就可以了。 90 91 双向适配器 92 93 适配器也可以实现双向的适配,既可以把Adaptee适配成为Target,也可以把Target适配成为Adaptee。也就是说这个适配器可以同时当作Target和Adaptee使用。 94 在两个不同的客户需要用不用的方式查看同一个对象时,适合使用双向适配器。 95 96 */ 97 98 /* 99 对象适配器的实现: 依赖于对象的组合。。 100 101 类适配器的实现: 采用多重继承对一个接口与另一个接口进行适配。 102 */ 103 104 /* 105 如果你有一个具有3个字符串参数的函数,但客户系统拥有的却是一个包含三个字符串元素的对象或数组,此时就可以用一个是配起来衔接二者。 106 */ 107 var clientObject = { 108 string1: 'foo', 109 string2: 'bar', 110 string3: 'baz' 111 }; 112 function interfaceMethod(str1, str2, str3) { 113 //... 114 } 115 // 适配器 116 function clientToInterfaceAdapter(o) { 117 interfaceMethod(o.string1, o.string2, o.string3); 118 } 119 clientToInterfaceAdapter(clientObject); 120 121 122 /* 123 适配原有实现 124 125 在某些情况下,从客户一方对代码进行修改是不可能的。有些程序员因此索性避免创建API。如果现有接口发生了改变,那么客户代码也必须进行相应的修改后才能使用这个新接口,否则整个应用系统就有失灵的危险。在引入新接口之后,一般说来最好向客户方提供一些可为其实现新接口的适配器。 126 */ 127 128 // 示例:适配两个库 129 // 下面的例子要实现的是从Prototype库的$函数到YUI的get方法的转换 130 // 先看看他们在接口方面的差异 131 132 // Prototype $ function 133 function $() { 134 var elements = []; 135 for (var i = 0; i < arguments.length; i++) { 136 var element = arguments[i]; 137 if (typeof element === 'string') { 138 element = document.getElementById(element); 139 } 140 if (arguments.length === 1) { 141 return element; 142 } 143 elements.push(element); 144 } 145 return elements; 146 } 147 148 // YUI get method 149 YAHOO.util.Dom.get = function (el) { 150 if (YAHOO.lang.isString(el)) { 151 return document.getElementById(el); 152 } 153 if (YAHOO.lang.isArray(el)) { 154 var c = []; 155 for (var i = 0, len = el.length; i < len; ++i) { 156 c[c.length] = YAHOO.util.Dom.get(el[i]); 157 } 158 return c; 159 } 160 if (el) { 161 return el; 162 } 163 return null; 164 }; 165 166 // Adapter 167 function PrototypeToYUIAdapter() { 168 return YAHOO.util.Dom.get(arguments); 169 } 170 function YUIToPrototypeAdapter(el) { 171 return $.apply(window, el instanceof Array ? el : [el]); 172 } 173 174 // 有了这些适配器,现有的客户系统就可以继续使用其熟悉的API。 175 $ = PrototypeToYUIAdapter; 176 // or 177 YAHOO.util.Dom.get = YUIToPrototypeAdapter; 178 179 180 // 示例:适配电子邮件API 181 /* 182 本例研究的是一个web邮件API,它可以用来接收,发送邮件并执行一些别的任务。我们将采用类Ajax技术从服务器或取消息,然后将消息详情载入DOM。 183 */ 184 /* 185 <!DOCTYPE html> 186 <html> 187 <head> 188 <title>Mail API Demonstration</title> 189 <meta charset="utf-8"> 190 <style> 191 body { 192 font: 62.5% georgia, times, serif; 193 } 194 #doc { 195 margin: 0 auto; 196 500px; 197 font-size: 1.3em; 198 } 199 </style> 200 </head> 201 <body> 202 <div id="doc"> 203 <h1>Email Application Interface</h1> 204 <ul> 205 <li><a class="thread" id="msg-1" href="#">load message Sister Sonya</a></li> 206 <li><a class="thread" id="msg-2" href="#">load message Lindsey Simon</a></li> 207 <li><a class="thread" id="msg-3" href="#">load message Margaret Stoooart</a></li> 208 </ul> 209 <div id="message-pane"></div> 210 </div> 211 212 <script src="Library.js"></ script> 213 < script> 214 */ 215 // application utilities 216 var DED = {}; 217 DED.util = { 218 substitute: function (s, o) { 219 return s.replace(/\{([^\{\}]*)\}/g, function (a, b) { 220 var r = o[b]; 221 return typeof r === 'string' || typeof r === 'number' ? r : a; 222 }); 223 }, 224 asyncRequest: (function () { 225 function handleReadyState(o, callback) { 226 o.onreadystatechange = function () { 227 if (o && o.readyState === 4) { 228 if ((o.status >= 200 && o.status < 300) || o.status === 304) { 229 if (callback) { 230 callback(o); 231 } 232 } 233 } 234 }; 235 } 236 237 var getXHR = function () { 238 var http; 239 try { 240 http = new XMLHttpRequest(); 241 getXHR = function () { 242 return new XMLHttpRequest(); 243 }; 244 } catch (ex) { 245 var msxml = [ 246 'MSXML2.XMLHTTP.3.0', 247 'MSXML2.XMLHTTP', 248 'Microsoft.XMLHTTP' 249 ]; 250 for (var i = 0, len = msxml.length; i < len; i++) { 251 try { 252 http = new ActiveXObject(msxml[i]); 253 getXHR = function () { 254 return new ActiveXObject(getXHR.str); 255 }; 256 getXHR.str = msxml[i]; 257 } catch (ex) { 258 } 259 } 260 } 261 return http; 262 }; 263 264 return function (method, url, callback, postData) { 265 var http = getXHR(); 266 http.open(method, url, true); 267 handleReadyState(http, callback); 268 http.send(postData || null); 269 return http; 270 }; 271 })() 272 }; 273 274 // dedMail application interface 275 var dedMail = (function () { 276 function request(id, type, callback) { 277 DED.util.asyncRequest( 278 'GET', 279 'mail-api.php?ajax=true&id=' + id + '&type=' + type, 280 function (o) { 281 callback(o.responseText); 282 } 283 ); 284 } 285 286 return { 287 getMail: function (id, callback) { 288 request(id, 'all', callback); 289 }, 290 sendMail: function (body, recipient) { 291 // Send mail with body text to the supplied recipient 292 }, 293 save: function (id) { 294 // Save a draft copy with the supplied email ID. 295 }, 296 move: function (id, destination) { 297 // Move the email to the supplied destination folder. 298 }, 299 archive: function (id) { 300 // Archive the email.This can be a basic facade method that uses 301 // the move method, hard-coding the destination. 302 }, 303 trash: function (id) { 304 // This can also be a facade method which moves the message to 305 // the trash folder. 306 }, 307 reportSpam: function (id) { 308 // Move message to spam folder and add sender to the blacklist. 309 }, 310 formatMessage: function (e) { 311 var e = e || window.event; 312 if (e.preventDefault) { 313 e.preventDefault(); 314 } else { 315 e.returnValue = false; 316 } 317 var targetEl = e.target || e.srcElement; 318 var id = targetEl.id.toString().split('-')[1]; 319 dedMail.getMail(id, function (msgObject) { 320 var resp = eval('(' + msgObject + ')'); 321 var details = '<p><strong>From:</strong> {from}<br>'; 322 details += '<strong>Sent:</strong> {date}</p>'; 323 details += '<p><strong>Message:</strong><br>'; 324 details += '{message}</p>'; 325 $('message-pane').innerHTML = DED.util.substitute(details, resp); 326 }); 327 } 328 }; 329 })(); 330 331 // Set up mail implementation 332 addEvent(window, 'load', function () { 333 var threads = getElementsByClass('thread', document, 'a'); 334 for (var i = 0, len = threads.length; i < len; i++) { 335 addEvent(threads[i], 'click', dedMail.formatMessage); 336 } 337 }); 338 /* 339 </ script> 340 </body> 341 </html> 342 */ 343 /** 344 * 从fooMail转向dedMail 345 */ 346 // 先来看一段使用fooMail这个API的代码,该方法以一个回调方法为参数 347 fooMail.getMail(function (text) { 348 $('message-oane').innerHTML = text; 349 }); 350 351 // 适配器 352 var dedMailtoFooMailAdapter = {}; 353 dedMailtoFooMailAdapter.getMail = function (id, callback) { 354 dedMail.getMail(id, function (resp) { 355 resp = eval('(' + resp + ')'); 356 var details = '<p><strong>From:</strong> {from}<br>'; 357 details += '<strong>Sent:</strong> {data}</p>'; 358 callback(DED.util.substitute(details, resp)); 359 }); 360 }; 361 // Other methods needed to adapter dedMail to the fooMail interface. 362 //... 363 364 // Assign the adapter to the fooMail variable. 365 fooMail = dedMailtoFooMailAdapter; 366 367 368 /* 369 适配器模式的适用场合 370 371 适配器适用于客户系统期待的接口与现有API提供的接口不兼容这种场合。它只能用来协调语法上的差异问题。适配器所适配的两个方法执行的应该是类似的任务,否则的话它就解决不了问题。就像桥接元素和门面元素一样,通过创建适配器,可以把抽象与其实现隔离开来,以便二者独立变化。 372 */ 373 374 /* 375 适配器之利 376 377 适配器有助于避免大规模改写现有客户的代码。其工作机制是,用一个新的接口对现有类的接口进行包装。 378 379 适配器之弊 380 381 可能有些工程师不想使用适配器,其原因主要在于他们实际上需要彻底重写代码。有人认为适配器是一种不必要的开销,完全可以通过重写现有代码避免。此外适配器模式也会引入一批需要支持的新工具。如果现有API还未定形,捉着新接口还未定形,那么适配器可能不会一直管用。 382 */ 383 384 // http://www.joezimjs.com/javascript/javascript-design-patterns-adapter/ 385 var AjaxLogger = { 386 sendLog: function() { 387 var data = this.urlEncode(arguments); 388 389 jQuery.ajax({ 390 url: "http://example.com/log", 391 data: data 392 }); 393 }, 394 395 urlEncode: function(arg) { 396 //… 397 return encodedData; 398 } 399 //… 400 }; 401 402 var AjaxLoggerAdapter = { 403 log: function(arg) { 404 AjaxLogger.sendLog(arg); 405 } 406 }; 407 408 /* Adjust the LoggerFactory */ 409 410 var LoggerFactory = { 411 getLogger: function() { 412 // just gotta change what's returned here 413 return AjaxLoggerAdapter; 414 } 415 //… 416 }; 417 418 419 /* 420 相关模式 421 422 适配器模式与桥接模式 423 424 其实这两个模式除了结构略为相似外,功能上完全不同。 425 适配器模式是把两个或者多个接口的功能进行转换匹配;而桥接模式是让接口和实现部分相分离,以便它们可以相对于独立地变化。 426 427 适配器模式与装饰模式 428 429 适配器模式能模拟实现简单的装饰模式的功能,也就是为已有功能增添功能。 430 两个模式有一个很大的不同:一般适配器适配过后是需要改变接口的,如果不改接口就没有必要适配了;而装饰模式是不改变接口的,无论多少层装饰都是一个接口。因此装饰模式可以很容易地支持递归组合,而适配器就做不到,每次的接口不同,无法递归。 431 432 适配器模式与代理模式 433 434 适配器模式可以和代理模式组合使用。在实现适配器的时候,可以通过代理来调用Adaptee,这样可以获得更大的灵活性。 435 436 适配器模式与抽线工厂模式 437 438 在适配器实现的时候,通常需要得到被适配的对象。如果被适配的是一个接口,那么就可以结合一些可以创造对象实例的设计模式,来得到被适配的对象实例,比如抽象工厂模式,单例模式,工厂方法模式等。 439 440 */ 441 442 443 // http://www.dofactory.com/javascript-adapter-pattern.aspx 444 445 (function(){ 446 // old interface 447 function Shipping() { 448 this.request = function(zipStart, zipEnd, weight) { 449 // ... 450 return "$49.75"; 451 } 452 } 453 454 // new interface 455 function AdvancedShipping() { 456 this.login = function(credentials) { /* ... */ }; 457 this.setStart = function(start) { /* ... */ }; 458 this.setDestination = function(destination) { /* ... */ }; 459 this.calculate = function(weight) { return "$39.50"; }; 460 } 461 462 // adapter interface 463 function ShippingAdapter(credentials) { 464 var shipping = new AdvancedShipping(); 465 shipping.login(credentials); 466 467 return { 468 request: function(zipStart, zipEnd, weight) { 469 shipping.setStart(zipStart); 470 shipping.setDestination(zipEnd); 471 return shipping.calculate(weight); 472 } 473 }; 474 } 475 476 // log helper 477 var log = (function () { 478 var log = ""; 479 return { 480 add: function (msg) { log += msg + "\n"; }, 481 show: function () { alert(log); log = ""; } 482 } 483 })(); 484 485 486 function run() { 487 488 var shipping = new Shipping(); 489 490 var credentials = {token: "30a8-6ee1"}; 491 var adapter = new ShippingAdapter(credentials); 492 493 // original shipping object and interface 494 var cost = shipping.request("78701", "10010", "2 lbs"); 495 log.add("Old cost: " + cost); 496 497 // new shipping object with adapted interface 498 cost = adapter.request("78701", "10010", "2 lbs"); 499 log.add("New cost: " + cost); 500 501 log.show(); 502 } 503 }()); 504 505 </script> 506 </body> 507 </html>