JavaScript设计模式笔记-命令模式

定义

假设有一个快餐店,我们会把每个客户的需求全部记录在一个清单上,这样用户不必关系是那个厨师来负责而是关系自己点的餐能不能收到.
这些清单就是命令模式

9.1 命令模式的用途

命令模式最常见的应用场景是:有些时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道被请求的操作是什么.此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接受者能够消除彼此之间的耦合关系.
命令模式把客人订餐的请求封装成command对象,也就是订餐中的订单对象.这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中,这样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接受者之间的耦合关系.

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

假设我们要编写一个有数十个button的菜单界面,某些程序猿负责绘制按钮样式,某些程序猿负责编写点击按钮后的具体行为.那么当完成这个按钮的绘制之后,应该如何给它绑定onclick事件呢?
回想一下命令模式的应用场景:

有些时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道被请求的操作是什么.此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接受者能够消除彼此之间的耦合关系.

我们可以很快找到在这样运用命令模式的理由:点击了按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者.但是目前并不知道接收者是什么对象,也不知道接收者究竟会做什么.
设计模式的主题总是把不变的事物和变化的事物分离开来.按下按钮之后会发生的一些事情是不变的,而具体会发生什么事情是可变的.

1
2
3
4
5
6
7
8
<button id="button1">点击按钮1</button>
<button id="button2">点击按钮2</button>
<button id="button3">点击按钮3</button>
----
var button1 = document.getElementById( 'button1' ),
var button2 = document.getElementById( 'button2' ),
var button3 = document.getElementById( 'button3' );

接下来定义setCommand函数,这个函数负责往按钮上安装命令,可以肯定的是,点击按钮会执行某个command命令,执行命令的动作被约定为调用command对象的execute()方法.这样一来,负责绘制按钮的程序猿不关心这些事情,他只需要预留好安装接口也就是这个setCommand函数,接下来command对象会帮我们自动处理

1
2
3
4
5
var setCommand=function(button,command){
button.onclick=function(){
command.execute();//这个约定是不变的 总是执行这个execute方法
}
}

最后,负责编写点击按钮之后执行的具体行为的程序猿只需要实现这些command的,比如刷新菜单界面,增加子菜单和删除子菜单这几个功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var MenuBar={
refresh:function(){
console.log("刷新菜单目录")
}
}
var SubMenu={
add:function(){
console.log("增加子菜单")
},
del:function(){
console.log("删除子菜单")
}
}

上面这些就是执行者但是并不是command对象,于是我们还得封装一层 加一个command执行类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var RefreshMenuBarCommand=function(receiver){ //刷新command类
this.receiver=receiver
}
RefreshMenuBarCommand.prototype.execute=function(){
this.receiver.refresh()
}
var AddSubMenuCommand=function(receiver){//增加子菜单 command类
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上

1
2
3
4
5
6
7
var refreshMenuBarCommand=new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand=new AddSubMenuCommand(SubMenu);
var delSubMenuCommand=new DelSubMenuCommand(SubMenu);
setCommand(button1,refreshMenuBarCommand);
setCommand(button2,addSubMenuCommand);
setCommand(button3,delSubMenuCommand);

这里给button1绑定了刷新command类,给button2绑定了添加command类,给button3绑定删除command类,这样绘图的程序猿只需要留一个设置类的接口统一执行execute这个方法.

9.3 JavaScript中的命令模式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);

其实这种说法是正确的,9.2节中的示例代码是模拟传统面向对象语言的命令模式实现的.命令模式将过程式的调用封装在command对象的execute方法中,通过封装方法调用,我们可以把运算块包装成形.
命令模式的由来,其实是回调函数的一个面向对象的替代品.
JavaScript作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已经融入到了JavaScript语言之中,运算块不一样要封装在command.execute方法中,也可以被封装在普通函数中,函数作为一等对象本身就可以被四处传递.即时我们依然需要请求“接收者”那也未必使用面向对象的方式,闭包也可以完成同样的功能.
在闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅是执行回调函数即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var setCommand=function(button,cb){
button.onclick=function(){
cb()
}
}
var MenuBar={
refresh:function(){
console.log("刷新菜单界面")
}
}
var RefreshMenuBarCommand=function(receiver){
return function(){
receiver.refresh();
}
}
var refreshMenuBarCommand=RefreshMenuBarCommand(MenuBar);//创建刷新的command类
setCommand(button1,refreshMenuBarCommand);

当然如果未来要添加撤销之类的命令最好还是把执行函数改为调用execute方法比较好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 );

9.4 撤销命令

这里用5.4节中的Animate类来编写一个动画,在input输入相应的数值点击button,小球会移动至输入的数值的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
输入小球移动后的位置:<input id="pos"/>
<button id="moveBtn">开始移动</button>
<script type="text/javascript">
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>

但是我们要增加一个撤销按钮,点击撤销后小球会返回原来所在的位置,虽然我们也可以输入-移动数值来返回原位置. 先把当前代码改成命令模式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var ball = document.getElementById( 'ball' );
var pos = document.getElementById( 'pos' );
var moveBtn = document.getElementById( 'moveBtn' );
var MoveCommand=function(receiver){
this.receiver=receiver;
this.pos=pos;
}
MoveCommand.prototype.execute=function(){
this.receiver.start('left',this.pos,1000,'strongEaseOut');
}
var moveCommand;
moveBtn=function(){
var animate=new Animate(ball);
moveCommand=new MoveCommand(animate,pos.value);//创建命令方法
moveCommand.execute();//执行命令方法
}

