命令模式是将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
主要解决的问题:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
命令模式的结构
Command:定义命令的接口,声明执行的方法。
ConcreteCommand:命令接口实现对象,是"虚"的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
Receiver:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
Invoker:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
Client:创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。
命令模式不一定具备上面的所有结构,简单的命令模式可以只有命令的发布者和命令的接收者,这种简单命令模式中,接受者和执行者是同一个对象。比如:小明的妈妈告诉小明说:“今天中午妈妈会很忙,没时间出去买菜。你放学回来的时候,顺便帮妈妈把菜买回来”,在这个例子中,命令的发布者是小明的妈妈,小明是命令的接收者和执行者:
//发布命令者 var Command = function(receiver){ this.receiver = receiver; }; Command.prototype.execute = function(){ this.receiver.action(); }; //命令的执行者和接受者 var Receiver = function(name){ this.name = name; }; Receiver.prototype.action = function(){ alert(this.name + "中午放学后顺便把菜买了!"); }; var xiaoming = new Receiver("小明"); var xiaomingmama = new Command(xiaoming); xiaomingmama.execute();
命令模式例子——菜单命令
现在页面中有三个button元素和一个菜单程序界面,这三个button元素的作用是:当用户点击时,他们分别会执行“刷新菜单”、“添加子菜单”和“删除子菜单”这三个功能。如果由一个程序员来完成这个功能就非常的简单了,因为他非常清楚那个按钮对应那个功能。但是如果实在一个分工很细致的团队里就不是这样了,例如一个人负责写html和css样式布局,另一个人负责写js,他们两个人同时进行各自的工作。在这种情况下,负责写js的那个人就很难确定哪个按钮对应哪个功能了。我们回想一下命令模式的使用场景:
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
我们发现这种情况非常符合使用命令模式。
HTML按钮结构:
<button class="ref">刷新菜单</button> <button class="add">添加子菜单</button> <button class="del">删除子菜单</button>
首先我们对功能进行封装,将三个功能分别封装到Menu和SubMenu两个对象中:
//菜单对象 var Menu = { refresh: function(){ console.log("刷新菜单"); } }; //子菜单 var SubMenu = { add: function(){ console.log('增加子菜单'); }, del: function(){ console.log('删除子菜单'); } };
然后封装三个功能调用的命令:
//封装刷新菜单命令 var refreshMenuCommand = function(receiver){ this.receiver = receiver; } refreshMenuCommand.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(); }
在然后是封装设置命令函数:
//设置命令函数 function setCommand(btn, command){ btn.addEventListener("click", function(){ command.execute(); }) };
最后是客户端(client)的调用:
//client客户调用 var refreshCommand = new refreshMenuCommand(Menu); var addCommand = new addSubMenuCommand(SubMenu); var delCommand = new delSubMenuCommand(SubMenu); var btn = document.querySelectorAll("button"); setCommand(btn[0], refreshCommand); setCommand(btn[1], addCommand); setCommand(btn[2], delCommand);
JavaScript中的命令模式:
大家细看上面菜单的例子,会发现实现一个这么简单的功能,竟然弄得代码这么复杂难懂。即使不用什么模式,用下面几行代码就可以实现相同的功能:
//菜单对象 var Menu = { refresh: function(){ console.log("刷新菜单"); } }; //子菜单 var SubMenu = { add: function(){ console.log('增加子菜单'); }, del: function(){ console.log('删除子菜单'); } }; //事件绑定函数 function addEvent(dom, fn, Capture){ dom.addEventListener("click", fn, !!Capture); };
var btn = document.querySelectorAll("button"); addEvent(btn[0], Menu.refresh); addEvent(btn[1], SubMenu.add); addEvent(btn[2], SubMenu.del);
这就是JavaScript语言中的命令模式,在JavaScript语言中函数是一等对象,它可以作为一个参数传递到函数内部去执行。所以在JavaScript这门语言中,命令模式和策略模式一样是JavaScript这门语言的天赋(生来即具有的属性)或者是隐性模式。命令模式其实就是回调函数一个面向对象的替代品,在JavaScript中命令模式和策略模式一样依赖回调函数实现,使用起来也更简单、更便捷。但有些时候这会成为一种缺点,因为他无法执行撤销操作,所以在实现撤销操作时,我们最好还是使用命令对象的execute方法为好。
撤销操作的实例
现在页面中有一个元素,有两个按钮,其中一个按钮点击时元素会往右移动一段距离,另一个按钮是撤销上一个移动操作。用命令模式实现这个功能,代码如下:
HTML结构:
<div class="demo"> <button class="move">移动</button> <button class="undo">撤销</button> <div class="target" style="left:0"></div> </div>
CSS样式:
.demo{ width:100%; height:100px; position:relative; } .target{ width:50px; height:50px; position:absolute; bottom:0; background-color:red; }
js代码:
//移动对象 var Animate = function(dom){ this.dom = dom; var self = this; //移动函数 this.move = function(){ var left = parseInt(self.dom.style.left); self.dom.style.left = left + 10 + 'px'; }; //取消移动函数 this.undo = function(){ var left = parseInt(self.dom.style.left); self.dom.style.left = left - 10 + 'px'; }; }; //命令对象 var Command = function(receiver){ var self = this; this.receiver = receiver; this.count = 0; //记录执行命令的次数 //执行命令 this.execute = function(){ self.receiver.move(); self.count+= 1; }; //撤销命令 this.unexecute = function(){ if(self.count === 0) return; self.receiver.undo(); self.count-= 1; } }; //设置命令函数 function addEvent(dom, fn, Capture){ dom.addEventListener("click", fn, !!Capture); }; //client调用 var dom = document.querySelector(".target"); var moveBtn = document.querySelector(".move"); var unmoveBtn = document.querySelector(".undo"); var m = new Animate(dom); var c = new Command(m); addEvent(moveBtn, c.execute); addEvent(unmoveBtn, c.unexecute);
命令模式的优缺点:
优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。