JavaScript设计模式之命令模式

定义

命令模式是最简单和优雅的设计模式之一,命令模式中的“命令”指的是执行某些特定操做的指令。命令模式最经常使用的场景是:有时候须要向某些对象发送请求,可是不知道请求的接收者是谁,也不知道被请求的操做是什么。这时候就能够经过命令模式使得请求发送者和请求接收者可以消除彼此之间的耦合关系。javascript

一个例子--实现菜单

假设咱们在实现一个菜单的功能,菜单上有不少按钮,点击不一样的按钮将会执行不一样的操做。由于程序复杂,因此将按钮的绘制和点击按钮具体的行为分配给不一样的人员编写。对于绘制按钮的程序员来讲,他彻底不知道某个按钮未来要作什么,他知道点击每一个按钮会作一些操做。分析应用场景以后,咱们发现这个需求很适合用命令模式来设计。理由以下:点击按钮以后,必须向某些负责具体行为的对象发送请求,这些对象就是行为的接收者。可是目前不知道接收者是什么对象,也不知道接收者具体作什么操做,借助命令模式,就能够解耦按钮和行为对象之间的关系。下面看具体的代码:html

<body>
  <div class="menu-list">
    <button id="btn1">按钮1</button>
    <button id="btn2">按钮2</button>
    <button id="btn3">按钮3</button>
  </div>
<body>

<script> const btn1 = document.getElementById('btn1') const btn2 = document.getElementById('btn2') const btn3 = document.getElementById('btn3') </script>
复制代码

定义setCommand函数:java

function setCommand (btn, command) {
  btn.onclick = function () {
    command.excute()
  }
}
复制代码

定义菜单的行为,先实现刷新菜单界面、增长子菜单和删除子菜单的功能:程序员

const menuBar = {
  refresh () {
    console.log('刷新菜单')
  }
}

const subMenu = {
  add () {
    console.log('增长子菜单')
  },
  del () {
    console.log('删除子菜单')
  }
}
复制代码

封装命令类:设计模式

function RefreshMenuBarCommand (receiver) {
  this.receiver = receiver
}

RefreshMenuBarCommand.prototype.excute = function () {
  this.receiver.refresh()
}

function AddSubMenuCommand (receiver) {
  this.receiver = receiver
}

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

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

DelSubMenuCommand.prototype.excute = function () {
  this.receiver.del()
}
复制代码

最后把命令接收者传入到command对象中,并把command对象安装到button上:闭包

const refreshMenuBarCommand = new RefreshMenuBarCommand(menuBar)
const addSubMenuCommand = new AddSubMenuCommand(subMenu)
const delSubMenuCommand = new DelSubMenuCommand(subMenu)

setCommand(btn1, refreshMenuBarCommand)
setCommand(btn2, addSubMenuCommand)
setCommand(btn3, delSubMenuCommand)
复制代码

这样就实现了一个简单的命令模式,从中能够看出命令模式是如何将请求的发送者和接收者解耦的。dom

JavaScript中的命令模式

在JavaScript中,函数做为一等对象,是能够做为参数四处传递的。命令模式中的运算块不必定要封装在command.excute方法中,也能够封装在普通的函数中。若是咱们须要请求的”接收者”,也能够经过闭包来实现。下面经过JavaScript直接实现命令模式:函数

const setCommand = function (btn, command) {
  btn.onclick = function () {
    command.excute()
  }
}

const menuBar = {
  refresh () {
    console.log('刷新菜单')
  }
}

const RefreshMenuBarCommand = function (receiver) {
  return {
    excute: function () {
      receiver.refresh()
    }
  }
}

const refreshMenuBarCommand = new RefreshMenuBarCommand(menuBar)
setCommand(btn1, refreshMenuBarCommand)
复制代码

撤销命令

命令模式的另外一个做用就是能够很方便地给命令对象增长撤销操做,就像在美团上下单以后也能够取消订单。下面经过一个例子来实现撤销功能,实现一个动画,这个动画是让在页面上的小球能够移动到水平方向的某个位置。页面有一个输入框和按钮,输入框中能够输入小球移动到的水平位置,点击按钮小球将开始移动到指定的坐标,使用命令模式实现代码以下:动画

<body>
  <div id="ball"></div>
  输入小球移动后的位置:<input id="pos"/>
  <button id="moveBtn">开始移动</button>
</body>

<script> const ball = document.getElementById('ball') const pos = document.getElementById('pos') const moveBtn = document.getElementById('moveBtn') const MoveCommand = function (receiver, pos) { this.receiver = receiver this.pos = pos } MoveCommand.prototype.excute = function () { this.receiver.start('left', this.pos, 1000, 'strongEaseOut') } let moveCommand moveBtn.onclick = function () { const animate = new Animate(ball) moveCommand = new MoveCommand(animate, pos.value) moveCommand.excute() } </script>
复制代码

