JavaScript设计模式之命令模式
概念
在《JavaScript设计模式与开发实践》中其实并没有对命令模式的定义,不过有这么一句话:命令模式中的命令指的是一个执行某些特定事情的指令。 命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时需要一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
命令模式的例子
菜单程序
背景:假如我们正在编写一个用户界面程序,该用户界面至少有数十个Button按钮。因为项目比较复杂,我们决定让某个程序员负责绘制这些按钮,而另一些程序员负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。 使用命令模式理由:点击按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但目前并不知道接收者是什么对象,也不知道接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。
那么,我们如何使用命令模式去实现这样的功能呢?
- 首先,先定义一个
setCommand
函数,执行命令的动作被约定为调用command
对象的execute()
方法,代码如下:
var setCommand = function(button, command) {
button.onclick = function() {
command.execute();
}
}
- 然后,完成了几个菜单界面的具体功能,这些功能分布在
MenuBar
和SubMenu
这两个对象中,这两个对象也被称为命令接收者,代码如下:
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();
}
之后的AddSubMenuCommand和DelSubMenuCommand的封装类似,添加一个
execute
函数,通过命令接受者执行对应的函数add
和del
即可
- 最后,把命令接受者传入到命令类中,并将这些命令对象绑定到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 = receiver
this.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 = receiver
this.pos = pos
this.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();
现在,通过命令模式轻松地实现了撤销功能。撤销是命令模式里非常有用的功能。
总结
由于命令模式在JavaScript中使用较少,并没有在一些源码中找到相应的例子,所以没有介绍一些优秀的真实案例,通过前面的介绍,命令模式的一个使用场景就是撤销重做功能,命令模式可以通过指定方法execute
来执行,而并不需要关注是谁来执行,所以这样就可以将每一次执行数据很方便的记录下来。
通过前面的例子,我们能总结出命令模式的两个适用场景:封装运算块和撤销重做功能。对于运算模块的封装,我们不仅通过面向对象的方式实现,也利用
JavaScript
的闭包特性将之前的代码进行优化,使用更简洁的代码实现了相同的功能。 同时,我们也总结出实现命令模式的关键要素:发布者、接收者、命令对象。而发布者和接收者的解耦也是命令模式的优势,当我们想要使用命令模式实现一个需求时,也要首先分清谁是发布者谁是接收者,每个功能分离开独立开发各自维护,也可增加代码的可扩展性。最后,本章我们主要介绍了命令模式的概念和使用方式。跟其他语言不同,
JavaScript
可以用高阶函数非常方便地实现命令模式。命令模式在JavaScript语言中是一种隐形的模式。感谢阅读 🙏
转载自:https://juejin.cn/post/7171804379471675422