接下来增加撤销按钮

1
2
3
4
<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
输入小球移动后的位置:<input id="pos"/>
<button id="moveBtn">开始移动</button>
<button id="cancelBtn">cancel</button> <!--增加取消按钮-->

撤销命令的操作实现一般是给命令对象增加一个名为unexecude或者undo的方法,在该方法里执行execute的反向操作,在command.execute方法让小球开始回到真正运动之前我们需要先记录小球的当前位置,然后在undo方法再让小球回到刚刚的位置.
为了加深理解 下述代码我打算用闭包实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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){
var _receiver=receiver;
var _pos=pos;
var _oldPos=null;
var execute=function(){
_receiver.start("left",this.pos,1000,"strongEaseOut");
_oldPos=receiver.dom.getBoundingClientRect()[receiver.propertyName ];
}
var undo=function(){
_receiver.start("left",_oldPos,1000,"strongEaseOut")
}
return{
execute:execute,
undo:undo
}
}
var moveCommand;
moveBtn.onclick=function(){
var animate=new Animate(ball);
moveCommand=MoveCommand(animate,pos.value);
moveCommand.execute();
}
cancelBtn.onclick=function(){
moveCommand.undo();//撤销命令
}

命令模式的撤销功能还可以实现悔棋这个功能,编辑器的ctrl+z功能

9.5 撤销和重做

比如有时候我们需要撤销一系列命令,比如在一个围棋程序中我们下了10步棋,我们很多时候需要一次性悔棋到第五步.在这之前我们可以把所有执行过的下棋命令都存储在一个历史列表中,然后倒序来循环一次执行这些命令的undo操作,直到循环到第五个命令为止

而然有些情况无法利用undo操作让对象回到execute之前的状态 那么这时候可以逆转思维 先清空当前的情况 然后重新执行保存的execute方法
比如作者编写的HTML5游戏街头霸王的回放功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<body>
<button id='replay'>播放录像</button>
</body>
<script type="text/javascript">
var Ryu={ //执行类
acttack:function(){
console.log("攻击")
},
defense:function(){
console.log("防御")
},
jump:function(){
console.log("跳跃")
},
crouch:function(){
console.log("蹲下")
}
}
var makeCommand=function(receiver,state){ //创建命令
return function(){ //返回command类
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){//按下的键盘为W||A||S||D
command();//执行命令
commandStack.push(command);//保存命令
}
}
document.getElementById("replay").onclick=function(){//点击回放按钮
var command;
while(command=commandStack.shift()){//从堆栈里依次取出命令并且执行
command();
}
}
</script>

9.6 命令队列

队列在动画中的运用场景也非常多,比如之前的小球运动程序有可能遇到另外一个问题,有些用户反馈,这个程序在多次点击也就是连续点击的情况下,此时小球的前一个动画可能尚未结束,于是前一个动画会骤然停止,小球转而开始第二个动画过程.
把请求命令对象的优点在这里再次体现了出来,对象的生命周期几乎是永久的,除非我们去主动回收它,也就是说,命令对象在声明周期跟初始请求发生的时间无关,command对象的execute可以在程序的任何时候执行.
这样一来要实现队列,只需要把这些命令push进一个堆栈,然后当动画执行完毕后通知这个堆栈进行下一个函数.问题在于如何在动画完毕后通知队列 比如回调函数或者订阅——发布模式.
本渣渣来实现下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
var dosth={
hello:function(cb){
console.log("hello")
setTimeout(function(){
cb()
},1000)
}
}
var queue={
commandStack:[],
isrun:false,
next:function(){
console.log("commandStack",this.commandStack)
var fn=this.commandStack.shift();
if(fn){
this.isrun=true;
fn()
}else{
this.isrun=false;
}
},
add:function(fn,cb){
var _cb=function(){//重写回调函数
queue.next()
cb()
}
if(!this.isrun){
this.isrun=true
this.commandStack.push(function(){fn(_cb)})
this.next()
}else{
this.commandStack.push(function(){fn(_cb)})
}
},
}
var createHelloCommand=function(receiver,cb){
return function(){
queue.add(receiver.hello,cb)//添加到队列而不是直接执行
}
}
$("#button1").on("click",function(){
var command=createHelloCommand(dosth,function(){console.log("执行完毕")});
command()
})

貌似没发现什么bug..恳请大牛来纠正

9.7宏命令

宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var closeDoorCommand={
execute:function(){
console.log("关门")
}
}
var openPcCommand={
execute=function(){
console.log("开电脑")
}
}
var openQQCommand={
execute:function(){
console.log("登陆QQ")
}
}

接下来定义宏命令MacroCommand.macroCommand.add方法表示把子命令添加进宏命令对象,当调用宏命令对象的execute方法时,会迭代这一组子命令对象
比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var MacroCommand=function(){
return{
commandsList:[],
add:function(){
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();

9.8 智能命令与傻瓜命令

看一下我们在9.7节创建的命令

1
2
3
4
5
var closeDoorCommand={
execute:function(){
console.log("关门")
}
}

很奇怪,closeDoorCommand中没有包含任何receiver信息,它本身就包揽了执行请求的行为,这跟我们之前看到的任何一个命令对象都包含了一个receiver是矛盾的.
一般来说,命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,这种情况下的命令对象是’傻瓜式’的,它只负责把客户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接受者之前尽可能的解耦.

这里作者谈了与策略模式的不同 请大家购买正版书籍去支持作者 这里不做笔记

9.9 小结

请大家购买正版书籍去支持作者 这里不做笔记