增长取消按钮:ui

<body>
  <div id="ball"></div>
  输入小球移动后的位置:<input id="pos"/>
  <button id="moveBtn">开始移动</button>
  <button id="cancelBtn">取消</button>
</body>
复制代码

增长撤销功能,通常是给命令对象增长一个undo的方法:

const ball = document.getElementById('ball')
  const pos = document.getElementById('pos')
  const moveBtn = document.getElementById('moveBtn')
  const cancelBtn = document.getElementById('cancelBtn')

  const MoveCommand = function (receiver, pos) {
    this.receiver = receiver
    this.pos = pos
    this.oldPos = null
  }

  MoveCommand.prototype.excute = function () {
    this.receiver.start('left', this.pos, 1000, 'strongEaseOut')
    // 记录小球的位置
    this.oldPos = this.receiver.dom.getBoundingClientReact()[this.receiver.propertyName]
  }

  MoveCommand.prototype.undo = function () {
    this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut')
  }

  let moveCommand

  moveBtn.onclick = function () {
    const animate = new Animate(ball)
    moveCommand = new MoveCommand(animate, pos.value)
    moveCommand.excute()
  }

  cancelBtn.onclick = function () {
    moveCommand.undo()
  }
复制代码

这样就完成了撤销的功能,若是使用普通的方法来实现,可能要每次记录小球的运动轨迹,才能让它回到以前的位置。而命令模式中小球的原始位置已经在小球移动以前做为command对象的属性存起来了,因此只须要编写undo方法,在这个方法中让小球回到记录的位置就能够了。

撤销和重作

上面咱们实现了小球回到上一个位置的撤销功能,有时候咱们要实现屡次撤销,好比下棋游戏中,咱们可能须要悔棋5步,这时候须要使用一个历史列表来记录以前下棋的命令,而后倒序循环来每一次执行这些命令的undo操做,直到回到咱们须要的那个状态。

可是在一些场景下,没法顺利地使用undo操做让对象回到上一个状态。例如在Canvas画图中,画布上有一些点,咱们在这些点之间画了不少线,若是这是用命令模式来实现的,就很难实现撤销操做,由于在Canvas中,擦除一条线相对不容易实现。
这时候最好的办法就是擦除整个画布,而后把以前的命令所有从新执行一遍,咱们只须要实现一个历史列表记录以前的命令。对于处理不可逆的命令,这种方式是最好的。

命令队列

在现实生活中,咱们出去吃饭,点菜下单后,若是订单数量过多,餐厅的厨师人手不够,就须要对订单进行排队处理。这时候命令模式把请求封装成命令对象的优势再次体现了出来,对象的生命周期几乎是永久的,除非咱们主动回收它。换句话来讲,命令对象的生命周期跟初始请求发生的时间无关,咱们能够在任什么时候刻执行command对象的excute方法。

拿前面的动画例子来讲,咱们能够把div的运动过程封装成命令对象,再把他们压入一个队列,当动画执行完,当command对象的职责完成后,而后主动通知队列,此时从队列中取出下一个命令对象,而后执行它。这样若是用户重复点击执行按钮,那么不会出现上一个动画还没执行完,下一个动画已经开始的问题,用户能够完整看到每个动画的执行过程。

宏命令

宏命令是一组命令的集合,经过执行宏命令,能够一次执行一组命令。若是在你家里有一个万能遥控器,天天回家的时候,只要按一个按钮,就能够帮咱们打开电脑,打开电视,打开空调。下面实现这个宏命令:

const openComputerCommand = {
  excute () {
    console.log('打开电脑')
  }
}

const openTVCommand = {
  excute () {
    console.log('打开电视')
  }
}

const openAirCommand = {
  excute () {
    console.log('打开空调')
  }
}

const MacroCommand = function {
  return {
    commandList: [],
    add (command) {
      this.commandList.push(command)
    },
    excute () {
      for (let i = 0, len = this.commandList.length; i < len; i++) {
        const command = this.commandList[i]
        command.excute()
      }
    }
  }
}
复制代码

咱们也能够为宏命令添加undo操做,跟excute方法相似,调用宏命令的undo方法就是把命令列表里的每一个命令对象都执行对应的undo方法。

总结

跟传统的面向对象的方式实现命令模式不一样的是,在JavaScript中,能够用高阶函数和闭包来实现命令模式,这种方式更加简单。

相关文章
相关标签/搜索