> 文章列表 > 设计模式之命令模式

设计模式之命令模式

设计模式之命令模式

参考资料

  • 曾探《JavaScript设计模式与开发实践》;
  • JavaScript设计模式之命令模式
  • JavaScript 设计模式(十八):命令模式

定义

命令模式中的命令指的是一个执行某些特定事情的指令
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时需要一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

命令模式由三种角色构成:

  1. 发布者 invoker(发出命令,调用命令对象,不知道如何执行与谁执行);

  2. 接收者 receiver (提供对应接口处理请求,不知道谁发起请求);

  3. 命令对象 command(接收命令,调用接收者对应接口处理发布者的请求)。

在这里插入图片描述

发布者 invoker 和接收者 receiver 各自独立,将请求封装成命令对象 command ,请求的具体执行由命令对象 command 调用接收者 receiver 对应接口执行。

命令对象 command 充当发布者 invoker 与接收者 receiver 之间的连接桥梁(中间对象介入)。实现发布者与接收之间的解耦,对比过程化请求调用,命令对象 command 拥有更长的生命周期,接收者 receiver 属性方法被封装在命令对象 command 属性中,使得程序执行时可任意时刻调用接收者对象 receiver 。因此 command 可对请求进行进一步管控处理,如实现延时、预定、排队、撤销等功能。

使用场景:

  • 餐馆点菜;
  • 客户下单,订单记录了客户购买的产品,仓库根据订单给客户备货;
  • 不关注执行者,不关注执行过程,只要结果,支持撤销请求、延后处理、日志记录等。

命令模式的例子-菜单程序

背景:假如我们正在编写一个用户界面程序,该用户界面至少有数十个Button按钮。因为项目比较复杂,我们决定让某个程序员负责绘制这些按钮,而另一些程序员负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。
使用命令模式理由:点击按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但目前并不知道接收者是什么对象,也不知道接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。

那么,我们如何使用命令模式去实现这样的功能呢?

  • 首先,先定义一个setCommand函数,执行命令的动作被约定为调用command对象的execute()方法,代码如下:

    var setCommand = function(button, command) {button.onclick = function() {command.execute();}
    }
    
  • 然后,完成了几个菜单界面的具体功能,这些功能分布在MenuBarSubMenu这两个对象中,这两个对象也被称为命令接收者,代码如下:

    var MeunBar = {refresh: function() {console.log('刷新菜单目录')}
    }var SubMenu = {add: function() {console.log('新增子菜单')},del: function() {console.log('删除子菜单')}
    }
    
  • 将这些行为封装成命令类,通过命令接受者执行对应的命令,代码如下:

    var RefreshMenuBarCommand = function(receiver) {this.receiver = receiver
    }RefreshMenuBarCommand.prototype.execute = function() {this.receiver.refresh();
    }
    

    之后的AddSubMenuCommandDelSubMenuCommand的封装类似,添加一个execute函数,通过命令接受者执行对应的函数adddel即可

  • 最后,把命令接受者传入到命令类中,并将这些命令对象绑定到button按钮上,代码如下:

    var refreshMenuBarCommand = new RefreshMenuBarCommand(MeunBar)
    var addSubMenuCommand = new AddSubMenuCommand(SubMenu)
    var delSubMenuCommand = new DelSubMenuCommand(SubMenu)setCommand(button1, refreshMenuBarCommand)
    setCommand(button2, addSubMenuCommand)
    setCommand(button3, delSubMenuCommand)
    

    至此,我们就使用命令模式请求发送者请求接收者解耦开。

    根据上面的例子,我们可总结出:命令模式的特点是要有发布者接收者命令对象,其中,refreshMenuBarCommand 为命令对象,用来接收命令,并调用接收者执行命令;MeunBar接收者,提供对应的命令处理函数;button1点击事件发布者,当按钮点击时:由发布者发出命令给命令对象,再由命令对象调用接收者对应的函数执行。 而在这个过程中,发布者接收者各自独立,这也就很好的将两部分逻辑解耦,各自维护即可,这也是命令模式的一大优势。

JavaScript中的命令模式

上面的代码看起来实现非常的繁琐,即使我们不使用设计模式,也可以很轻松的实现相同的功能,如:

var bindClick = function(button, func) {button.onclick = func
}bindClick(button1, MeunBar.refresh)

