JavaScript设计模式之命令模式

1,300 阅读6分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

概念

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

命令模式的例子

菜单程序

背景:假如我们正在编写一个用户界面程序,该用户界面至少有数十个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 = 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语言中是一种隐形的模式。

感谢阅读 🙏