命令模式的例子——菜单程序

使用面向对象的方式

  1. var button1 = document.getElementById('button1');
  2. var button2 = document.getElementById('button2');
  3. var button3 = document.getElementById('button3');
  4. var setCommand = function (button, command) {
  5. button.onclick = function () {
  6. command.execute();
  7. }
  8. };
  9. var MenuBar = {
  10. refresh: function () {
  11. console.log('刷新菜单目录');
  12. }
  13. };
  14. var SubMenu = {
  15. add: function () {
  16. console.log('增加子菜单');
  17. },
  18. del: function () {
  19. console.log('删除子菜单');
  20. }
  21. };
  22. var RefreshMenuBarCommand = function (receiver) {
  23. this.receiver = receiver;
  24. };
  25. RefreshMenuBarCommand.prototype.execute = function () {
  26. this.receiver.refresh();
  27. };
  28. var AddSubMenuCommand = function (receiver) {
  29. this.receiver = receiver;
  30. };
  31. AddSubMenuCommand.prototype.execute = function () {
  32. this.receiver.add();
  33. };
  34. var DelSubMenuCommand = function (receiver) {
  35. this.receiver = receiver;
  36. };
  37. DelSubMenuCommand.prototype.execute = function () {
  38. console.log('删除子菜单');
  39. };
  40. var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
  41. var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
  42. var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
  43. setCommand(button1, refreshMenuBarCommand);
  44. setCommand(button2, addSubMenuCommand);
  45. setCommand(button3, delSubMenuCommand);

JavaScript中的命令模式

也许我们会感到很奇怪,所谓的命令模式,看起来就是给对象的某个方法取了execute的名字。引入command对象和receiver这两个无中生有的角色无非是把简单的事情复杂化了,即使不用什么模式,用下面寥寥几行代码就可以实现相同的功能

  1. var bindClick = function (button, func) {
  2. button.onclick = func;
  3. };
  4. var MenuBar = {
  5. refresh: function () {
  6. console.log('刷新菜单界面');
  7. }
  8. };
  9. var SubMenu = {
  10. add: function () {
  11. console.log('增加子菜单');
  12. },
  13. del: function () {
  14. console.log('删除子菜单');
  15. }
  16. };
  17. bindClick(button1, MenuBar.refresh);

用闭包实现的命令模式

  1. var RefreshMenuBarCommand = function (receiver) {
  2. return {
  3. execute: function () {
  4. receiver.refresh();
  5. }
  6. }
  7. };
  8. var setCommand = function (button, command) {
  9. button.onclick = function () {
  10. command.execute();
  11. }
  12. };
  13. var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
  14. setCommand(button1, refreshMenuBarCommand);

撤销命令

  1. var ball = document.getElementById('ball');
  2. var pos = document.getElementById('pos');
  3. var moveBtn = document.getElementById('moveBtn');
  4. var cancelBtn = document.getElementById('cancelBtn');
  5. var MoveCommand = function (receiver, pos) {
  6. this.receiver = receiver;
  7. this.pos = pos;
  8. this.oldPos = null;
  9. };
  10. MoveCommand.prototype.execute = function () {
  11. this.receiver.start('left', this.pos, 1000, 'strongEaseOut');
  12. this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
  13. // 记录小球开始移动前的位置
  14. };
  15. MoveCommand.prototype.undo = function () {
  16. this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut');
  17. // 回到小球移动前记录的位置
  18. };
  19. var moveCommand;
  20. moveBtn.onclick = function () {
  21. var animate = new Animate(ball);
  22. moveCommand = new MoveCommand(animate, pos.value);
  23. };
  24. moveCommand.execute();
  25. cancelBtn.onclick = function () {
  26. moveCommand.undo(); // 撤销命令
  27. };

撤消和重做

上一节我们讨论了如何撤销一个命令。很多时候,我们需要撤销一系列的命令。比如在一个围棋程序中,现在已经下了10步棋,我们需要一次性悔棋到第5步。在这之前,我们可以把所有执行过的下棋命令都储存在一个历史列表中,然后倒序循环来依次执行这些命令的undo操作,直到循环执行到第5个命令为止。
然而,在某些情况下无法顺利地利用undo操作让对象回到execute之前的状态。比如在一个Canvas画图的程序中,画布上有一些点,我们在这些点之间画了N条曲线把这些点相互连接起来,当然这是用命令模式来实现的。但是我们却很难为这里的命令对象定义一个擦除某条曲线的undo操作,因为在Canvas画图中,擦除一条线相对不容易实现。
这时候最好的办法是先清除画布,然后把刚才执行过的命令全部重新执行一遍,这一点同样可以利用一个历史列表堆栈办到。记录命令日志,然后重复执行它们,这是逆转不可逆命令的一个好办法。

  1. var Ryu = {
  2. attack: function () {
  3. console.log('攻击');
  4. },
  5. defense: function () {
  6. console.log('防御');
  7. },
  8. jump: function () {
  9. console.log('跳跃');
  10. },
  11. crouch: function () {
  12. console.log('蹲下');
  13. }
  14. };
  15. var makeCommand = function (receiver, state) { // 创建命令
  16. return function () {
  17. receiver[state]();
  18. }
  19. };
  20. var commands = {
  21. "119": "jump", // W
  22. "115": "crouch", // S
  23. "97": "defense", // A
  24. "100": "attack" // D
  25. };
  26. var commandStack = []; // 保存命令的堆栈
  27. document.onkeypress = function (ev) {
  28. var keyCode = ev.keyCode,
  29. command = makeCommand(Ryu, commands[keyCode]);
  30. if (command) {
  31. command(); // 执行命令
  32. commandStack.push(command); // 将刚刚执行过的命令保存进堆栈
  33. }
  34. };
  35. document.getElementById('replay').onclick = function () { // 点击播放录像
  36. var command;
  37. while (command = commandStack.shift()) { // 从堆栈里依次取出命令并执行
  38. command();
  39. }
  40. };

宏命令

下面我们看看如何逐步创建一个宏命令。

  • 首先,我们依然要创建好各种Command:
  1. var closeDoorCommand = {
  2. execute: function () {
  3. console.log('关门');
  4. }
  5. };
  6. var openPcCommand = {
  7. execute: function () {
  8. console.log('开电脑');
  9. }
  10. };
  11. var openQQCommand = {
  12. execute: function () {
  13. console.log('登录QQ' );
  14. }
  15. };
  • 接下来定义宏命令MacroCommand,它的结构也很简单。macroCommand.add方法表示把子命令添加进宏命令对象,当调用宏命令对象的execute方法时,会迭代这一组子命令对象,并且依次执行它们的execute方法:
  1. var MacroCommand = function () {
  2. return {
  3. commandsList: [],
  4. add: function (command) {
  5. this.commandsList.push(command);
  6. },
  7. execute: function () {
  8. for (var i = 0, command; command = this.commandsList[i++];) {
  9. command.execute();
  10. }
  11. }
  12. }
  13. };
  14. var macroCommand = MacroCommand();
  15. macroCommand.add(closeDoorCommand);
  16. macroCommand.add(openPcCommand);
  17. macroCommand.add(openQQCommand);
  18. macroCommand.execute();