javascript设计模式-桥接模式
1 <!DOCTYPE HTML> 2 <html lang="en-US"> 3 <head> 4 <meta charset="utf-8"> 5 <title></title> 6 </head> 7 <body> 8 <script> 9 /** 10 * 桥接模式 11 * 12 * 定义: 13 * 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 14 * 15 * 本质: 16 * 分离抽象与实现 17 * 18 *在实现APi的时候,桥接模式非常有用。实际上,这也许是被用的最不够充分的模式之一。在所有模式中,这种模式最容易付诸实施。在设计一个JS API的时候,可以用这个模式来弱化它与使用它的的类和对象之间的耦合。按GoF的定义,桥接模式的作用在于将抽象与其实现隔离开来,以便二者独立变化。这种模式对于JS中常见的事件驱动的编程大有裨益。 19 * 20 * 无论是用来创建Web服务API还是普通的取值器(accessor)方法和赋值器(mutator)方法,在实现过程中桥接模式都有助于保持API代码的简洁。 21 */ 22 23 // 示例:事件监听器 24 /* 25 桥接模式最常见和实际的应用场合之一是事件监听器回调函数。 26 假设有一个名为getBeerById的API函数,它根据一个标示符返回有关某种啤酒的信息。那个被电击的元素很可能具有啤酒的标示符信息,它可能是作为元素自身的ID保存,也可能使作为别的自定义属性保存。 27 */ 28 // 下面是一种做法: 29 addEvent(element, 'click', getBeearById); 30 function getBeearById(e) { 31 var id = this.id; 32 asyncRequest('GET', 'beer.uri?id=' + id, function (resp) { 33 // callback response 34 console.log('Requested Beer: ' + resp.responseText); 35 }); 36 } 37 38 // 这个API函数不方便做单元测试,或者在命令行环境中执行。 39 // 作为一个优良的API,不要把它与任何特定的实现搅在一起。毕竟,我们希望所有人都能获取到啤酒的信息, 40 function getBeearById(id, callback) { 41 // make request for beer by ID, then return the beer data 42 asyncRequest('GET', 'beer.uri?id=' + id, function (resp) { 43 // callback response 44 callback(resp.responseText); 45 }); 46 } 47 48 addEvent(element, 'click', getBeerByIdBridge); 49 function getBeerByIdBridge(e) { 50 getBeerById(this.id, function (beer) { 51 console.log('Requested Beer: ' + beer); 52 }); 53 } 54 55 /* 56 有了这层桥接元素,这个API的使用范围大大拓宽了,这给类更大的设计自由。getBeerById并没有和事件对象捆绑在一起,你也可以在单元测试中运行这个API。 57 */ 58 59 // 桥接模式的其他例子 60 /* 61 除了在事件回调函数与接口之间进行桥接外,桥接模式也可以用来连接公开的API代码和私用的实现代码。此外,它还可以用来把多个类连结在一起。从类的角度来看,这意味着把接口作为公开的代码编写,而把类的实现作为私用代码编写。 62 63 如果一个公用的接口抽象了一些也许应该属于私用性的(尽管在此情况下它不一定非得是私用的)较复杂的任务,那么可以使用桥接模式来收集某些私用性的信息。可以用一些具有特殊权利的方法作为桥梁以便访问私用变量空间,而不必冒险下到具体实现的浑水中。这一特例中的桥接性函数又称特权函数 64 */ 65 var Public = function () { 66 var secret = 3; 67 this.privilegedGetter = function () { 68 return secret; 69 }; 70 }; 71 var o = new Public(); 72 var data = o.privilegedGetter(); 73 74 // 用桥接模式联结多个类 75 var Class1 = function (a, b, c) { 76 this.a = a; 77 this.b = b; 78 this.c = c; 79 }; 80 var Class2 = function (d) { 81 this.d = d; 82 }; 83 var BridgeClass = function (a, b, c, d) { 84 this.one = new Class1(a, b, c); 85 this.two = new Class2(d); 86 }; 87 88 /* 89 本例中实际上没有客户系统要求提供数据。它只不过是用来接纳大量数据并将其发送给责任方的一种辅助性手段。此外,BridgeClass并不是一个客户系统已经实现的现有接口。引入这个类的目的只不过是要桥接一些类而已。这里使用桥接模式是为了让Class1和Class2能够独立于BridgeClass而发生改变。与门面类不同。 90 */ 91 92 93 // 示例:构建XHR连接队列 94 /* 95 这个对象把请求存储在浏览器内存中的一个队列化数组中。刷新队列时每个请求都会按“先入先出”的顺序被发送给一个后端的web服务。如果次序事关重要,那么在web应用程序中使用队列化系统是有好处的。另外队列还有一个好处,任何涉及因用户输入引起的频繁动作的系统都是适用的例子。最后,连接队列可以帮助用户克服网络连接带来的不便,甚至可以允许他们离线工作。 96 */ 97 // 添加核心工具 98 var asyncRequest = (function () { 99 function handleReadyState(o, callback) { 100 o.onreadystatechange = function () { 101 if (o && o.readyState === 4) { 102 if ((o.status >= 200 && o.status < 300) || o.status === 304) { 103 if (callback) { 104 callback.call(o, o.responseText, o.responseXML); 105 } 106 } 107 } 108 }; 109 } 110 111 var getXHR = function () { 112 var http; 113 try { 114 http = new XMLHttpRequest(); 115 getXHR = function () { 116 return new XMLHttpRequest(); 117 }; 118 } catch (e) { 119 var msxml = [ 120 'MSXML2.XMLHTTP.3.0', 121 'MSXML2,XMLHTTP', 122 'Microsoft.XMLHTTP' 123 ]; 124 for (var i = 0, len = msxml.length; i < len; i++) { 125 try { 126 http = new ActiveXObject(msxml[i]); 127 getXHR = function () { 128 return new ActiveXObject(getXHR.str); 129 }; 130 getXHR.str = msxml[i]; 131 break; 132 } catch (e) { 133 } 134 } 135 136 } 137 return http; 138 }; 139 140 return function (method, url, callback, postVars) { 141 var http = getXHR(); 142 handleReadyState(http, callback); 143 http.open(method, url, true); 144 http.send(postVars || null); 145 return http; 146 } 147 })(); 148 149 // 扩展链式调用方法 150 Function.prototype.method = function (name, fn) { 151 this.prototype[name] = fn; 152 return this; 153 }; 154 155 // 扩展数组方法 156 if (!Array.prototype.forEach) { 157 Array.method('forEach', function (fn, thisObj) { 158 var scope = thisObj || window; 159 for (var i = 0, len = this.length; i < len; i++) { 160 fn.call(scope, this[i], i, this); 161 } 162 }); 163 } 164 165 if (!Array.prototype.filter) { 166 Array.method('filter', function (fn, thisObj) { 167 var scope = thisObj || window; 168 var a = []; 169 for (var i = 0, len = this.length; i < len; i++) { 170 if (!fn.call(scope, this[i], i, this)) { 171 continue; 172 } 173 a.push(this[i]); 174 } 175 return a; 176 }); 177 } 178 179 // demo: 180 function isBigEnough(element, index, array) { 181 return (element >= 10); 182 } 183 var filtered = [12, 5, 8, 130, 44].filter(isBigEnough); 184 // 12, 130, 44 185 186 187 // 添加观察者系统 188 window.DED = window.DED || {}; 189 DED.util = DED.util || {}; 190 DED.util.Observer = function () { 191 this.fns = []; 192 }; 193 DED.util.Observer.prototype = { 194 subscribe: function (fn) { 195 this.fns.push(fn); 196 }, 197 unsubscribe: function (fn) { 198 // 过滤掉当前函数名 199 this.fns = this.fns.filter(function (el) { 200 if (el !== fn) { 201 return el; 202 } 203 }); 204 }, 205 fire: function (o) { 206 // 触发(运行)当前函数 207 this.fns.forEach(function (el) { 208 el(o); 209 }); 210 } 211 }; 212 213 // 开发队列的基本框架 214 /* 215 首先该队列是一个真正的队列,遵从先入先出的基本规则。 216 因为这是一个用于存储待发请求的连接队列,所以你可能希望设置“重试”的次数限制。此外,根据每个队列的请求的大小,你可能也希望能设置“超时”限制。 217 最后,我们应该能够向队列添加新请求和清空队列,当然,还要能够刷新队列。此外还应该可以从队列中删除请求,这种操作称为出列(dequeue)。 218 */ 219 DED.Queue = function () { 220 // Queued requests. 221 this.queue = []; 222 223 // Observable Objects that can notify the client of interesting 224 // moments on each DED.Queue instance. 225 this.onComplete = new DED.util.Observer(); 226 this.onFailure = new DED.util.Observer(); 227 this.onFlush = new DED.util.Observer(); 228 229 // Core properties that set up a frontend queueing system. 230 this.retryCount = 3; 231 this.currentRetry = 0; 232 this.paused = false; 233 this.timeout = 5000; 234 this.conn = {}; 235 this.timer = {}; 236 }; 237 238 DED.Queue.method('flush',function () { 239 if (!this.queue.length) { 240 return; 241 } 242 if (this.paused) { 243 this.paused = false; 244 return; 245 } 246 var that = this; 247 this.currentRetry++; 248 // 撤销请求 249 var abort = function () { 250 that.conn.abort(); 251 // 如果达到重试次数,触发错误方法 252 // 否则重新请求当前队列 253 if (that.currentRetry === that.retryCount) { 254 that.onFailure.fire(); 255 that.currentRetry = 0; 256 } else { 257 that.flush(); 258 } 259 }; 260 // 达到时间段时终止请求,再重新发送 261 this.timer = window.setTimeout(abort, that.timeout); 262 // 请求成功后的回调函数,停止重试时间器 263 // 移除队列的第一个元素 264 // 继续下一个请求,直到完成 265 var callback = function (o) { 266 window.clearTimeout(that.timer); 267 that.currentRetry = 0; 268 that.queue.shift(); 269 that.onFlush.fire(o.responseText); 270 if (that.queue.length === 0) { 271 that.onComplete.fire(); 272 return; 273 } 274 // recursive call to flush 275 that.flush(); 276 }; 277 // 发送请求 278 this.conn = asyncRequest( 279 this.queue[0]['method'], 280 this.queue[0]['url'], 281 callback, 282 this.queue[0]['param'] 283 ); 284 }). 285 method('setRetryCount',function (count) { 286 this.retryCount = count; 287 }). 288 method('setTimeout',function (time) { 289 this.timeout = time; 290 }). 291 method('add',function (o) { 292 this.queue.push(o); 293 }). 294 method('pause',function () { 295 this.paused = true; 296 }). 297 method('dequeue',function () { 298 this.queue.pop(); 299 }). 300 method('clear', function () { 301 this.queue = []; 302 }); 303 304 /* 305 queue属性是一个数组字面量,用于保存对每一个请求的引用。add和dequeue这类方法所做的只是对这个数组进行push和pop操作。flush方法则会把请求发送出去并将它们移出数组。 306 */ 307 308 // 实现队列 309 var q = new DED.Queue(); 310 // Reset our retry count to be higher for slow connections. 311 q.setRetryCount(5); 312 // Decrease timeout limit because we still want fast connections 313 q.setTimeout(1000); 314 q.add({ 315 method: 'GET', 316 url: '/path/to/file.php?ajax=true' 317 }); 318 q.add({ 319 method: 'GET', 320 url: '/path/to/file.php?ajax=true&woe=me' 321 }); 322 // Flush the queue 323 q.flush(); 324 // Pause the queue, retaining the requests 325 q.pause(); 326 // Clear our queue and start fresh 327 q.clear(); 328 // Add two requests 329 q.add({ 330 method: 'GET', 331 url: '/path/to/file.php?ajax=true' 332 }); 333 q.add({ 334 method: 'GET', 335 url: '/path/to/file.php?ajax=true&woe=me' 336 }); 337 // Remove the last request from the queue 338 q.dequeue(); 339 // Flush the queue again 340 q.flush(); 341 342 343 // 示例: 344 addEvent(window, 'load', function () { 345 var q = new DED.Queue(); 346 q.setRetryCount(5); 347 q.setTimeout(3000); 348 var items = $('items'), 349 results = $('results'), 350 queue = $('queue-items'), 351 requests = []; 352 q.onFlush.subscribe(function (data) { 353 results.innerHTML = data; 354 requests.shift(); 355 queue.innerHTML = requests.toString(); 356 }); 357 q.onFailure.subscribe(function () { 358 results.innerHTML += '<span style="color:red;">Connection Error.</span>'; 359 }); 360 q.onComplete.subscribe(function () { 361 results.innerHTML += '<span style="green;">Completed</span>'; 362 }); 363 var actionDispatcher = function (element) { 364 switch (element) { 365 case 'flush': 366 q.flush(); 367 break; 368 case 'dequeue': 369 q.dequeue(); 370 requests.pop(); 371 queue.innerHTML = requests.toString(); 372 break; 373 case 'pause': 374 q.pause(); 375 break; 376 case 'clear': 377 q.clear(); 378 requests = []; 379 queue.innerHTML = ''; 380 break; 381 default: 382 break; 383 } 384 }; 385 var addRequest = function (data) { 386 q.add({ 387 method: 'GET', 388 url: 'bridge-connection-queue.phph?ajax=true&s=' + data, 389 params: null 390 }); 391 requests.push(data); 392 queue.innerHTML = requests.toString(); 393 }; 394 395 addEvent(items, 'click', function (e) { 396 e = e || window.event; 397 var src = e.target || e.srcElement; 398 try { 399 e.preventDefault(); 400 } catch (e) { 401 e.returnValue = false; 402 } 403 actionDispatcher(src.id); 404 }); 405 406 var adders = $('adders'); 407 addEvent(adders, 'click', function (e) { 408 e = e || window.event; 409 var src = e.target || e.srcElement; 410 try { 411 e.preventDefault(); 412 } catch (e) { 413 e.returnValue = false; 414 } 415 addRequest(src.id.split('-')[1]); 416 }); 417 }); 418 419 /* 420 在供用户执行刷新和暂停操作的部分,我们提供了一个动作调度函数,其作用就是桥接用户操作所包含的输入信息并将其委托给恰当的处理代码。在DOM脚本变成中这种技术也称为事件委托(event delegation)。 421 422 判断什么地方应该使用桥接模式通常很简单。假如有下面的代码: 423 $('example').onclick=function(){ 424 new RichTextEditor(); 425 }; 426 从中你无法看出那个编辑器要显示在什么地方,它有些什么配置选项以及应该怎样修改它。这里的要诀是要让接口“可桥接(bridgeable)”,实际上也就是可适配(adaptable) 427 */ 428 /* 429 桥接模式之利 430 掌握如何在软件开发中实现桥接模式,收益的不只是你,还有那些负责维护你的作品的人。把抽象与其实现隔离开,有助于独立地管理软件的各组成部分。Bug也因此更容易查找,而软件发生严重故障的可能性也减小了。说大地,桥接元素应该是粘合每一个抽象的粘合因子。 431 432 桥接模式之弊 433 没使用一个桥接元素都要增加一次函数调用,这对应用程序的性能会有一些负面影响。此外,他们也提高了系统的复杂程度,在出现问题时这会导致代码更难调用。大多情况下桥接模式都非常有用,但注意不要滥用。举个例来说,如果一个桥接函数被用于连接两个函数,而其中某个函数根本不会在桥接函数之外被调用,那么此时这个桥接函数就不是非要不可。 434 */ 435 436 // http://www.joezimjs.com/javascript/javascript-design-patterns-bridge/ 437 var RemoteControl = function (tv) { 438 this.tv = tv; 439 440 this.on = function () { 441 this.tv.on(); 442 }; 443 444 this.off = function () { 445 this.tv.off(); 446 }; 447 448 this.setChannel = function (ch) { 449 this.tv.tuneChannel(ch); 450 }; 451 }; 452 453 /* Newer, Better Remote Control */ 454 var PowerRemote = function (tv) { 455 this.tv = tv; 456 this.currChannel = 0; 457 458 this.setChannel = function (ch) { 459 this.currChannel = ch; 460 this.tv.tuneChannel(ch); 461 }; 462 463 this.nextChannel = function () { 464 this.setChannel(this.currChannel + 1); 465 }; 466 467 this.prevChannel = function () { 468 this.setChannel(this.currChannel - 1); 469 }; 470 }; 471 PowerRemote.prototype = new RemoteControl(); 472 473 474 /** TV Interface 475 Since there are no Interfaces in JavaScript I'm just 476 going to use comments to define what the implementors 477 should implement 478 479 function on 480 function off 481 function tuneChannel(channel) 482 */ 483 484 /* Sony TV */ 485 var SonyTV = function () { 486 this.on = function () { 487 console.log('Sony TV is on'); 488 }; 489 490 this.off = function () { 491 console.log('Sony TV is off'); 492 }; 493 494 this.tuneChannel = function (ch) { 495 console.log('Sony TV tuned to channel ' + ch); 496 }; 497 } 498 499 /* Toshiba TV */ 500 var ToshibaTV = function () { 501 this.on = function () { 502 console.log('Welcome to Toshiba entertainment'); 503 }; 504 505 this.off = function () { 506 console.log('Goodbye Toshiba user'); 507 }; 508 509 this.tuneChannel = function (ch) { 510 console.log('Channel ' + ch + ' is set on your Toshiba television'); 511 }; 512 }; 513 514 /* Let's see it in action */ 515 var sony = new SonyTV(), 516 toshiba = new ToshibaTV(), 517 std_remote = new RemoteControl(sony), 518 pwr_remote = new PowerRemote(toshiba); 519 520 std_remote.on(); // prints "Sony TV is on" 521 std_remote.setChannel(55); // prints "Sony TV tuned to channel 55" 522 std_remote.setChannel(20); // prints "Sony TV tuned to channel 20" 523 std_remote.off(); // prints "Sony TV is off" 524 525 pwr_remote.on(); // prints "Welcome to Toshiba entertainment" 526 pwr_remote.setChannel(55); // prints "Channel 55 is set on your Toshiba television" 527 pwr_remote.nextChannel(); // prints "Channel 56 is set on your Toshiba television" 528 pwr_remote.prevChannel(); // prints "Channel 55 is set on your Toshiba television" 529 pwr_remote.off(); // prints "Goodbye Toshiba user" 530 531 532 // http://www.dofactory.com/javascript-bridge-pattern.aspx 533 534 (function(){ 535 // input devices 536 537 var Gestures = function (output) { 538 this.output = output; 539 this.tap = function () { this.output.click(); } 540 this.swipe = function () { this.output.move(); } 541 this.pan = function () { this.output.drag(); } 542 this.pinch = function () { this.output.zoom(); } 543 }; 544 545 var Mouse = function (output) { 546 this.output = output; 547 this.click = function () { this.output.click(); } 548 this.move = function () { this.output.move(); } 549 this.down = function () { this.output.drag(); } 550 this.wheel = function () { this.output.zoom(); } 551 }; 552 553 // output devices 554 555 var Screen = function () { 556 this.click = function () { log.add("Screen select"); } 557 this.move = function () { log.add("Screen move"); } 558 this.drag = function () { log.add("Screen drag"); } 559 this.zoom = function () { log.add("Screen zoom in"); } 560 }; 561 562 var Audio = function () { 563 this.click = function () { log.add("Sound oink"); } 564 this.move = function () { log.add("Sound waves"); } 565 this.drag = function () { log.add("Sound screetch"); } 566 this.zoom = function () { log.add("Sound volume up"); } 567 }; 568 569 // logging helper 570 571 var log = (function () { 572 var log = ""; 573 return { 574 add: function (msg) { log += msg + "\n"; }, 575 show: function () { alert(log); log = ""; } 576 } 577 })(); 578 579 580 function run() { 581 582 var screen = new Screen(); 583 var audio = new Audio(); 584 585 var hand = new Gestures(screen); 586 var mouse = new Mouse(audio); 587 588 hand.tap(); 589 hand.swipe(); 590 hand.pinch(); 591 592 mouse.click(); 593 mouse.move(); 594 mouse.wheel(); 595 596 log.show(); 597 } 598 }()); 599 600 </script> 601 </body> 602 </html>