前言
今天来讲命令模式,命令模式让一个动作从发起人和执行人之间过程的耦合度减低。合理地使用命令模式可以大大提高我们代码的可维护性。
设计思路
一个命令从端到端之间消费,一定会存在很多复杂的逻辑。命令模式就是复杂在这个过程中解耦的。举个例子,页面上有一个按纽,现在希望点击这个按钮之后,页面可以展开菜单。代码大致如下:
var btn = document.getElementById('btn');
var menu = {
el:el,
show:false
};
// 触发按钮,可以理解为发起一个命令。
btn.addEventListener('click',()=>{
// menu如果想要接收这个命令,就得在这里加入代码。
menu.show = true;
})
假如现在随着项目开发需要在点击按钮之后,让另一个菜单折叠起来。我们分析这个需求,无非就是在按钮的点击命令发起后,让第二个菜单也接收到这个命令而已嘛。可以更新下列代码:
var btn = document.getElementById('btn');
var menu = {
el:el,
show:false
};
var newMenu = {
el:el,
show:true
};
btn.addEventListener('click',()=>{
menu.show = true;
// 加入让newMenu接收这个命令
newMenu.show = false;
})
不难发现,随着需要接受这个命令的对象增加,这里的代码耦合度会变得越来越高。想要解耦,我们先来把这个过程思考一下,在这个过程中究竟发生了什么?实际上在这个过程中,一共出现了3个对象:1. 命令发起者。2. 命令本身。 3. 命令的接收者。 图例大致如下:
根据命令模式的规范,我们可以整理下列代码:
var btn = document.getElementById('btn');
var menu = {
el:el,
show:false
};
var newMenu = {
el:el,
show:true
};
var setCommand = function(btn,command){
btn.addEventListener('click',()=>{
// 执行命令对象的execute方法,不管命令具体的内容是什么
command.execute();
})
};
class MenuCommand{
constructor(receiver){
// 命令对象接受一个命令的接受者
this.receiver = receiver;
}
execute(){
// 定义execute方法
this.receiver.show = !this.receiver.show;
}
}
// 示例化命令实例
const menuCommand = new MenuCommand(menu);
const newMenuCommand = new MenuCommand(newMenu);
// 设置命令
setCommand(btn,menuCommand);
setCommand(btn,newMenuCommand);
根据上述代码,就彻底把命令者,命令,接收者三者区分开了。这就是命令模式的思路架构。
JavaScript中的命令模式
上方的示例代码实际上是严格仿照了面向对象的实现的,如定义命令对象的execute方法等。与策略模式相同,JavaScript有实现命令模式的便捷方式。
var btn = document.getElementById('btn');
var menu = {
el:el,
show:false,
trigger(){
this.show = !this.show;
}
};
var newMenu = {
el:el,
show:true,
trigger(){
this.show = !this.show;
}
};
var setCommand = function(btn,func){
// 直接传入一个回调
btn.addEventListener('click',func)
};
// 设置命令
setCommand(btn,menu.trigger);
setCommand(btn,menu.trigger);
从上面代码可以看到js可以把命令模式的写法简化,主要原因是因为js中的函数的“一等公民”特性。利用函数作为入参回调实现了一套简化的写法,同时又保持着命令模式的优势。
命令模式的延申
看到这里相信很多人会想到,这不是跟发布订阅也能做的事吗?当然命令模式能做的不仅如此,与别的设计模式相比命令模式侧重的是“命令”本身。围绕命令对象,其实我们还能设计很多功能。
撤销命令
既然有命令,当然也是可以撤回的。因此我们可以给命令对象加上撤回的功能。
var btn = document.getElementById('btn');
var light = {
color: '#999999',
// 颜色可以被随机刷新。
refreshColor(){
this.color = randoColor();
}
};
var setCommand = function(btn,command){
btn.addEventListener('click',()=>{
// 执行命令对象的execute方法,不管命令具体的内容是什么
command.execute();
})
btn.addEventListener('keyup',()=>{
// 假设监听键盘事件触发撤回
command.undo();
})
};
class LightCommand{
constructor(receiver){
this.receiver = receiver;
this.lastColor = this.receiver.color;
}
execute(){
// 定义execute方法
this.receiver.refreshColor();
this.lastColor = this.receiver.color;
}
// 定义撤回操作
undo(){
this.receiver.color = this.lastColor;
}
}
// 示例化命令实例
const lightCommand = new LightCommand(light);
// 设置命令
setCommand(btn,lightCommand);
重做命令
既然能撤回,自然也能重做。原理也很简单,只要在命令上加上redo就行了。
class LightCommand{
constructor(receiver){
// ...
}
execute(){
// ...
}
undo(){
// ...
}
// 定义重做操作
redo(){
// ...
}
}
命令队列
当开始管理“命令”之后,我们会发现更多时候命令是一连串的,就是说他会想是一个队列有序地执行。命令队列有2个好处,一是可以让每个命令有序执行,确保前面的命令执行完之后再执行下一个。二是可以让命令变得可追踪。在实现上无非就是加一个数组管理命令。
命令宏
当习惯把“命令”作为“对象”看待之后,我们甚至可以将几个命令组合实现“宏”的操作。
const commandA = new CommandA(a);
const commandB = new CommandB(b);
const commandC = new CommandC(c);
class MacroCommand{
constructor(){
this.commandList = [];
}
add(command){
this.commandList.push(command);
}
execute(){
for(const item of this.commandList){
item.execute();
}
}
}
const macroCommand = new MacroCommand();
macroCommand.add(commandA);
macroCommand.add(commandB);
macroCommand.add(commandC);
总结
命令模式是一种非常实用的设计模式,加上JavaScript的天然优势,前端开发者可以很容易地在业务中使用这种思想,从而提高代码的可读性和可维护性。
参考
《JavaScript设计模式与开发实践》—— 曾探