命令模式(Command),将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
从命令模式的结构图可以看出,它涉及到五个角色,它们分别是:
- 客户角色:发出一个具体的命令并确定其接受者。
- 命令角色:声明了一个给所有具体命令类实现的抽象接口
- 具体命令角色:定义了一个接受者和行为的弱耦合,负责调用接受者的相应方法。
- 请求者角色:负责调用命令对象执行命令。
- 接受者角色:负责具体行为的执行。
在下面的情况下可以考虑使用命令模式:
- 系统需要支持命令的撤销(undo)。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo方法吧命令所产生的效果撤销掉。命令对象还可以提供redo方法,以供客户端在需要时,再重新实现命令效果。
- 系统需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命周期。意思为:原来请求的发出者可能已经不存在了,而命令对象本身可能仍是活动的。这时命令的接受者可以在本地,也可以在网络的另一个地址。命令对象可以串行地传送到接受者上去。
- 如果一个系统要将系统中所有的数据消息更新到日志里,以便在系统崩溃时,可以根据日志里读回所有数据的更新命令,重新调用方法来一条一条地执行这些命令,从而恢复系统在崩溃前所做的数据更新。
- 系统需要使用命令模式作为“CallBack(回调)”在面向对象系统中的替代。Callback即是先将一个方法注册上,然后再以后调用该方法。
C#命令模式:
namespace 命令模式 { class Program { static void Main(string[] args) { //开店前的准备 Barbecuer boy = new Barbecuer(); Command bakeMuttonCommand1 = new BakeMuttonCommand(boy); Command bakeMuttonCommand2 = new BakeMuttonCommand(boy); Command bakeChickenWingCommand1 = new BakeChickenWingCommand(boy); Waiter girl = new Waiter(); //开门营业 顾客点菜 girl.SetOrder(bakeMuttonCommand1); girl.SetOrder(bakeMuttonCommand2); girl.SetOrder(bakeChickenWingCommand1); //点菜完闭,通知厨房 girl.Notify(); Console.Read(); } } //服务员 public class Waiter { private IList<Command> orders = new List<Command>(); //设置订单 public void SetOrder(Command command) { if (command.ToString() == "命令模式.BakeChickenWingCommand") { Console.WriteLine("服务员:鸡翅没有了,请点别的烧烤。"); } else { orders.Add(command); Console.WriteLine("增加订单:" + command.ToString() + " 时间:" + DateTime.Now.ToString()); } } //取消订单 public void CancelOrder(Command command) { orders.Remove(command); Console.WriteLine("取消订单:" + command.ToString() + " 时间:" + DateTime.Now.ToString()); } //通知全部执行 public void Notify() { foreach (Command cmd in orders) { cmd.ExcuteCommand(); } } } //抽象命令 public abstract class Command { protected Barbecuer receiver; public Command(Barbecuer receiver) { this.receiver = receiver; } //执行命令 abstract public void ExcuteCommand(); } //烤羊肉串命令 class BakeMuttonCommand : Command { public BakeMuttonCommand(Barbecuer receiver) : base(receiver) { } public override void ExcuteCommand() { receiver.BakeMutton(); } } //烤鸡翅命令 class BakeChickenWingCommand : Command { public BakeChickenWingCommand(Barbecuer receiver) : base(receiver) { } public override void ExcuteCommand() { receiver.BakeChickenWing(); } } //烤肉串者 public class Barbecuer { public void BakeMutton() { Console.WriteLine("烤羊肉串!"); } public void BakeChickenWing() { Console.WriteLine("烤鸡翅!"); } } }
Javascript中的命令模式
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);
命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
用闭包实现的命令模式如下代码所示:
var setCommand = function(button,func){ button.onclick = function(){ func(); } }; var MenuBar = { refresh:function(){ console.log('刷新菜单界面'); } }; var RefreshMenuBarCommand = function(receiver){ return function(){ receiver.refresh(); } }; var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar); setCommand(button1,refreshMenuBarCommand);
将来有可能还要提供撤销的命令等操作。那我们最好还是把执行函数改为调用execute方法:
var RefreshMenuBarCommand = function(receiver){ return{ execute:function(){ receiver.refresh(); } } }; var setCommand = function(button,command){ button.onclick = function(){ command.execute(); } }; var refreshMenuCommand = RefreshMenuBarCommand(MenuBar); setCommand(button1,refreshMenuCommand);
撤销命令
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(); };
撤销操作的实现一般是给命令对象增加一个名为unexcude或者undo的方法,在该方法里执行execute的反向操作。
<script> var ball = doucment.getElementById('ball'); var pos = doucment.getElementById('pos'); var moveBtn = doucment.getElementById('moveBtn'); var cancelBtn = doucment.getElementById('cancelBtn'); var MoveCommand = function(receiver,pos){ this.receicer = 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(); //撤销命令 }; </script>
撤销和重做
比如在一个围棋程序中,现在已经下了10步棋,我们需要一次性悔棋到第5步。在这之前,我们可以把所有执行过的下棋命令都存储在一个历史列表 中,Cnavas画图的程序中,我们却很难为这里的命令对象定义一个擦除某条曲线的undo操作,这时候最好的办法是先清除画布,然后把刚才执行过的命令 全部重新执行一遍,这一点同样可以利用一个历史列表堆栈办到。这是逆转不可逆命令的一个好办法。
<html> <body> <button id="replay">播放录像</button> </body> <script> 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(); } }; </script> </html>
命令队列
一个动画结束后该如何通知队列。通常可以使用回调函数来通知队列,除了回调函数之外,还可以选择发布-订阅模式。即在一个动画结束后发布一个消息,订阅者接收到这个消息之后,便开始执行队列里的下一个动画。
宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。
var closeDoorCommand = { execute:function(){ console.log('关门'); } }; var openPcCommand = { execute:function(){ console.log('开电脑'); } }; var openQQCommand = { execute:function(){ console.log('登录QQ'); } }; var MacroCmmand = 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();
宏命令是命令模式与组合模式的联用产物。
智能命令与傻瓜命令
一般来说,命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客
户的请求转交给接收者来执行,这种模式的好处是请求发起者和请求接收者之间尽可能地得到了解耦。
但是我们也可以定义一些更“聪明”的命令对象,“聪明”的命令对象可以直接实现请求,这样一来就不再需要接收者的存在,这种“聪明
”的命令对象也叫作智能命令。