什么是命令模式
用一个生活例子来介绍。假如你是一家餐厅的服务员,那么你的工作应该是这样的:
当某位客人点餐后,你要根据客人点的菜在电脑上创建一条订单,创建成功后,这条订单进入订单列表中,厨房会收到新订单提醒开始做菜,客人不用关心是那位厨师帮他炒菜,只要在桌子上等菜就行。
当某位客人打电话订餐,要求一个小时后开始炒他的菜,你要根据客人点的菜在电脑上创建一条订单,并注明一个小时后开始炒,厨房会在一个小时后收到新订单提醒开始做菜。假如过了半个小时,客人有事来不了,打电话过来取消了,你要在订单列表找到这个客人的订单,取消订单即可,如果超过一个小时,就不能取消订单了。
如果有太多的客人点餐,厨房可以按照订单列表中的订单顺序排队炒菜。
上面就是一种命令模式,客人到餐厅吃饭,本质上是客人向厨师发起请求,厨师接收到请求后开始炒菜,但是客人不认识厨师,怎么办呢?餐厅就靠订单列表把客人和厨师关联起来,通过订单列表,客人就可以命令厨师开始炒菜,这些记录着订餐信息的订单列表,便是命令模式中的命令对象。
命令模式的用途
命令模式中的命令(command)指的是一个执行某些特定事情的指令。其最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
拿订餐来说,客人需要向厨师发送请求,但是客人完全不知道这些厨师的名字和联系方式。命令模式把客人订餐的请求封装成command对象,也就是订餐中的订单列表对象。这个对象可以在程序中被四处传递,就像订单列表可以从服务员手中传到厨师的手中。这样一来,客人就不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。
另外,相对于过程化的请求调用,command对象拥有更长的生命周期。对象的生命周期是跟初始请求无关的,因为这个请求已经被封装在了command对象的方法中,成为了这个对象的行为。我们可以在程序运行的任意时刻去调用这个方法,就像厨师可以在客人预定一个小时之后才帮他炒菜,相当于程序在一个小时之后才开始执行command对象的方法。除此之外,命令模式还支持撤销、排队等操作。
实践命令模式
假设正在开放一个餐厅订餐系统的界面,该界面有非常多个 Button 按钮,且这些按钮都用权限来控制。因为权限控制比较复杂,所以决定让一个程序员专门负责绘制这些按钮,而另外一个程序员则负责编写点击按钮后的具体行为,且这些行为都将被封装在对象里。
对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新订单列表,也可能用来增加订单,他只知道点击这个按钮会发生某些事情。那么当完成这个按钮的绘制之后,应该如何给它绑定 onclick 事件呢?或许你很快就给出解决方案了。
<body>
<button id="button1">点击按钮 1</button>
<button id="button2">点击按钮 2</button>
<button id="button3">点击按钮 3</button>
</body>
<script>
const button1 = document.getElementById( 'button1' ),
const button2 = document.getElementById( 'button2' ),
const button3 = document.getElementById( 'button3' );
</script>
const bindClick = (button,func) =>{
button.onclick = func;
};
const OrderList = {
refresh(){
console.log( '刷新订单列表' );
},
};
const Order = {
add(){
console.log( '增加订单' );
},
del(){
console.log( '删除订单' );
}
};
bindClick( button1, OrderList.refresh);
bindClick( button2, Order.add);
bindClick( button3, Order.del);
上面的代码的确可以解决上述的需求,但是发送请求者bindClick( button1, Order.refresh)和请求接收者Order耦合在一起了,只要修改了Order对象的方法名,发送请求者也得跟着修改。导致开发点击按钮后的具体行为的程序员一旦改动代码,那个绘制按钮的程序员也得跟着做对应的修改。
回顾一下上文介绍的命令模式的用途,我们可以使用命令模式来实现上述需求,就可以消除请求发送者和请求接收者之间的耦合关系。
构建命令对象
如何构建命令对象command是实现命令模式的关键。设计模式的主题总是把不变的事物和变化的事物分离开来,命令模式也不例外。所以在上述需求中,点击按钮后,会固定执行命令对象command的一个方法execute,这是不变的事物,而变化的事物是execute方法中执行接收者receiver中某个方法。
const command = (receiver) =>{
return {
execute(){
//执行接收者receiver中某个方法
}
}
}
接下来实现一个创建添加订单命令的函数。
const OrderCommand = (receiver) =>{
return {
execute: function () {
receiver.add();
}
}
};
其中OrderCommand是一个创建命令的函数,接受一个命令请求接收者Order对象,在Order对象集合一系列的订单行为方法。
创建一个添加订单命令对象。
const orderCommand = OrderCommand(Order);
这样orderCommand命令对象就被创建出来,它的生命周期跟发起请求者和接收请求者无关,可以再程序中各处调用。即使发起请求者和接收请求者销毁了,orderCommand命令对象还存在。
安装命令对象
命令构建完成,得把命令对象安装到按钮上,点击按钮才会执行命令。实现一个安装命令的方法setCommand。
const setCommand = (button, command) =>{
button.onclick = () =>{
command.execute();
}
};
其中参数button是按钮的DOM元素,参数command是一个命令对象,button的点击事件中执行命令对象command中的execute方法。
接下来给按钮2安装上一个执行添加订单的命令对象。
setCommand(button2, orderCommand);
安装命令对象完成后,点击按钮2就会执行Order对象中的添加订单的方法add,假如点击按钮不执行添加订单,而是要执行删除订单,只要修改OrderCommand创建命令对象的函数即可。
const OrderCommand = (receiver) =>{
return {
execute: function () {
receiver.del();
}
}
};
而不要去同时修改发送请求者和接收请求者的代码,这就达到消除发送请求者和接收请求者之间的耦合关系。