在JavaScript中,运算块并不一定需要封装在command.execute中,也可以封装在普通函数中。即使我们需要请求接收者,也可以通过闭包的方式实现同样的功能。代码如下:

var setCommand = function(button, func) {button.onclick = function() {func();}
}var MeunBar = {refresh: function() {console.log('刷新菜单目录')}
}var RefreshMenuBarCommand = function(receiver) {return function() {receiver.refresh();}
}var refreshMenuBarCommand = RefreshMenuBarCommand(MeunBar)setCommand(button1, refreshMenuBarCommand)

撤销命令

命令模式的作用不仅仅是封装运算模块,而且可以很方便地给命令对象增加撤销操作。下面来看撤销命令的例子:我们实现一个可以让小球水平移动到某个位置,当通过输入框输入一个数字,并点击按钮后,小球便水平移动相应的距离。我们先使用命令模式实现一下功能,代码如下:

var MoveCommand = function(receiver, pos) {this.receiver = receiverthis.pos = pos
}MoveCommand.prototype.execute = function() {this.receiver.start('left', this.pos)
}// 这里简化了animate的实现,实际为一个包含可以将小球移动到指定位置的start函数
var moveCommand = new MoveCommand(animate, 100)
// 小球移动
moveCommand.execute();

接下来我们新增一个撤销操作的按钮,在运动之前,先记录下小球当前的位置,当执行撤销操作后,再使小球回到之前的位置,代码如下:

var MoveCommand = function(receiver, pos) {this.receiver = receiverthis.pos = posthis.oldPos = null
}MoveCommand.prototype.execute = function() {// 记录小球移动前的位置this.oldPos = this.receiver.dom.getBoundingClientRect()['left']this.receiver.start('left', this.pos)
}// 撤销命令,回到运动前的位置
MoveCommand.prototype.undo = function() {this.receiver.start('left', this.oldPos)
}var moveCommand = new MoveCommand(animate, 100)
// 小球移动
moveCommand.execute();
// 撤销移动
moveCommand.undo();

宏命令

宏命令:一组命令集合(命令模式与组合模式的产物)

发布者发布一个请求,命令对象会遍历命令集合下的一系列子命令并执行,完成多任务。

// 宏命令对象
class MacroCommand {constructor() {this.commandList = [];  // 缓存子命令对象}add(command) {            // 向缓存中添加子命令this.commandList.push(command);}exceute() {               // 对外命令执行接口// 遍历自命令对象并执行其 execute 方法for (const command of this.commandList) {command.execute();}}
}const openWechat = {  // 命令对象execute: () => {console.log('打开微信');}
};const openChrome = {  // 命令对象execute: () => {console.log('打开Chrome');}
};const openEmail = {   // 命令对象execute: () => {console.log('打开Email');}
}const macroCommand = new MacroCommand();macroCommand.add(openWechat); // 宏命令中添加子命令
macroCommand.add(openChrome); // 宏命令中添加子命令
macroCommand.add(openEmail);  // 宏命令中添加子命令macroCommand.execute();       // 执行宏命令
/* 输出:
打开微信
打开Chrome
打开Email
*/

傻瓜命令与智能命令

傻瓜命令:命令对象需要接收者来执行客户的请求。智能命令:命令对象直接实现请求,不需要接收者,“聪明”的命令对象。

“傻瓜命令” 与 “智能命令” 的区别在于是否有 “接收者” 对象。

// openWechat 是智能命令对象,并没有传入 receiver 接收对象
const openWechat = {execute: () => {  // 命令对象直接处理请求console.log('打开微信');}
};

没有 “接收者” 的智能命令与策略模式很类似。代码实现类似,区别在于实现目标不同。

  1. 策略模式中实现的目标是一致的,只是实现算法不同(如目标:根据KPI计算奖金);
  2. 智能命令的解决问题更广,目标更具散发性。(如目标:计算奖金/计算出勤率等)。

优缺点

优点:

  • 降低对象之间的耦合度,发布者与接收者实现解耦。
  • 新的命令可以很容易地加入到系统中。
  • 可以比较容易地设计一个组合命令。
  • 调用同一方法实现不同的功能。

缺点:

  • 额外增加命令对象,非直接调用,存在一定开销。
  • 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。

总结

跟其他语言不同,JavaScript可以用高阶函数非常方便地实现命令模式命令模式JavaScript语言中是一种隐形的模式。