1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>命令模式</title> 5 <meta charset="utf-8"> 6 </head> 7 <body> 8 9 <script> 10 /** 11 * 命令模式 12 * 13 * 定义: 14 * 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。 15 * 16 * 本质: 17 * 封装请求 18 * 19 * 命令模式是一种封装方法调用的方式。命令模式与普通函数所有不同。它可以用来对方法调用进行参数化处理和传送,经这样处理过的方法调用可以在任何需要的时候执行。它也可以用来消除调用操作的对象和实现操作对象之间的耦合,这位各种具体的类的更换带来了极大的灵活性。这种模式可以用在许多不同场合,不过它在创建用户界面这一方面非常有用,特别是在需要不受限的(unlimited)取消(undo)操作的时候,它还可以用来替代回调函数,因为它能够提高在对象之间传递的操作的模块化程度。 20 * 21 * 在命令模式中,会定义一个命令的接口,用来约束所有的命令对象,然后提供具体的命令实现,每个命令实现对象是对客户端某个请求的封装,对应于机箱上的按钮,一个机箱上可以有很多按钮,也就相当于会有多个具体的命令实现对象。 22 * 在命令模式中,命令对象并不知道如何处理命令,会有相应的接收者对象来真正执行命令。就像电脑的例子,机箱上的按钮并不知道如何处理功能,而是把这个请求转发给主板,由主办来执行真正的功能,这个主板就相当于命令模式的接收者。 23 * 在命令模式中,命令对象和接收者对象的关系,并不是与生俱来的,需要有一个装配的过程,命令模式中的Client对象可以实现这样的功能。这就相当于在电脑的例子中,有了机箱上的按钮,也有了主板,还需要一个连接线把这个按钮连接到主板上才行。 24 * 命令模式还会提供一个Invoker对象来持有命令对象。就像电脑的例子,机箱上会有多个按钮,这个机箱就相当于命令模式的Invoker对象。这样一来,命令模式的客户端就可以通过Invoker来触发并要求执行相应的命令了,这也相当于真正的客户是按下机箱上的按钮来操作电脑一样。 25 * 26 * 命令模式的关键 27 * 命令模式的关键之处就是把请求封装成对象,也就是命令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储,转发,记录,处理,撤销等,整个命令模式都是围绕这个对象在进行。 28 * 29 * 命令模式的组装和调用 30 * 在命令模式中经常会有一个命令的组装者,用它来维护命令的“虚”实现和真实实现之间的关系。如果是超级智能的命令,也就是说命令对象自己完全实现好了,不需要接收者,那就是命令模式的退化,不需要接受者,自然也不需要组装者了。 31 * 而真正的用户就是具体化请求的内容,然后提交请求进行触发就可以了。真正的用户会通过Invoker来触发命令。 32 * 在实际开发过程中,Client和Invoker可以融合在一起,由客户在使用命令模式的时候,先进行命令对象和接收者的组装,组装完成后,就可以调用命令执行请求。 33 * 34 * 命令模式的接收者 35 * 接收者可以是任意的类,对它没有什么特殊要求,这个对象知道如何真正执行命令的操作,执行时是从Command的实现类里面转调过来。 36 * 一个接收者对象可以处理多个命令,接收者和命令之间没有约定的对应关系。接收者提供的方法个数,名称,功能和命令中的可以不一样,只要能够通过调用接收者的方法来实现命令对应的功能就可以了。 37 * 38 * 命令模式的调用顺序 39 * 使用命令模式的过程分成两个阶段,一个阶段是组装对象和接收者对象的过程,另外一个阶段是触发调用Invoker,来让命令真正的执行。 40 * 组装过程: 41 * 1.创建接收者对象。 42 * 2.创建命令对象,设置命令对象和接收者对象的关系。 43 * 3.创建Invoker对象。 44 * 4.把命令对象设置到Invoker中,让Invoker持有命令对象。 45 * 执行过程: 46 * 1.调用Invoker的方法,触发要求执行命令。 47 * 2.要求持有的命令对象只能执行功能。 48 * 3.要求持有的接收者真正实现功能。 49 * 50 */ 51 52 (function () { 53 // 示例代码 54 55 /** 56 * 具体的命令实现对象 57 * @params {Object} receiver 持有相应的接收者对象 58 */ 59 function Command(receiver) { 60 this.receiver = receiver; 61 // 命令对象可以有自己的状态 62 this.state = ''; 63 } 64 65 Command.prototype.execute = function () { 66 // 通常会转调接收者对象的相应方法,让接收者来真正执行功能 67 this.receiver.action(); 68 }; 69 70 // 接收者对象 71 function Receiver() { 72 } 73 74 // 真正执行命令相应地操作 75 Receiver.prototype.action = function () { 76 }; 77 78 /** 79 * 调用者 80 * 81 */ 82 function Invoker() { 83 } 84 85 /** 86 * @params {Object} command 持有命令对象 87 */ 88 Invoker.prototype.setCommand = function (command) { 89 this.command = command; 90 }; 91 // 要求命令执行请求 92 Invoker.prototype.runCommand = function () { 93 this.command.execute(); 94 }; 95 96 new function Client() { 97 var receiver = new Receiver(); 98 var command = new Command(receiver); 99 var invoker = new Invoker(); 100 invoker.setCommand(command); 101 invoker.runCommand(); 102 }(); 103 }()); 104 105 /* 106 命令的结构 107 108 最简形式的命令对象是一个操作和用以调用这个操作的对象的结合体。所有的命令对象都有一个执行操作(execute operation),其用途就是调用命令对象所绑定的操作。在大多数命令对象中,这个操作是一个名为execute或run的方法。使用同样接口的所有命令对象都可以被同等对待,并且可以随意互换,这是命令模式的魅力之一。 109 110 假设你想设计一个网页,客户可以在上面执行一些与自己的账户相关的操作,比如启用和停用某些广告。因为不知道其中的具体广告数量,所以你想设计一个尽可能灵活的用户界面(UI)。为此你打算用命令模式来弱化按钮之类的用户界面元素与其操作之间的耦合。 111 */ 112 113 // 定义两个类,分别用来封装广告的start方法和stop方法 114 // StopAd command class 115 var StopAd = function (adObject) { 116 this.ad = adObject; 117 }; 118 StopAd.prototype.execute = function () { 119 this.ad.stop(); 120 }; 121 122 // StartAd command class 123 var StartAd = function (adObject) { 124 this.ad = adObject; 125 }; 126 StartAd.prototype.execute = function () { 127 this.ad.start(); 128 }; 129 /* 130 现在有个两个可用在用户界面中的类,它们具有相同的接口。你不知道也不关心adObject的具体实现细节,只要它实现了start和stop方法就行。借助于命令模式,可以实现用户界面对象与广告对象的隔离。 131 */ 132 133 /* 134 下面的代码创建的用户界面中,用户名下的每个广告都有两个按钮,分别用于启动和停止广告的轮播: 135 */ 136 // implementation code 137 var ads = getAds(); 138 for (var i = 0, len = ads.length; i < len; i++) { 139 // Create command objects for starting and stopping the ad 140 var startCommand = new StartAd(ads[i]); 141 var stopCommand = new StopAd(ads[i]); 142 143 // Create the UI elements that will execute the command on click 144 new UIButton('Start ' + ads[i].name, startCommand); 145 new UIButton('stop ' + ads[i].name, stopCommand); 146 } 147 /* 148 UIButton类的构造函数有两个参数,一个是按钮上的文字,另一个是命令对象。它会在网页上生成一个按钮,该按钮被点击时会执行那个命令对象的execute方法。这个类也不需要知道所用命令对象的确切实现。因为所有命令对象都实现了execute方法,所以可以把任何一种命令对象提供给UIButton。这有助于创建高度模块化和低耦合的用户界面。 149 */ 150 151 /* 152 用闭包创建命令对象 153 154 还有另外一种办法可以用来封装函数。这种办法不需要创建一个具有execute方法的对象,而是把想要执行的方法包装在闭包中。如果想要创建的命令对象像前例中那样只有一个方法。那么这种办法由其方便。现在你不再调用execute方法,因为那个命令可以作为函数直接执行。这样做还可以省却作用域和this关键字的绑定的烦恼。 155 */ 156 157 // Command using closures 158 function makeSart(adObject) { 159 return function () { 160 adObject.start(); 161 }; 162 } 163 function makeStop(adObject) { 164 return function () { 165 adObject.stop(); 166 }; 167 } 168 169 // Implementation code 170 var startCommand = makeStart(ads[0]); 171 var stopCommand = makeStop(ads[0]); 172 173 startCommand(); 174 stopCommand(); 175 /* 176 不适用于需要多个命令方法的场合,比如后面要实现取消功能的示例 177 */ 178 179 /* 180 客户,调用者和接收者 181 182 这个系统中有三个参与者:客户(client),调用者(invoking object)和接收者(receiving object)。客户负责实例化命令并将其交给调用者。在前面的例子中,for循环中的代码就是客户。它通常被包装为一个对象,但也不是非这样不可。调用者接过命令并将其保存下来。它会在某个时候调用该命令对象的execute方法,或者将其交给另一个潜在的调用者。前例中的调用者就是UIButton类创建的按钮。用户点击它的时候,它就会调用命令对象的execute方法。接收者则是实际执行操作的对象。调用者进行“commandObject.execute()”这种形式的调用时,它所调用的方法将转而以“receiver.action()”这种形式调用恰当的方法。而接收者就是广告对象,它所能执行的操作要么是start方法,要么是stop方法。 183 184 客户创建命令,调用者执行该命令,接收者在命令执行时执行相应操作。 185 所有使用命令模式的系统都有客户和调用者,但不一定有接收者。 186 */ 187 188 // 在命令模式中使用接口 189 // If no exception is thrown, youcan safely invoke the 190 // execute operation 191 someCommand.execute(); 192 193 // 如果用必报来创建命令函数,只需检查是否为函数即可 194 if (typeof someCommand !== 'function') { 195 throw new Error('Command isn\'t a function'); 196 } 197 198 199 // 命令对象的类型 200 /* 201 简单命令对象就是把现有接收者的操作(广告对象的start和stop方法)与调用者(按钮)绑定在一起。这类命令对象最简单,其模块程度也最高。它们与客户,接收者和调用者之间只是松散地偶合在一起: 202 */ 203 // SimpleCommand, a loosely coupled, simple command class. 204 var SimpleCommand = function (receiver) { 205 this.receiver = receiver; 206 }; 207 SimpleCommand.prototype.execute = function () { 208 this.receiver.action(); 209 }; 210 211 /* 212 另一种则是那种封装着一套复杂指令的命令对象。这种命令对象实际上没有接受者,因为它自己提供了操作的具体实现。它并不把操作委托给接收者实现,所有用于实现相关操作的代码都包含在其内部: 213 */ 214 // ComplexCommand, a tightly coupled, complex command class. 215 var ComplexCommand = function () { 216 this.logger = new Logger(); 217 this.xhrHandler = XhrManager.createXhrHandler(); 218 this.parameters = {}; 219 }; 220 ComplexCommand.prototype = { 221 setParameter: function (key, value) { 222 this.parameters[key] = value; 223 }, 224 execute: function () { 225 this.logger.log('Executing command'); 226 var postArray = []; 227 for (var key in this.parameters) { 228 if (this.parameters.hasOwnProperty(key)) { 229 postArray.push(key + '=' + this.parameters[key]); 230 } 231 } 232 var postString = postArray.join('&'); 233 this.xhrHandler.request( 234 'POST', 235 'script.php', 236 function () { 237 }, 238 postString 239 ); 240 } 241 }; 242 243 /* 244 有些命令对象不但封装了接收者的操作,而且其execute方法中也具有一些实现代码。这类命令对象是一个灰色地带: 245 */ 246 // GreyAreaCommand, somewhere between simple and complex 247 var GreyAreaCommand = function (receiver) { 248 this.logger = new Logger(); 249 this.receiver = receiver; 250 }; 251 GreyAreaCommand.prototype.execute = function () { 252 this.logger.log('Executing command'); 253 this.receiver.prepareAction(); 254 this.receiver.action(); 255 }; 256 257 /* 258 简单命令对象一般用来消除两个对象(接受着和调用者)之间的耦合,而复杂命令对象则一般用来封装不可分的或事务性的指令。 259 */ 260 261 // 实例: 菜单项 262 // 菜单组合对象 263 /* 264 接下来要实现的事Menubar,Menu和MenuItem类,作为一个整体,他们要能显示所有可用操作,并且根据要求调用这些操作,Menubar和Menu都是组合对象类,而MenuItem则是叶类。Menubar类保存着所有Menu实例: 265 */ 266 // MenuBar class, a composite 267 var MenuBar = function () { 268 this.menus = {}; 269 this.element = document.createElement('ul'); 270 this.element.style.display = 'none'; 271 }; 272 MenuBar.prototype = { 273 add: function (menuObject) { 274 this.menus[menuObject.name] = menuObject; 275 this.element.appendChild(this.menus[menuObject.name].getElement()); 276 }, 277 remove: function (name) { 278 delete this.menus[name]; 279 }, 280 getChild: function (name) { 281 return this.menus[name]; 282 }, 283 getElement: function () { 284 return this.element; 285 }, 286 show: function () { 287 this.element.style.display = ''; 288 for (var name in this.menus) { 289 this.menus[name].show(); 290 } 291 } 292 }; 293 294 // Menu class, a composite 295 var Menu = function (name) { 296 this.name = name; 297 this.items = {}; 298 this.element = document.createElement('li'); 299 this.element.style.display = 'none'; 300 this.container = document.createElement('ul'); 301 this.element.appendChild(this.container); 302 }; 303 Menu.prototype = { 304 add: function (menuItemObject) { 305 this.items[menuItemObject.name] = menuItemObject; 306 this.container.appendChild(this.items[menuItemObject.name].getElement()); 307 }, 308 remove: function () { 309 delete this.items[name]; 310 }, 311 getChild: function (name) { 312 return this.items[name]; 313 }, 314 getElement: function () { 315 return this.element; 316 }, 317 show: function () { 318 this.element.style.display = ''; 319 for (var name in this.items) { 320 this.items[name].show(); 321 } 322 } 323 }; 324 325 // 调用者类 326 // MenuItem class, a leaf 327 var MenuItem = function (name, command) { 328 this.name = name; 329 this.element = document.createElement('li'); 330 this.element.style.display = 'none'; 331 this.anchor = document.createElement('a'); 332 this.anchor.href = '#'; 333 this.element.appendChild(this.anchor); 334 this.anchor.innerHTML = this.name; 335 336 addEvent(this.anchor, 'click', function (e) { 337 e = e || window.event; 338 if (typeof e.preventDefault === 'function') { 339 e.preventDefault(); 340 } else { 341 e.returnValue = false; 342 } 343 command.execute(); 344 }); 345 }; 346 MenuItem.prototype = { 347 add: function () { 348 }, 349 remove: function () { 350 }, 351 getChild: function () { 352 }, 353 getElement: function () { 354 return this.element; 355 }, 356 show: function () { 357 this.element.style.display = ''; 358 } 359 }; 360 361 // 命令类 362 // MenuCommand class, a command object 363 var MenuCommand = function (action) { 364 this.action = action; 365 }; 366 MenuCommand.prototype.execute = function () { 367 this.action.action(); 368 }; 369 370 371 // Receiver objects, instantiated from existing classes 372 var Test1 = function () { 373 console.log('test1'); 374 }; 375 Test1.prototype = { 376 action: function () { 377 console.log('this is test1 fn1'); 378 } 379 }; 380 var Test2 = function () { 381 console.log('test2'); 382 }; 383 Test2.prototype = { 384 action: function () { 385 console.log('this is test2 fn1'); 386 } 387 }; 388 var Test3 = function () { 389 console.log('test3'); 390 }; 391 var test1 = new Test1(); 392 var test2 = new Test2(); 393 var test3 = new Test3(); 394 395 // Create the menu bar 396 var appMenuBar = new MenuBar(); 397 398 // The File menu 399 var fileMenu = new Menu('File'); 400 401 var test1Command1 = new MenuCommand(test1); 402 403 fileMenu.add(new MenuItem('test1-1', test1Command1)); 404 405 appMenuBar.add(fileMenu); 406 407 var insertMenu = new Menu('Insert'); 408 var test2Command2 = new MenuCommand(test2); 409 insertMenu.add(new MenuItem('test2-1', test2Command2)); 410 411 appMenuBar.add(insertMenu); 412 413 document.body.appendChild(appMenuBar.getElement()); 414 appMenuBar.show(); 415 416 417 (function () { 418 // 补偿式或者反操作式 419 420 // 取消操作和命令日志 421 422 // ReversibleCommand interface 423 var ReversibleCommand = new Interface('ReversibleCommand', ['execute', 'undo']); 424 425 // 接下来要做的是创建4个命令类, 426 // 它们分别用来向上下左右四个方向移动指针: 427 var MoveUp = function (cursor) { 428 this.cursor = cursor; 429 }; 430 MoveUp.prototype = { 431 execute: function () { 432 this.cursor.move(0, -10); 433 }, 434 undo: function () { 435 this.cursor.move(0, 10); 436 } 437 }; 438 439 var MoveDown = function (cursor) { 440 this.cursor = cursor; 441 }; 442 MoveDown.prototype = { 443 execute: function () { 444 this.cursor.move(0, 10); 445 }, 446 undo: function () { 447 this.cursor.move(0, -10); 448 } 449 }; 450 451 var MoveLeft = function (cursor) { 452 this.cursor = cursor; 453 }; 454 MoveLeft.prototype = { 455 execute: function () { 456 this.cursor.move(-10, 0); 457 }, 458 undo: function () { 459 this.cursor.move(10, 0); 460 } 461 }; 462 463 var MoveRight = function (cursor) { 464 this.cursor = cursor; 465 }; 466 MoveRight.prototype = { 467 execute: function () { 468 this.cursor.move(10, 0); 469 }, 470 undo: function () { 471 this.cursor.move(-10, 0); 472 } 473 }; 474 475 // 接收者,负责实现指针移动 476 // Cursor class 实现了命令类所要求的操作 477 var Cursor = function (width, height, parent) { 478 this.width = width; 479 this.height = height; 480 this.position = { 481 x: width / 2, 482 y: height / 2 483 }; 484 485 this.canvas = document.createElement('canvas'); 486 this.canvas.width = this.width; 487 this.canvas.height = this.height; 488 parent.appendChild(this.canvas); 489 490 this.ctx = this.canvas.getContext('2d'); 491 this.ctx.fillStyle = '#cc0000'; 492 this.move(0, 0); 493 }; 494 Cursor.prototype.move = function (x, y) { 495 this.position.x += x; 496 this.position.y += y; 497 498 this.ctx.clearRect(0, 0, this.width, this.height); 499 this.ctx.fillRect(this.position.x, this.position.y, 3, 3); 500 }; 501 502 // 下面这个装饰者的作用就是在执行一个命令之前先将其压栈 503 // UndoDecorator class 504 var UndoDecorator = function (command, undoStack) { 505 this.command = command; 506 this.undoStack = undoStack; 507 }; 508 UndoDecorator.prototype = { 509 execute: function () { 510 this.undoStack.push(this.command); 511 this.command.execute(); 512 }, 513 undo: function () { 514 this.command.undo(); 515 } 516 }; 517 518 // 用户界面类,负责生成必要的HTML元素,并且为其注册click事件监听器, 519 // 这些监听器要么调用execute方法要么调用undo方法: 520 // CommandButton class 521 var CommandButton = function (label, command, parent) { 522 this.element = document.createElement('button'); 523 this.element.innerHTML = label; 524 parent.appendChild(this.element); 525 526 addEvent(this.element, 'click', function () { 527 command.execute(); 528 }); 529 }; 530 531 // UndoButton class 532 var UndoButton = function (label, parent, undoStack) { 533 this.element = document.createElement('button'); 534 this.element.innerHTML = label; 535 parent.appendChild(this.element); 536 537 addEvent(this.element, 'click', function () { 538 if (undoStack.length === 0) return; 539 var lastCommand = undoStack.pop(); 540 lastCommand.undo(); 541 }); 542 }; 543 /* 544 像UndoDecorator类一样,UndoButton类的构造函数也需要把命令栈作为参数传入。这个栈其实就是一个数组。调用经UndoDecorator对象装饰过的命令对象的execute方法时这个命令对象会被压入栈。为了执行取消操作,取消按钮会从命令栈中弹出最近的命令并调用其undo方法。这将逆转刚执行过的操作。 545 */ 546 547 // Implementation code 548 var body = document.body; 549 var cursor = new Cursor(400, 400, body); 550 var undoStack = []; 551 552 var upCommand = new UndoDecorator(new MoveUp(cursor), undoStack); 553 var downCommand = new UndoDecorator(new MoveDown(cursor), undoStack); 554 var leftCommand = new UndoDecorator(new MoveLeft(cursor), undoStack); 555 var rightCommand = new UndoDecorator(new MoveRight(cursor), undoStack); 556 557 var upButton = new CommandButton('Up', upCommand, body); 558 var downButton = new CommandButton('Down', downCommand, body); 559 var leftButton = new CommandButton('Left', leftCommand, body); 560 var rightButton = new CommandButton('Right', rightCommand, body); 561 var undoButton = new UndoButton('Undo', body, undoStack); 562 }()); 563 564 565 (function () { 566 // 使用命令日志实现不可逆操作的取消 567 /* 568 在画布上画线很容易,不过要取消这条线的绘制是不可能的。从一个点到另一个点的移动这种操作具有精确的对立操作,执行后者的结果看起来就像前者被逆转了一样。但是对于从A到B画一条线这种操作,从B到A再画一条线是无法逆转前一操作的,这只不过是在第一条线的上方又画一条线而已。 569 570 取消这种操作的唯一办法是清除状态,然后把之前执行过的操作(不含最近那个)一次重做一遍。这很容易办到,为此需要把所有执行过的命令记录在栈中。要想取消一个操作,需要做的就是从栈中弹出最近那个命令并弃之不用,然后清理画布并从头开始重新执行记录下来的所有命令。 571 */ 572 573 // Movement commands 574 var MoveUp = function (cursor) { 575 this.cursor = cursor; 576 }; 577 MoveUp.prototype = { 578 execute: function () { 579 this.cursor.move(0, -10); 580 } 581 }; 582 583 var MoveDown = function (cursor) { 584 this.cursor = cursor; 585 }; 586 MoveDown.prototype = { 587 execute: function () { 588 this.cursor.move(0, 10); 589 } 590 }; 591 592 var MoveLeft = function (cursor) { 593 this.cursor = cursor; 594 }; 595 MoveLeft.prototype = { 596 execute: function () { 597 this.cursor.move(-10, 0); 598 } 599 }; 600 601 var MoveRight = function (cursor) { 602 this.cursor = cursor; 603 }; 604 MoveRight.prototype = { 605 execute: function () { 606 this.cursor.move(10, 0); 607 } 608 }; 609 610 // Cursor class, with an internal command stack 611 var Cursor = function (width, height, parent) { 612 this.width = width; 613 this.height = height; 614 this.commandStack = []; 615 616 this.canvas = document.createElement('canvas'); 617 this.canvas.width = this.width; 618 this.canvas.height = this.height; 619 parent.appendChild(this.canvas); 620 621 this.ctx = this.canvas.getContext('2d'); 622 this.ctx.strokeStyle = '#cc0000'; 623 this.move(0, 0); 624 }; 625 Cursor.prototype = { 626 move: function (x, y) { 627 var that = this; 628 this.commandStack.push(function () { 629 that.lineTo(x, y); 630 }); 631 this.executeCommands(); 632 }, 633 lineTo: function (x, y) { 634 this.position.x += x; 635 this.position.y += y; 636 this.ctx.lineTo(this.position.x, this.position.y); 637 }, 638 executeCommands: function () { 639 this.position = { 640 x: this.width / 2, 641 y: this.height / 2 642 }; 643 this.ctx.clearRect(0, 0, this.width, this.height); 644 this.ctx.beginPath(); 645 this.ctx.moveTo(this.position.x, this.position.y); 646 for (var i = 0, len = this.commandStack.length; i < len; i++) { 647 this.commandStack[i](); 648 } 649 this.ctx.stroke(); 650 }, 651 undo: function () { 652 this.commandStack.pop(); 653 this.executeCommands(); 654 } 655 }; 656 657 // UndoButton class 658 var UndoButton = function (label, parent, cursor) { 659 this.element = document.createElement('button'); 660 this.element.innerHTML = label; 661 parent.appendChild(this.element); 662 addEvent(this.element, 'click', function () { 663 cursor.undo(); 664 }); 665 }; 666 // CommandButton class 667 var CommandButton = function (label, command, parent) { 668 this.element = document.createElement('button'); 669 this.element.innerHTML = label; 670 parent.appendChild(this.element); 671 672 addEvent(this.element, 'click', function () { 673 command.execute(); 674 }); 675 }; 676 677 var body = document.body; 678 var cursor = new Cursor(400, 400, body); 679 680 var upCommand = new MoveUp(cursor); 681 var downCommand = new MoveDown(cursor); 682 var leftCommand = new MoveLeft(cursor); 683 var rightCommand = new MoveRight(cursor); 684 685 var upButton = new CommandButton('Up', upCommand, body); 686 var downButton = new CommandButton('Down', downCommand, body); 687 var leftButton = new CommandButton('Left', leftCommand, body); 688 var rightButton = new CommandButton('Right', rightCommand, body); 689 var undoButton = new UndoButton('Undo', body, cursor); 690 }()); 691 692 (function () { 693 // 宏命令 694 /* 695 去饭店吃饭过程。 696 697 客户: 只负责发出命令,就是点菜操作。 698 命令对象: 就是点的菜。 699 服务员: 知道真正的接收者是谁,同时持有菜单,当你点菜完毕,服务员就启动命令执行。 700 后厨, 凉菜部: 相当于接收者。 701 702 菜单命令包含多个命令对象 703 */ 704 705 // 坐热菜的厨师 706 var HotCook = function () { 707 }; 708 HotCook.prototype = { 709 cook: function (name) { 710 console.log('本厨师正在做:' + name); 711 } 712 }; 713 714 // 做凉菜的厨师 715 var CoolCook = function () { 716 }; 717 CoolCook.prototype = { 718 cook: function (name) { 719 console.log('凉菜' + name + '已经做好,本厨师正在装盘。'); 720 } 721 } 722 723 // 定义了三道菜,每道菜是一个命令对象 724 725 var DuckCommand = function () { 726 this.cookApi = null; 727 }; 728 DuckCommand.prototype = { 729 constructor: DuckCommand, 730 setCookApi: function (cookApi) { 731 this.cookApi = cookApi; 732 }, 733 execute: function () { 734 this.cookApi.cook('北京烤鸭'); 735 } 736 }; 737 738 var ChopCommand = function () { 739 this.cookApi = null; 740 }; 741 ChopCommand.prototype = { 742 constructor: ChopCommand, 743 setCookApi: function (cookApi) { 744 this.cookApi = cookApi; 745 }, 746 execute: function () { 747 this.cookApi.cook('绿豆排骨煲'); 748 } 749 }; 750 751 var PorkCommand = function () { 752 this.cookApi = null; 753 }; 754 PorkCommand.prototype = { 755 constructor: PorkCommand, 756 setCookApi: function (cookApi) { 757 this.cookApi = cookApi; 758 }, 759 execute: function () { 760 this.cookApi.cook('蒜泥白肉'); 761 } 762 }; 763 764 // 菜单对象,宏命令对象 765 var MenuCommand = function () { 766 var col = []; 767 768 this.addCommand = function (cmd) { 769 col.push(cmd); 770 }; 771 772 this.execute = function () { 773 for (var i = 0 , len = col.length; i < len; i++) { 774 col[i].execute(); 775 } 776 }; 777 }; 778 779 // 服务员,负责组合菜单,负责组装每个菜和具体的实现者。 780 var Waiter = function () { 781 var menuCommand = new MenuCommand(); 782 783 // 客户点菜 784 this.orderDish = function (cmd) { 785 var hotCook = new HotCook(); 786 var coolCook = new CoolCook(); 787 788 if (cmd instanceof DuckCommand) { 789 cmd.setCookApi(hotCook); 790 } else if (cmd instanceof ChopCommand) { 791 cmd.setCookApi(hotCook); 792 } else if (cmd instanceof PorkCommand) { 793 cmd.setCookApi(coolCook); 794 } 795 796 menuCommand.addCommand(cmd); 797 }; 798 799 // 点菜完毕 800 this.orderOver = function () { 801 menuCommand.execute(); 802 }; 803 }; 804 805 var waiter = new Waiter(); 806 var chop = new ChopCommand(); 807 var duck = new DuckCommand(); 808 var pork = new PorkCommand(); 809 810 waiter.orderDish(chop); 811 waiter.orderDish(duck); 812 waiter.orderDish(pork); 813 814 waiter.orderOver(); 815 816 }()); 817 818 (function () { 819 // 队列请求 820 821 function createCommand(name) { 822 function Command(tableNum) { 823 this.cookApi = null; 824 this.tableNum = tableNum; 825 } 826 827 Command.prototype = { 828 setCookApi: function (cookApi) { 829 this.cookApi = cookApi; 830 }, 831 execute: function () { 832 this.cookApi.cook(this.tableNum, name); 833 } 834 }; 835 836 return Command; 837 } 838 839 var ChopCommand = createCommand('绿豆排骨煲'); 840 var DuckCommand = createCommand('北京烤鸭'); 841 842 var CommandQueue = { 843 cmds: [], 844 addMenu: function (menu) { 845 var cmds = menu.getCommands(); 846 for (var i = 0, len = cmds.length; i < len; i++) { 847 this.cmds.push(cmds[i]); 848 } 849 }, 850 getOneCommand: function () { 851 return this.cmds.length ? this.cmds.shift() : null; 852 } 853 }; 854 855 var MenuCommand = function () { 856 this.col = []; 857 }; 858 MenuCommand.prototype = { 859 addCommand: function (cmd) { 860 this.col.push(cmd); 861 }, 862 setCookApi: function (cookApi) { 863 }, 864 getTableNum: function () { 865 return 0; 866 }, 867 getCommands: function () { 868 return this.col; 869 }, 870 execute: function () { 871 CommandQueue.addMenu(this); 872 } 873 }; 874 875 var HotCook = function (name) { 876 this.name = name; 877 }; 878 HotCook.prototype = { 879 cook: function (tableNum, name) { 880 var cookTime = parseInt(10 * Math.random() + 3); 881 console.log(this.name + '厨师正在为' + tableNum + '号桌做:' + name); 882 883 var me = this; 884 setTimeout(function () { 885 console.log(me.name + '厨师为' + tableNum + '号桌做好了:' + name + ',共计耗时=' + cookTime + '秒'); 886 }, cookTime * 1000); 887 }, 888 run: function () { 889 var me = this; 890 setTimeout(function () { 891 var cmd; 892 893 while ((cmd = CommandQueue.getOneCommand())) { 894 cmd.setCookApi(me); 895 cmd.execute(); 896 } 897 }, 1000); 898 } 899 }; 900 901 var Waiter = function () { 902 this.menuCommand = new MenuCommand(); 903 }; 904 Waiter.prototype = { 905 orderDish: function (cmd) { 906 this.menuCommand.addCommand(cmd); 907 }, 908 orderOver: function () { 909 this.menuCommand.execute(); 910 } 911 }; 912 913 var c1 = new HotCook('张三'); 914 c1.run(); 915 916 for (var i = 0; i < 5; i++) { 917 var waiter = new Waiter(); 918 var chop = new ChopCommand(i); 919 var duck = new DuckCommand(i); 920 921 waiter.orderDish(chop); 922 waiter.orderDish(duck); 923 924 waiter.orderOver(); 925 } 926 927 }()); 928 929 function test() { 930 // 日志请求 931 // TODO 该示例在写入文件内容的时候并不能把实例的原型对象序列化, 932 // 因此读取文件内容后,反序列化后没有原型对应的方法 933 var fs = require('fs'); 934 var Promise = require('d:\\node\\node_modules\\rsvp'); 935 936 var FileOpeUtil = { 937 readFile: function (pathName) { 938 var def = Promise.defer(); 939 940 fs.open(pathName, 'r', function opened(err, fd) { 941 if (err) { 942 def.reject(); 943 fs.close(fd); 944 throw err; 945 } 946 947 var readBuffer = new Buffer(1024); 948 var bufferOffset = 0; 949 var bufferLength = readBuffer.length; 950 var filePosition = null; 951 952 fs.read( 953 fd, 954 readBuffer, 955 bufferOffset, 956 bufferLength, 957 filePosition, 958 function read(err, readBytes) { 959 if (err) { 960 def.reject(err); 961 fs.close(fd); 962 return; 963 } 964 965 if (readBytes >= 0) { 966 try { 967 def.resolve(JSON.parse(readBuffer.slice(0, readBytes).toString('utf8'))); 968 } catch (e) { 969 def.reject(e); 970 } 971 972 fs.close(fd); 973 } 974 } 975 ); 976 }); 977 978 return def.promise; 979 }, 980 writeFile: function (pathName, list) { 981 var def = Promise.defer(); 982 983 fs.open(pathName, 'w', function opened(err, fd) { 984 if (err) { 985 def.reject(); 986 fs.close(fd); 987 throw err; 988 } 989 990 var writeBuffer = new Buffer(JSON.stringify(list)); 991 var bufferPosition = 0; 992 var bufferLength = writeBuffer.length; 993 var filePosition = null; 994 995 fs.write( 996 fd, 997 writeBuffer, 998 bufferPosition, 999 bufferLength, 1000 filePosition, 1001 function wrote(err, written) { 1002 if (err) { 1003 def.reject(err); 1004 fs.close(fd); 1005 return; 1006 } 1007 1008 console.log('wrote ' + written + ' bytes'); 1009 def.resolve(written); 1010 fs.close(fd); 1011 } 1012 ); 1013 }); 1014 1015 return def.promise; 1016 } 1017 }; 1018 1019 function createCommand(name) { 1020 function Command(tableNum) { 1021 this.cookApi = null; 1022 this.tableNum = tableNum; 1023 } 1024 1025 Command.prototype = { 1026 setCookApi: function (cookApi) { 1027 this.cookApi = cookApi; 1028 }, 1029 execute: function () { 1030 this.cookApi.cook(this.tableNum, name); 1031 } 1032 }; 1033 1034 return Command; 1035 } 1036 1037 var ChopCommand = createCommand('绿豆排骨煲'); 1038 var DuckCommand = createCommand('北京烤鸭'); 1039 1040 var MenuCommand = function () { 1041 this.col = []; 1042 }; 1043 MenuCommand.prototype = { 1044 addCommand: function (cmd) { 1045 this.col.push(cmd); 1046 }, 1047 setCookApi: function (cookApi) { 1048 }, 1049 getTableNum: function () { 1050 return 0; 1051 }, 1052 getCommands: function () { 1053 return this.col; 1054 }, 1055 execute: function () { 1056 CommandQueue.addMenu(this); 1057 } 1058 }; 1059 1060 var HotCook = function (name) { 1061 this.name = name; 1062 }; 1063 HotCook.prototype = { 1064 cook: function (tableNum, name) { 1065 var cookTime = parseInt(10 * Math.random() + 3); 1066 console.log(this.name + '厨师正在为' + tableNum + '号桌做:' + name); 1067 1068 var me = this; 1069 setTimeout(function () { 1070 console.log(me.name + '厨师为' + tableNum + '号桌做好了:' + name + ',共计耗时=' + cookTime + '秒'); 1071 }, cookTime * 1000); 1072 }, 1073 run: function () { 1074 var me = this; 1075 setTimeout(function () { 1076 var cmd; 1077 1078 while ((cmd = CommandQueue.getOneCommand())) { 1079 cmd.setCookApi(me); 1080 cmd.execute(); 1081 break; 1082 } 1083 }, 1000); 1084 } 1085 }; 1086 1087 var Waiter = function () { 1088 this.menuCommand = new MenuCommand(); 1089 }; 1090 Waiter.prototype = { 1091 orderDish: function (cmd) { 1092 this.menuCommand.addCommand(cmd); 1093 }, 1094 orderOver: function () { 1095 this.menuCommand.execute(); 1096 } 1097 }; 1098 1099 1100 var CommandQueue = { 1101 cmds: [], 1102 addMenu: function (menu) { 1103 var cmds = menu.getCommands(); 1104 for (var i = 0, len = cmds.length; i < len; i++) { 1105 this.cmds.push(cmds[i]); 1106 } 1107 FileOpeUtil.writeFile('./test.txt', this.cmds); 1108 }, 1109 getOneCommand: function () { 1110 var cmd = null; 1111 1112 if (this.cmds.length) { 1113 cmd = this.cmds.shift(); 1114 FileOpeUtil.writeFile('./test.txt', this.cmds); 1115 } 1116 1117 return cmd; 1118 } 1119 }; 1120 1121 var FILE_NAME = './test.txt'; 1122 1123 FileOpeUtil.readFile(FILE_NAME) 1124 .then(function (data) { 1125 console.log(data); 1126 data.map(function () { 1127 1128 }); 1129 1130 CommandQueue.cmds = data; 1131 main(); 1132 }, function () { 1133 main(); 1134 }); 1135 1136 function main() { 1137 var c1 = new HotCook('张三'); 1138 c1.run(); 1139 1140 for (var i = 0; i < 5; i++) { 1141 var waiter = new Waiter(); 1142 var chop = new ChopCommand(i); 1143 var duck = new DuckCommand(i); 1144 1145 waiter.orderDish(chop); 1146 waiter.orderDish(duck); 1147 1148 waiter.orderOver(); 1149 } 1150 } 1151 } 1152 1153 /* 1154 用于崩溃恢复的命令日志 1155 1156 命令日志的一个有趣的用途是在程序崩溃后恢复其状态。在前面这个示例中,可以用XHR把经过序列化处理的命令记录到服务器上。用户下次访问该网页的时候,系统可以找出这些命令并用其将画布上的图案精确恢复到浏览器关闭时的状态。这可以替用户把应用程序状态保管下来,以便其撤销先前的任何一次浏览器会话中执行的操作。如果应用系统比较复杂,那么这种类型的命令日志会很大的存储需求。为此你可以提供一个按钮,用户可以用它提交到当时为止的所有操作,从而清空命令栈。 1157 */ 1158 1159 /* 1160 命令模式的适用场合 1161 1162 1.如果需要抽象出需要执行的动作,并参数化这些对象,可以选用命令模式。将这些需要执行的动作抽象成为命令,然后实现命令的参数化配置。 1163 2.如果需要在不同的时刻指定,排列和执行请求。将这些请求封装成为命令对象,然后实现请求队列化。 1164 3.如果需要支持取消操作,可以选用,通过管理命令对象,能很容易的实现命令的恢复和重做功能。 1165 4.如果需要支持当系统奔溃时,能将系统的操作功能重新执行一遍时。将这些操作功能的请求封装成命令对象,然后实现日志命令,就可以在系统恢复以后,通过日志获取命令列表,从而重新执行一遍功能。 1166 5.在需要事务的系统中,命令模式提供了对事务进行建模的方法。 1167 1168 1169 命令模式之利 1170 1171 1.更松散的耦合 1172 命令模式使得发起命令的对象--客户端,和具体实现命令的对象--接收者对象完全解耦,也就是说发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。 1173 2.更动态的控制 1174 命令模式把请求封装起来,可以动态地对它进行参数化,队列花和日志化等操作,从而使得系统更灵活。 1175 3.很自然的复合命令 1176 很容易地组合符合命令,也就是宏命令。 1177 4.更好的扩展性 1178 1179 1180 1181 命令模式之弊 1182 1183 如果一个命令对象只包装了一个方法调用,而且其唯一目的就是这层对象包装的话,那么这种做法是一种浪费。如果你不需要命令模式给予的任何额外特性,也不需要具有一致接口的类所带来的模块性,那么直接使用方法引用而不是完整的命令对象也许更恰当。命令对象也会增加代码调试的难度,因为在应用了命令模式之后原有的方法之上又多了一层可能出错的代码。 1184 1185 1186 相关模式 1187 1188 命令模式和组合模式 1189 可以组合使用 1190 宏命令的功能就可以使用组合模式。 1191 1192 命令模式和备忘录模式 1193 可以组合使用 1194 在实现可撤销功能时,如果采用保存命令执行前的状态,撤销的时候就把状态恢复,就可以考虑使用备忘录模式。 1195 1196 命令模式和模板方法模式 1197 命令模式可以作为模板方法的一种替代模式,也就是说命令模式可以模仿实现模板方法模式的功能。 1198 */ 1199 1200 1201 /* Title: Command 1202 Description: creates objects which encapsulate actions and parameters 1203 */ 1204 1205 (function () { 1206 1207 var CarManager = { 1208 1209 /* request information */ 1210 requestInfo: function (model, id) { 1211 return 'The purchase info for ' + model + ' with ID ' + id + ' is being processed...'; 1212 }, 1213 1214 /* purchase the car */ 1215 buyVehicle: function (model, id) { 1216 return 'You have successfully purchased Item ' + id + ', a ' + model + '.'; 1217 } 1218 1219 }; 1220 1221 CarManager.execute = function (commad) { 1222 return CarManager[commad.request](commad.model, commad.carID); 1223 }; 1224 1225 var actionA = CarManager.execute({request: 'requestInfo', model: 'Ford Mondeo', carID: '543434'}); 1226 console.log(actionA); 1227 var actionB = CarManager.execute({request: 'buyVehicle', model: 'Ford Mondeo', carID: '543434'}); 1228 console.log(actionB); 1229 1230 })(); 1231 1232 1233 // http://www.joezimjs.com/javascript/javascript-design-patterns-command/ 1234 var EnableAlarm = function (alarm) { 1235 this.alarm = alarm; 1236 } 1237 EnableAlarm.prototype.execute = function () { 1238 this.alarm.enable(); 1239 } 1240 1241 var DisableAlarm = function (alarm) { 1242 this.alarm = alarm; 1243 } 1244 DisableAlarm.prototype.execute = function () { 1245 this.alarm.disable(); 1246 } 1247 1248 var ResetAlarm = function (alarm) { 1249 this.alarm = alarm; 1250 } 1251 ResetAlarm.prototype.execute = function () { 1252 this.alarm.reset(); 1253 } 1254 1255 var SetAlarm = function (alarm) { 1256 this.alarm = alarm; 1257 } 1258 SetAlarm.prototype.execute = function () { 1259 this.alarm.set(); 1260 } 1261 1262 var alarms = [/* array of alarms */], 1263 i = 0, len = alarms.length; 1264 1265 for (; i < len; i++) { 1266 var enable_alarm = new EnableAlarm(alarms[i]), 1267 disable_alarm = new DisableAlarm(alarms[i]), 1268 reset_alarm = new ResetAlarm(alarms[i]), 1269 set_alarm = new SetAlarm(alarms[i]); 1270 1271 new Button('enable', enable_alarm); 1272 new Button('disable', disable_alarm); 1273 new Button('reset', reset_alarm); 1274 new Button('set', set_alarm); 1275 } 1276 1277 1278 var makeEnableCommand = function (alarm) { 1279 return function () { 1280 alarm.enable(); 1281 } 1282 } 1283 1284 var makeDisableCommand = function (alarm) { 1285 return function () { 1286 alarm.disable(); 1287 } 1288 } 1289 1290 var makeResetCommand = function (alarm) { 1291 return function () { 1292 alarm.reset(); 1293 } 1294 } 1295 1296 var makeSetCommand = function (alarm) { 1297 return function () { 1298 alarm.set(); 1299 } 1300 } 1301 1302 </script> 1303 </body> 1304 </html>