命令模式的例子——菜单程序
使用面向对象的方式
var button1 = document.getElementById('button1');var button2 = document.getElementById('button2');var button3 = document.getElementById('button3');var setCommand = function (button, command) {button.onclick = function () {command.execute();}};var MenuBar = {refresh: function () {console.log('刷新菜单目录');}};var SubMenu = {add: function () {console.log('增加子菜单');},del: function () {console.log('删除子菜单');}};var RefreshMenuBarCommand = function (receiver) {this.receiver = receiver;};RefreshMenuBarCommand.prototype.execute = function () {this.receiver.refresh();};var AddSubMenuCommand = function (receiver) {this.receiver = receiver;};AddSubMenuCommand.prototype.execute = function () {this.receiver.add();};var DelSubMenuCommand = function (receiver) {this.receiver = receiver;};DelSubMenuCommand.prototype.execute = function () {console.log('删除子菜单');};var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);var addSubMenuCommand = new AddSubMenuCommand(SubMenu);var delSubMenuCommand = new DelSubMenuCommand(SubMenu);setCommand(button1, refreshMenuBarCommand);setCommand(button2, addSubMenuCommand);setCommand(button3, delSubMenuCommand);
JavaScript中的命令模式
也许我们会感到很奇怪,所谓的命令模式,看起来就是给对象的某个方法取了execute的名字。引入command对象和receiver这两个无中生有的角色无非是把简单的事情复杂化了,即使不用什么模式,用下面寥寥几行代码就可以实现相同的功能
var bindClick = function (button, func) {button.onclick = func;};var MenuBar = {refresh: function () {console.log('刷新菜单界面');}};var SubMenu = {add: function () {console.log('增加子菜单');},del: function () {console.log('删除子菜单');}};bindClick(button1, MenuBar.refresh);
用闭包实现的命令模式
var RefreshMenuBarCommand = function (receiver) {return {execute: function () {receiver.refresh();}}};var setCommand = function (button, command) {button.onclick = function () {command.execute();}};var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);setCommand(button1, refreshMenuBarCommand);
撤销命令
var ball = document.getElementById('ball');var pos = document.getElementById('pos');var moveBtn = document.getElementById('moveBtn');var cancelBtn = document.getElementById('cancelBtn');var MoveCommand = function (receiver, pos) {this.receiver = receiver;this.pos = pos;this.oldPos = null;};MoveCommand.prototype.execute = function () {this.receiver.start('left', this.pos, 1000, 'strongEaseOut');this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];// 记录小球开始移动前的位置};MoveCommand.prototype.undo = function () {this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut');// 回到小球移动前记录的位置};var moveCommand;moveBtn.onclick = function () {var animate = new Animate(ball);moveCommand = new MoveCommand(animate, pos.value);};moveCommand.execute();cancelBtn.onclick = function () {moveCommand.undo(); // 撤销命令};
撤消和重做
上一节我们讨论了如何撤销一个命令。很多时候,我们需要撤销一系列的命令。比如在一个围棋程序中,现在已经下了10步棋,我们需要一次性悔棋到第5步。在这之前,我们可以把所有执行过的下棋命令都储存在一个历史列表中,然后倒序循环来依次执行这些命令的undo操作,直到循环执行到第5个命令为止。
然而,在某些情况下无法顺利地利用undo操作让对象回到execute之前的状态。比如在一个Canvas画图的程序中,画布上有一些点,我们在这些点之间画了N条曲线把这些点相互连接起来,当然这是用命令模式来实现的。但是我们却很难为这里的命令对象定义一个擦除某条曲线的undo操作,因为在Canvas画图中,擦除一条线相对不容易实现。
这时候最好的办法是先清除画布,然后把刚才执行过的命令全部重新执行一遍,这一点同样可以利用一个历史列表堆栈办到。记录命令日志,然后重复执行它们,这是逆转不可逆命令的一个好办法。
var Ryu = {attack: function () {console.log('攻击');},defense: function () {console.log('防御');},jump: function () {console.log('跳跃');},crouch: function () {console.log('蹲下');}};var makeCommand = function (receiver, state) { // 创建命令return function () {receiver[state]();}};var commands = {"119": "jump", // W"115": "crouch", // S"97": "defense", // A"100": "attack" // D};var commandStack = []; // 保存命令的堆栈document.onkeypress = function (ev) {var keyCode = ev.keyCode,command = makeCommand(Ryu, commands[keyCode]);if (command) {command(); // 执行命令commandStack.push(command); // 将刚刚执行过的命令保存进堆栈}};document.getElementById('replay').onclick = function () { // 点击播放录像var command;while (command = commandStack.shift()) { // 从堆栈里依次取出命令并执行command();}};
宏命令
下面我们看看如何逐步创建一个宏命令。
- 首先,我们依然要创建好各种Command:
var closeDoorCommand = {execute: function () {console.log('关门');}};var openPcCommand = {execute: function () {console.log('开电脑');}};var openQQCommand = {execute: function () {console.log('登录QQ' );}};
- 接下来定义宏命令MacroCommand,它的结构也很简单。macroCommand.add方法表示把子命令添加进宏命令对象,当调用宏命令对象的execute方法时,会迭代这一组子命令对象,并且依次执行它们的execute方法:
var MacroCommand = function () {return {commandsList: [],add: function (command) {this.commandsList.push(command);},execute: function () {for (var i = 0, command; command = this.commandsList[i++];) {command.execute();}}}};var macroCommand = MacroCommand();macroCommand.add(closeDoorCommand);macroCommand.add(openPcCommand);macroCommand.add(openQQCommand);macroCommand.execute();
