[JS设计模式]命令模式

391 阅读4分钟

前言

今天来讲命令模式,命令模式让一个动作从发起人和执行人之间过程的耦合度减低。合理地使用命令模式可以大大提高我们代码的可维护性。

设计思路

一个命令从端到端之间消费,一定会存在很多复杂的逻辑。命令模式就是复杂在这个过程中解耦的。举个例子,页面上有一个按纽,现在希望点击这个按钮之后,页面可以展开菜单。代码大致如下:

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:falsetrigger(){
    this.show = !this.show;
  }
};
var newMenu = {
  el:el,
  show:truetrigger(){
    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设计模式与开发实践》—— 曾探