《js 设计模式与开发实践》读书笔记 9


  命令模式。假设有一个快餐店,而我是该餐厅的点餐服务员,那么我一天的工作应该是这样的:当某位客人点餐或者打来订餐电话后,我会把他的需求都写在清单上,然后交给厨房,客人不用关心是哪些厨师帮他炒菜。我们餐厅还可以满足客人需要的定时服务,比如客人可能当前在回家的路上,要求一个小时后才开始炒他的菜,只要订单还在,厨师就不会忘记。客人也可以很方便地打电话来撤销订单。另外如果有太多的客人点餐,厨房可以按照订单的顺序排队炒菜。这些记录着订单信息的清单,便是命令模式中的命令对象。

  命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。拿订单来说,客人需要向厨师发送请求,但是完全不知道这些厨师的名字和联系方式,也不知道厨师炒菜的方式和步骤。命令模式把客人订餐的请求封装成 command 对象,也就是订餐中的订单对象。这个对象在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中。这样以来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。另外,相对于过程化的请求调用,command 对象拥有更长的生命周期。对象的生命周期跟初始请求无关的,因为这个请求已经被封装在了 command 对象的方法中,成为了这个对象的行为。我们可以在程序的任意时刻去调用这个方法,就像厨师可以在客人预定 1 个小时之后才帮他炒菜,相当于程序在 1 个小时之后才开始执行 command 对象的方法。除了这两点之外,命令模式还支持撤销,排队等操作。

<script>
  var button1 = document.getElementById('button1')
  var button2 = document.getElementById('button2')
  var button3 = document.getElementById('button3')
</script>
<li><a id="button1">Home</a></li>
<li><a id="button2">Features</a></li>
<li><a id="button3">Pricing</a></li>

  先创建好发送者,接下来定义 setCommand 函数,setCommand 函数负责往按钮上安装命令。最后,负责编写点击按钮之后的具体行为的程序员总算交上了他们的成功,他们完成了刷新菜单界面,增加子菜单和删除子菜单这几个功能。这几个功能被分布在 MenuBar 和 SubMenu 这两个对象中:

var setCommand = function (button, command) {
  button.onclick = function () {
    command.execute()
  }
}

var MenuBar = {
  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()
}

var AddSubMenuCommand = function (receiver) {
  this.receiver = receiver
}
AddSubMenuCommand.prototype.execute = function () {
  this.receiver.add()
}

var DelSubMenuCommand = function (receiver) {
  this.receiver = receiver
}

DelSubMenuCommand.prototype.execute = function () {
  this.receiver.del()
}

  最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面。这只是一个很简单的命令模式示例,但从中可以看到我们是如何把请求发送者和请求接收者解耦开的。只是这个命令模式,看起来就是给对象的某个方法取了 execute 的名字。引入了 command 对象和 receiver 这两个无中生有的角色无非就是把简单的事情复杂化了。即使不用什么模式,用下面的几行代码就可以实现相同的功能。

var button1 = document.getElementById('button1')
var button2 = document.getElementById('button2')
var button3 = document.getElementById('button3')

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

var MenuBar = {
  refresh: function () {
    console.log('刷新菜单页面')
  }
}

var SubMenu = {
  add: function () {
    console.log('增加子菜单')
  },
  del: function () {
    console.log('删除子菜单')
  }
}

bindClick(button1, MenuBar.refresh)
bindClick(button2, SubMenu.add)
bindClick(button3, SubMenu.del)

  因为最上面的示例代码是模拟传统面向对象语言的命令模式实现。命令模式将过程式的请求调用封装在 command 对象的 execute 方法里,通过封装方法调用,我们可以把运算块包装成形。command 对象可以被四处传递,所以在调用命令的时候,客户不需要关心事情是如何进行的。命令模式的由来,其实是回调函数的一个面向对象的替代品。js 作为函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了 js 语言之中。在面向对象设计中,命令模式的接收者被当成 command 对象的属性保存起来,同时约定执行命令的操作调用 command.execute 方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。

  命令模式的作用不仅是封装运算块,而且可以很方便的给命令对象增加撤销操作。就像订餐时客人可以通过电话来取消订单一样。在某些情况下无法顺利利用 undo 操作让对象回到 execute 之前的状态。比如在一个 Canvas 画图的程序中,画布上有一些点,我们在这些点之间画了 n 条曲线把这些点相互连接起来,当然这是用命令模式来实现的。但是我们却很难为这里的命令对象定一个擦除某条曲线的 undo 操作,因为在 canvas 画图中,擦除一条线相对不容易实现。最好的方法是先清除画布,然后把刚才执行过的命令全部执行一遍,这一点同样可以利用一个历史列表堆栈办到。记录命令日志,然后重复执行它们,这是逆转不可逆命令的一个好办法。

  宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电脑并登录 qq。

var closeDoorCommand = {
  execute: function () {
    console.log('关门')
  }
}

var openPcCommand = {
  execute: function () {
    console.log('开电脑')
  }
}

var openQQCommand = {
  execute: function () {
    console.log('登录qq')
  }
}

var MacroCommand = function () {
  return {
    commandsList: [],
    add: function (command) {
      this.commandsList.push(command)
    },
    execute: function () {
      for (var i = 0; i < this.commandsList.length; i++) {
        command = this.commandsList[i]
        command.execute()
      }
    }
  }
}

var macroCommand = MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)
macroCommand.execute()

  当然我们还可以为宏命令添加撤销功能,跟 macroCommand.execute 类似,当调用 macroCommand.undo 方法时,宏命令里包含的所有子命令对象要依次执行各自的 undo 操作。 一般来说,命令模式都会在command对象中保存一个接收者来负责真正执行客户的请求,宏命令里的对象是傻瓜式的,它只负责把客户的请求转交给执行者执行,这种模式的好处就是请求发起者和请求接收者之间尽可能的得到了解耦。js可以用高阶函数非常方便的实现命令模式,命令模式在js语言中时一种隐形的模式。

posted @ 2022-09-16 11:12  艾路  阅读(23)  评论(0编辑  收藏  举报