第九章 命令模式
命令对象:快餐店的所有外卖信息都会形成一个清单。有了这个清单,厨房可以按照订单顺序炒菜,客户也可以很方便地打电话撤销订单。这些记录订餐信息的清单,就是命令模式中的命令对象。
9.1 命令模式的用途
命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
9.2 命令模式的一个例子——菜单程序
假设我们正在编写一个用户界面程序,该用户界面上至少有数十个 Button 按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。
设计模式的主题总是把不变的事物和变化的事物分离开来,命令模式也不例外。按下按钮之后会发生一些事情是不变的,而具体会发生什么事情是可变的。通过 command 对象的帮助,将来我们可以轻易地改变这种关联,因此也可以在将来再次改变按钮的行为。
html1<button id="button1">点击按钮1</button> 2<button id="button2">点击按钮2</button> 3<button id="button3">点击按钮3</button>
命令模式的第一步,是定义一个 setCommand 函数,负责安装命令。
js1var button1 = document.getElementById('button1') 2var button2 = document.getElementById('button2') 3var button3 = document.getElementById('button3') 4 5var setCommand = function (button, command) { 6 button.onclick = function () { 7 command.execute() // 这里执行命令的动作被约定为调用command对象的execute方法 8 } 9}
第二步,是将要做的行为都收集起来,这些用来收集行为的对象被称为命令接收者
js1//所有要做的行为 receiver 2var MenuBar = { 3 refresh: function() { 4 console.log(’刷新菜单目录’); 5 } 6}; 7 8var SubMenu = { 9 add: function() { 10 console.log(’增加子菜单’); 11 }, 12 del: function() { 13 console.log(’删除子菜单’); 14 } 15};
第三步,把这些定位都封装到命令类中
js1// 刷新类 2class RefreshMenuBarCommand { 3 constructor(receiver) { 4 this.receiver = receiver 5 } 6 execute() { 7 this.receiver.refresh() 8 } 9} 10// 增加菜单类 11class AddSubMenuCommand { 12 constructor(receiver) { 13 this.receiver = receiver 14 } 15 execute() { 16 this.receiver.add() 17 } 18} 19// 删除菜单类 20class DelSubMenuCommand { 21 constructor(receiver) { 22 this.receiver = receiver 23 } 24 execute() { 25 this.receiver.del() 26 } 27}
最后是将命令接收者传递给 setCommand 方法来将命令安装到 button 上
js1var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar) 2var addSubMenuCommand = new AddSubMenuCommand(SubMenu) 3var delSubMenuCommand = new DelSubMenuCommand(SubMenu) 4 5setCommand(button1, refreshMenuBarCommand) 6setCommand(button2, addSubMenuCommand) 7setCommand(button3, delSubMenuCommand)
9.3 JavaScript 中的命令模式
上面的命令模式,是模拟传统面向对象语言的命令模式实现。命令模式将过程式的请求调用封装在 command 对象的 execute 方法里,通过封装方法调用,我们可以把运算块包装成形。command 对象可以被四处传递,所以在调用命令的时候,客户(Client)不需要关心事情是如何进行的。
看起来整个过程就是给一个对象起一个 execute 方法。引入 command 对象和 receiver 这两个角色对于 JavaScript 这种函数为一等对象的语言来说,实在是将简单的事情复杂化了,我们给他做一个改写:
js1var setCommand = function (button, commandFunc) { 2 button.onclick = commandFunc 3} 4//所有要做的行为 5var MenuBar = { 6 refresh: function () { 7 console.log('刷新菜单目录') 8 }, 9} 10 11var SubMenu = { 12 add: function () { 13 console.log('增加子菜单') 14 }, 15 del: function () { 16 console.log('删除子菜单') 17 }, 18} 19 20setCommand(button1, MenuBar.refresh) 21setCommand(button1, SubMenu.add) 22setCommand(button1, SubMenu.del)
跟策略模式一样,命令模式早已融入 JavaScript 语言当中。我们不需要再封装一个 execute 方法,而是封装在普通函数中,就可以传递起来。
如果想要显式表示是命令模式,或者未来可能会添加撤销命令的那么可以继续使用 execute 方法。
js1var setCommand = function (button, command) { 2 button.onclick = function () { 3 command.execute() 4 } 5} 6//所有要做的行为 7var MenuBar = { 8 refresh: function () { 9 console.log('刷新菜单目录') 10 }, 11} 12 13var SubMenu = { 14 add: function () { 15 console.log('增加子菜单') 16 }, 17 del: function () { 18 console.log('删除子菜单') 19 }, 20} 21 22const refreshCommand = { 23 execute() { 24 MenuBar.refresh() 25 }, 26} 27const addCommand = { 28 execute() { 29 SubMenu.add() 30 }, 31} 32const delCommand = { 33 execute() { 34 SubMenu.del() 35 }, 36} 37 38setCommand(button1, refreshCommand) 39setCommand(button1, addCommand) 40setCommand(button1, delCommand)
本质上就是将类换成对象,由于函数可以自由传递,所以 receiver 可以不用传递。
9.4 撤销命令
撤销操作的实现一般是给命令对象增加一个名为 unexecude 或者 undo 的方法,在该方法里执行 execute 的反向操作。
比如下面定义设置命令的方法
js1function setCommand(target, command) { 2 target.onclick = command.execute.bind(command) 3} 4 5function delCommand(target, command) { 6 target.onclick = command.undo.bind(command) 7}
定义一个命令类:接收一个 receiver
js1class Command { 2 constructor(receiver) { 3 this.receiver = receiver 4 } 5 execute() { 6 this.receiver.add() 7 } 8 undo() { 9 this.receiver.del() 10 } 11}
收集 receiver 的行为
js1var SubMenu = { 2 add: function () { 3 console.log('增加子菜单') 4 }, 5 del: function () { 6 console.log('删除子菜单') 7 }, 8}
使用命令
js1setCommand(button1, new Command(SubMenu)) 2delCommand(button2, new Command(SubMenu))
9.7 宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电脑并登录 QQ。
创建宏命令的第一步,就是制定好执行的指令:
js1class closeDoorCommand { 2 execute() { 3 console.log('开门') 4 } 5} 6 7class OpenCompute { 8 execute() { 9 console.log('打开电脑') 10 } 11} 12 13class LoginQQ { 14 execute() { 15 console.log('登录QQ') 16 } 17}
第二步,是创建一个宏命令,宏命令有 add 方法,接收命令为参数,调用后会将命令添加到宏命令列表中。
js1class MacroCommand { 2 macroList = [] 3 add(commander) { 4 this.macroList.push(commander) 5 } 6 execute() { 7 for (let commander of this.macroList) { 8 commander.execute() 9 } 10 } 11}
第三步:添加设置命令的方法
js1function setCommand(target, command) { 2 command.execute.call(command) 3}
最后一步,添加命令并使用
js1const macroCommand = new MacroCommand() 2macroCommand.add(new closeDoorCommand()) 3macroCommand.add(new OpenCompute()) 4macroCommand.add(new LoginQQ()) 5 6setCommand(button1, macroCommand)
当设置命令后,macroCommand会执行内部的execute方法,execute方法将遍历宏命令列表,并执行收集好的commander.execute方法。
当然我们也可以为宏命令添加撤销功能,跟macroCommand.execute类似,当调用macroCommand.undo时, 宏命令里包含所有的字命令对象要依次执行各自的 undo 操作。
9.8 智能命令和傻瓜命令
js1 var closeDoorCommand = { 2 execute: function(){ 3 console.log( ’关门’ ); 4 } 5 };
closeDoorCommand 中没有包含任何 receiver 的信息,它本身就包揽了执行请求的行为,这跟我们之前看到的命令对象都包含了一个 receiver 是矛盾的。
一般来说,命令模式都会在 command 对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦。
但是我们也可以定义一些更“聪明”的命令对象,“聪明”的命令对象可以直接实现请求,这样一来就不再需要接收者的存在,这种“聪明”的命令对象也叫作智能命令。
没有接收者的智能命令,退化到和策略模式非常相近,从代码结构上已经无法分辨它们,能分辨的只有它们意图的不同。
- 策略模式指向的问题域更小,所有策略对象的目标总是一致的,它们只是达到这个目标的不同手段,它们的内部实现是针对“算法”而言的。
- 智能命令模式指向的问题域更广,command 对象解决的目标更具发散性。命令模式还可以完成撤销、排队等功能。
9.9 小结
Javascript 可以用高阶函数非常方便地实现命令模式。命令模式在 JavaScript 中是一种隐形的模式。