在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。
在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。
再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。
命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
命令模式的主要优点如下。
其缺点是:
命令模式应用场景:有时向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的命令背后的操作是什么此时希望一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
。例如在一个快餐店,用户向服务员点餐。服务员将用户的需求记录在清单上。
现在我们需要实现一个界面,包含很多个按钮。每个按钮有不同的功能,我们利用命令模式来完成。
<html>
<body>
<button id="MenuBar">MenuBar</button>
<button id="SubMenu">SubMenu</button>
<button id="subMenu2">subMenu2</button>
</body>
<script>
var menuBarEl = document.getElementById( 'MenuBar');
var subMenuEl = dxocument.getElementById( 'SubMenu');
var subMenuEl2 = dxocument.getElementById( 'subMenu2');
var setCommand = function(button, command) {
button.addEventListener('click', function() {
command.execute();
});
}
</script>
</html>
// 使用面向对象的思想创建功能函数
var MenuBar= function(name) {
this.name = name;
}
MenuBar.prototype.refresh = function() {
console.log(this.name + '刷新完成');
}
// 使用闭包的思想创建功能函数
var subMenu = function(name) {
return {
add: function() {
console.log(name + '菜单增加完毕');
},
del: function() {
console.log(name + '菜单删除完毕');
}
}
}
// 创建刷新任务命令
var MenuBarCommand = function(receiver) {
this.receiver = receiver;
}
MenuBarCommand.prototype.execute = function() {
this.receiver.refresh();
}
// 创建新增菜单命令
var subMenuAddCommand = function(reciever) {
return {
execute: function() {
reciever.add();
}
}
};
// 创建删除菜单命令
var subMenuDelCommand = function(reciever) {
return {
execute: function() {
reciever.del();
}
}
};
// 服务员需要知道厨师是谁
var menuBarCommand = new MenuBarCommand(new MenuBar('addMenu'));
// 点餐员告诉服务员点餐
setCommand(menuBarEl, menuBarCommand);
// 服务员需要知道厨师是谁(添加菜单)
var subMenuCommand_add = subMenuAddCommand(subMenu('add_subMenu'));
// 点餐员告诉服务员点餐
setCommand(subMenuEl, subMenuCommand_add);
var subMenuCommand_del = subMenuDelCommand(subMenu('del_subMenu'));
setCommand(subMenuEl2, subMenuCommand_del);
var bindClick = function(button, fn) {
button.addEventListener('click', fn);
}
var subMenu = {
add: function(name) {
console.log(name + '菜单增加完毕');
},
del: function(name) {
console.log(name + '菜单删除完毕');
}
bindClick(subMenuEl, subMenu.add('subMenu'));
这样的说法时正确的,之前的实例是模拟传统的面向对象语言的命令模式的实现,命令模式将过程式的请求封装到command对象的execute()方法里。通过封装方法的调用,我们可以把运算块包装成形。command对象可以被四处传递。客户端在调用命令的时候,不需要关心事情是如何进行的。
这里我们还是对点餐的例子来实现。我们向餐厅定了一个盒饭,在6点的送来。
// 顾客点餐
var Customer = function(command) {
return {
book: function(food, time) {
return command.execute(food, time);
},
undo: function(menu) {
command.undo(menu);
}
};
}
// 服务员,拥有点餐方法和撤销点餐方法
var foodCommand = function(cook){
return {
execute: function(food, time) {
var timer = cook.willCook(food, time);
return timer;
},
undo: function(food) {
cook.unCook(food);
}
};
}
// 厨师
var cook = function() {
return {
willCook: function(food, time) {
console.log('时间在' + time + ":开始煮:" + food);
var timer = setTimeout(function() {
console.log(food + '完成了');
}, time);
return timer;
},
unCook: function(timer) {
clearTimeout(timer);
}
};
}
var command = foodCommand(cook());
var customer = Customer(command);
var receipt = customer.book('西红寺炒鸡蛋', 5000); // 5秒后炒完菜
customer.undo(receipt); // 做了取消操作,则不会炒菜
在订餐的故事中,如果订单的数量过多而厨师的人手不够,则应该让这些订单排队处理,第一个订单完成后,再完成第二个订单。
把请求封装成为命令对象的有点:对象的生命周期几乎是永久的,除非我们主动回收它。也就是说,命令对象的生命周期与初始化请求发生的时间无关,命令对象的execute方法可在程序原型的任何时刻执行。即使订单的预定操作早就发生,但是我们的命令对象任然有生命。
我们可以把封装的订单命令压入一个队列堆栈,当前的订单命令执行完毕,就主动通知队列,此时去除正在队列中等待的第一个命令对象并执行它。
我们应该如何在当前的订单完成以后通知队列,通常可以使用队列函数,但是我们还可以选择发布-订阅模式。即在一个订单完成以后发布一个消息,订阅者接收到这个消息,开始执行下一个订单内容。
下面是错误的例子,我还不知道怎么讲发布订阅和命令模式结合在一起,谁看到了请指点一下:
var Customer = function(command) {
return {
book: function(food) {
return command.execute(food);
}
};
}
var Command = function(cook) {
return {
execute: function(food) {
cook.subscribe('cook', function() {
console.log(food + 'complete');
});
}
};
}
var Cook = function() {
var cache = [];
return {
subscribe: function(food, fn) {
if (!(food in cache)) {
cache[food] = [];
}
cache[food].push(fn);
},
notify: function(food) {
var fns = cache[food];
if (!fns || fns.length === 0) {
return;
}
fns.map(function(fn) {
fn();
});
}
};
}
var cook = Cook();
var customer = Customer(Command(cook));
customer.book('青菜炒茄子');
customer.book('黄瓜炒花椒');
cook.notify('cook');
跟许多其他语言不同, JavaScript 可以用高阶函数非常方便地实现命令模式。命令模式在 JavaScript 语言中是一种隐形的模式。