Javascript命令模式
- 1 什么是命令模式
- 2 命令模式的例子—菜单程序
- 3 JavaScript 中的命令模式
- 4 撤销命令
- 5 宏命令
1 什么是命令模式
在一个餐厅中,当客人现场点餐或者打电话订餐时,老板会把客人的需求写在清单上,厨师会按照清单的顺序给客人炒菜,有时餐厅还可以满足定时点餐,比如x时间之后再上菜,如果客人需要撤销订单,可以直接打电话给餐厅。
这些记录着订餐信息的清单,就是命令模式中的命令对象。
命令模式允许将功能请求封装为独立的对象,并根据需要将其排队、记录、撤销或重做。该模式的核心是将请求与其调用者分离,从而使命令对象独立于发送者和接收者。
在命令模式中,有4个主要角色:命令对象、客户端、接收者和调用者。命令对象封装了一个特定的功能请求,而客户端创建命令对象并将其传递给调用者。调用者是负责执行命令的对象,并将其发送给接收者执行所需的操作。
命令模式在Javascript中有很多应用场景,例如撤销和重做功能、动态添加命令、撤销命令、实现编辑器、处理用户交互等。
对于订餐来说,客人需要向厨师发送请求,但是并不关心厨师的名字和联系方式,也不知道厨师炒菜的方式和步骤。命令模式把客人订餐的请求封装成command
对象,这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中。这样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。
2 命令模式的例子—菜单程序
假设我们封装一个按钮组的组件,每个按钮都有自己的click
事件,对于封装这个组件的人来说,并不关心这个按钮点击之后接收者是什么对象,也不知道接收者究竟会做什么,这时我们可以借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合,首先是按钮组绘制:
<button id="button1">点击按钮1</button>
<button id="button2">点击按钮2</button>
<button id="button3">点击按钮3</button>
<script>
var button1 = document.getElementById("button1");
var button2 = document.getElementById("button2");
var button3 = document.getElementById("button3");
</script>
接下来定义setCommand
函数,setCommand
函数负责往按钮上面安装命令。可以肯定的是,点击按钮会执行某个command
命令,执行命令的动作被约定为调用command
对象的execute()
方法。
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
};
};
最后是按钮的点击事件,点击按钮后分别由刷新菜单界面、增加子菜单和删除子菜单这几个功能,这几个功能被分布在MenuBar
和SubMenu
这两个对象中:
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 () {
this.receiver.del();
};
最后把命令接收者传入到command
对象中,并且把command
对象安装到button
上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);
以上只是一个很简单的命令模式示例,但从中可以看到我们是如何把请求发送者和请求接收
者解耦开的。
3 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);
bindClick(button2, SubMenu.add);
bindClick(button3, SubMenu.del);
这种说法是正确的,上面的示例代码是模拟传统面向对象语言的命令模式实现。命令模式将过程式的请求调用封装在command
对象的execute
方法里,通过封装方法调用,我们可以把运算块包装成形,command
对象可以被四处传递,所以在调用命令的时候,客户不需要关心事情是如何进行的。
跟策略模式一样,命令模式也早已融入到了JavaScript
语言之中。运算块不一定要封装在command.execute
方法中,也可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。即使我们依然需要请求“接收者”,那也未必使用面向对象的方式,闭包可以完成同样的功能。
4 撤销命令
下面利用策略模式中Animate
类编写一个动画,Animate
类指路:JavaScript策略模式
这个动画的表现是让页面上的正方形移动到水平方向的某个位置。现在页面中有一个input
文本框和一个button
按钮,文本框中可以输入一些数字,表示正方形移动后的水平位置,正方形在用户点击按钮后立刻开始移动,代码如下:
<div id="ball" class="ball"></div>
请输入移动后的位置:<input id="pos" />
<button id="moveBtn">开始移动</button>
<script>
var ball = document.getElementById("ball");
var pos = document.getElementById("pos");
var moveBtn = document.getElementById("moveBtn");
moveBtn.onclick = function () {
var animate = new Animate(ball);
animate.start("left", pos.value, 1000, "strongEaseOut");
};
</script>
.ball {
position: absolute;
background-color: pink;
width: 50px;
height: 50px;
}
如果文本框输入200,然后点击moveBtn
按钮,可以看到正方形顺利地移动到水平方向200px的位置。现在我们需要一个方法让它还原到开始移动之前的位置,在页面上设计一个撤销按钮,点击撤销按钮之后,小球便能回到上一次的位置。
在给页面中增加撤销按钮之前,先把目前的代码改为用命令模式实现:
var ball = document.getElementById("ball");
var pos = document.getElementById("pos");
var moveBtn = document.getElementById("moveBtn");
var MoveCommand = function (receiver, pos) {
this.receiver = receiver;
this.pos = pos;
};
MoveCommand.prototype.execute = function () {
this.receiver.start("left", this.pos, 1000, "strongEaseOut");
};
var moveCommand;
moveBtn.onclick = function () {
var animate = new Animate(ball);
moveCommand = new MoveCommand(animate, pos.value);
moveCommand.execute();
};
接下来增加撤销按钮:
<div id="ball" class="ball"></div>
请输入移动后的位置:<input id="pos" />
<button id="moveBtn">开始移动</button>
<button id="cancelBtn">撤销</button>
撤销操作的实现一般是给命令对象增加一个名为unexecude
或者undo
的方法,在该方法里执行execute
的反向操作。在command.execute
方法让正方形开始真正运动之前,我们需要先记录正方形的当前位置,在unexecude
或者undo
操作中,再让它回到刚刚记录下的位置,代码如下:
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(); // 撤销命令
};
现在通过命令模式轻松地实现了撤销功能。如果用普通的方法调用来实现,也许需要每次都手工记录小球的运动轨迹,才能让它还原到之前的位置。而命令模式中小球的原始位置在小球开始移动前已经作为command
对象的属性被保存起来,所以只需要再提供一个undo
方法,并且在undo
方法中让小球回到刚刚记录的原始位置就可以了。
5 宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
比如说,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电脑并登录游戏。
下面我们逐步创建一个宏命令,首先,我们依然要创建好各种Command
:
var closeDoorCommand = {
execute: function () {
console.log("关门");
},
};
var openPcCommand = {
execute: function () {
console.log("开电脑");
},
};
var openGameCommand = {
execute: function () {
console.log("打开游戏");
},
};
接下来定义宏命令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();