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中,能夠用高階函數和閉包來實現命令模式,這種方式更加簡單。

相關文章
相關標籤/搜索