JavaScript设计模式之命令模式

1,684 阅读5分钟

本文由我们团队肖建朋学习总结

定义

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

在面向对象语言中,命令模式通常包括4个角色:

  • Command:定义命令的统一接口

  • ConcreteCommand:Command接口的实现者,用来执行具体的命令,某些情况下可以直接用来充当Receiver。

  • Receiver:命令的实际执行者

  • Invoker:命令的请求者,是命令模式中最重要的角色。这个角色用来对各个命令进行控制。

我们知道javascript是一种将函数作为一等对象的语言,那么我们该如何用函数来实现命令模式呢?我们先来看一个小例子。

一个小例子

假如有这样一个场景:有个页面,页面中有一个按钮,当点击按钮时,执行一些操作。

第一步,我们先创建一个html文件,body中包含一个按钮

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <button id="button1">点击按钮1</button>
</body>
</html>

第二步,我们需要得到这个按钮,在script中获取button1,还需要定义setCommand函数,setCommand 函数负责往按钮上面安装命令。

<script>
  var button1 = document.getElementById('button1')
  var setCommand = function(button, conmmand) {
      button.onclick = function() {
        conmmand.execute()
      }
    }
</script>

第三步,定义了一个MenuBar的对象,并且给了它一个方法属性refresh;还定义了一个RefreshMenuBarCommand,接收对象并返回了执行接受对象上refresh方法的结果。

var MenuBar = {
  refresh: function(){
    console.log('刷新了界面')
  }
}
var RefreshMenuBarCommand = function(receiver) {
  return {
      execute: function() {
          receiver.refresh()
      }
  }
}

第四步,把命令接收者传入到command 对象中,并且把command 对象安装到button 上面

var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
setCommand(button1, refreshMenuBarCommand)

在浏览器中打开html文件,点击按钮,在console中会输出刷新了界面.

通过这个例子我们可以看到如何把请求发送者和请求接收者解耦开的。在JavaScript中命令模式其实是回调(callback)函数的一个面向对象的替代品。作为函数为一等对象的语言,命令模式已经融入到了JavaScript语言之中。运算块不一定要封装方法中,也可以封装在普通函数中。command.execute函数作为一等对象,本身就可以被四处传递。即使我们依然需要请求“接收者”,那也未必使用面向对象的方式,闭包可以完成同样的功能。

撤销命令

现实生活中的很多命令模式例子不仅是能够发出命令,而且需要能够撤销命令,例如我们在电商网管购物,下单后发现商品选错了,我们可以取消订单。

撤销 字面意思就是让撤回之前的动作,让对象回到动作执行之前的状态。

我们现在实现一个计算功能,每点击增加按钮,数字都会加10,点击撤销按钮则返回最近一次点击按钮之前的数字。那么我们定义参数lastNumber记录增加的数字。下面请看代码:

<!DOCTYPE html>
<html>

<head>
</head>
<body>
  <input type="text" value="0" id="number"> </input>
  <button type="submit" id="add" >增加100以内的随机数</button>
  <button type="submit" id="revoke">返回上一步</button>
  <script type="text/javascript">
    var addBtn = document.getElementById('add')
    var revokeBtn = document.getElementById('revoke')
    var numberText = document.getElementById('number')
    var lastNumber = 0

    var setCommand = function(button, conmmand) {
      button.onclick = function() {
        conmmand.execute()
      }
    }
    var MenuBar = {
      add: function(){
        lastNumber = numberText.value
        numberText.value = parseInt(numberText.value) + parseInt(Math.random()*100)
        console.log(numberText.value)
      },
      revoke: function() {
        numberText.value = lastNumber
        console.log(numberText.value)
      }
    }
    var AddCommand = function(receiver) {
      return {
        execute: function() {
          receiver.add()
        }
      }
    }
    var RevokeCommand = function(receiver) {
      return {
        execute: function() {
          receiver.revoke()
        }
      }
    }
    var addCommand = AddCommand(MenuBar)
    setCommand(addBtn, addCommand)
    var revokeCommand = RevokeCommand(MenuBar)
    setCommand(revokeBtn, revokeCommand)

  </script>
</body>

</html>

这里我们实现了撤回一步操作。那么如果我们需要连续向前撤回或者撤回到某一步的状态是该怎么办呢?

此时我们就要用一个list来记录每一步操作前的数字。然后根据要撤回的步数,找到对应的历史数字。如果要实现连续点击一次向前撤回一步直到初始数字,那么我们可以结合前面学到的迭代器模式来实现。

其他几种命令

  • 命令对列

    在现实的订餐 场景中,当订单数量过多时,厨师人手不够同时处理这么多订单,那么则可以让这么订单进行排队处理,只有第一个订单处理完成后再开始第二个订单。

    把请求封装成命令对象的优点在这里就提现出来了,对象的生命周期是永久的,除非我们主动回收它,也就是说命令对象的生命周期给初始请求的时间无关,command的 excute 方法可以在程序的任何时刻执行即使点击按钮的请求早已发生,但我们的命令对象仍然是有生命的。

  • 宏命令
    宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。宏命令首先需要把子命令添加到宏命令对象,当调用宏命令的execute时,就迭代这一组子命令,依次执行他们的execute方法。

总结

由于JavaScript语言的特性,命令模式和其它语言有许多不同,JavaScript可以使用高阶函数很方便的实现命令模式。命令模式在JavaScript中是一种隐形模式。

优点

  1. 降低对象之间的耦合度。

  2. 新的命令可以很容易地加入到系统中。

  3. 可以比较容易地设计一个组合命令。

  4. 调用同一方法实现不同的功能

缺点

使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。