JavaScript設計模式之狀態模式

定義

容許一個對象在其內部狀態改變時來改變它的行爲,對象看起來彷佛修改了它的類。在狀態模式中,咱們把狀態封裝成獨立的類,並將請求委託給當前的狀態對象,因此當對象內部的狀態改變時,對象會有不一樣的行爲。狀態模式的關鍵就是區分對象的內部狀態。javascript

電燈程序

先實現一個不用狀態模式的電燈程序:

class Light {
  construct () {
    this.state = 'off'
    this.button = null
  }

  // 建立一個button負責控制電燈的開關
  init () {
    const button = document.createElement('button')
    this.button = document.body.appendChild(button)
    this.button.innerHTML = '開關'

    this.button.onclick = () => {
      this.buttonWasPressed()
    }
  }

  buttonWasPressed () {
    if (this.state === 'off') {
      console.log('開燈')
      this.state = 'on'
    } else if (this.state === 'on') {
      console.log('關燈')
      this.state = 'off'
    }
  }
}

const light = new Light()
light.init()
複製代碼

上面代碼實現了一個強壯的狀態機,看起來這段代碼設計得無懈可擊了,這個程序沒有任何Bug。

比較惋惜的是,世界上的電燈並不是都只有開關兩種狀態,一些酒店裏的電燈只有一個開關,可是它的表現是:第一次按下打開弱光,第二次按下打開強光,第三次纔是關閉電燈。因而,咱們須要修改前面的代碼:java

buttonWasPressed () {
    if (this.state === 'off') {
      console.log('弱光')
      this.state = 'weakLight'
    } else if (this.state === 'weakLight') {
      console.log('強光')
      this.state = 'strongLight'
    } else if (this.state === 'strongLight') {
      console.log('關燈')
      this.state = 'off'
  }
複製代碼

如今咱們來總結下上面的程序的缺點:算法

  • 首先,buttonWasPressed方法違反開放-封閉原則,每次新增或者修改light的狀態就須要修改該方法中的代碼。
  • 全部跟狀態相關的代碼都封裝在buttonWasPressed方法,致使這個方法會由於持續的加需求而膨脹到難以維護的地步。特別是在實際的開發中,每一個狀態可能要處理的邏輯比例子中的多不少。
  • 狀態切換不明顯,僅僅只是一句this.state = 'off'的賦值,這樣的代碼很容易被遺漏掉,要想了解電燈的全部狀態,咱們必須深刻到代碼內部,耐心讀完buttonWasPressed方法。
  • 狀態之間切換,是經過if-else語句來實現,增長或者修改一個狀態可能須要改變若干個操做,這將使得buttonWasPressed方法更加難以維護。

使用狀態模式來改進電燈程序

首先咱們先肯定電燈的狀態種類,而後把它們封裝成單獨的類,封裝通常是封裝對象的行爲,而不是對象的狀態。可是在狀態模式中,關鍵的就是把每種狀態封裝成單獨的類,跟狀態相關的行爲都封裝在類的內部。從以前的代碼得知,電燈有三種狀態: OffLightState、WeakLightState、StrongLightState。首先編寫狀態類:性能優化

class OffLightState {
  construct (light) {
    this.light = light
  }

  buttonWasPressed () {
    console.log('弱光')
    this.light.setState(this.light.weakLightState)
  }
}

class WeakLightState {
  construct (light) {
    this.light = light
  }

  buttonWasPressed () {
    console.log('強光')
    this.light.setState(this.light.strongLightState)
  }
}

class StrongLightState {
  construct (light) {
    this.light = light
  }

  buttonWasPressed () {
    console.log('關燈')
    this.light.setState(this.light.offLightState)
  }
}
複製代碼

接下來編寫Light類,咱們再也不須要一個字符串來記錄當前的狀態,而是使用更加立體化的狀態對象,在初始化Light類的時候就爲每個state類建立一個狀態對象:閉包

class Light {
  construct () {
    this.offLightState = new OffLightState(this)
    this.weakLightState = new WeakLightState(this)
    this.strongLightState = new StrongLightState(this)

    this.currentState = this.offLightState // 初始化電燈狀態
    this.button = null
  }

  init () {
    const button = document.createElement('button')
    this.button = document.body.appendChild(button)
    this.button.innerHTML = '開關'

    this.button.onclick = () => {
      this.currentState.buttonWasPressed()
    }
  }

  setState (newState) {
    this.currentState = newState
  }
}

const light = new Light()
light.init()
複製代碼

經過使用狀態模式重構以後,咱們看到程序有不少優勢:app

  • 每種狀態和它對應的行爲之間的關係局部化,這些行爲被分散在各個對象的狀態類之中,便於閱讀和管理。
  • 狀態之間的切換邏輯分佈在狀態類內部,這使得咱們無需編寫if-else語句來控制狀態直接的切換。
  • 當咱們須要爲Light類增長一種新的狀態時,只須要增長一個新的狀態類,再稍微改變一下現有的代碼。

缺乏抽象類的變通方式

在狀態模式中,Light類被稱爲上下文(Context)。Context持有全部狀態對象的引用 ,以便把請求委託給狀態對象。在上面的例子中,請求最後委託到的是狀態類的buttonWasPressed方法,因此全部的狀態類都必須實現buttonWasPressed方法。

在Java中,全部的狀態類必須繼承自一個State抽象類,從而保證全部的狀態子類都實現buttonWasPressed方法。遺憾的是,在JavaScript中沒有抽象類,也沒有接口的概念。咱們能夠編寫一個狀態類,而後實現buttonWasPressed方法,在函數體中拋出錯誤,若是繼承它的子類沒有實現buttonWasPressed方法就會在狀態切換時拋出異常,這樣至少在程序運行期間就能夠發現錯誤,下面優化上面的代碼:函數

class State {
  buttonWasPressed () {
    throw new Error('父類的buttonWasPressed必須被重寫')
  }
}

class OffLightState extend State {
  construct (light) {
    this.light = light
  }

  buttonWasPressed () {
    console.log('弱光')
    this.light.setState(this.light.weakLightState)
  }
}
複製代碼

狀態模式中的性能優化點

在上面的例子,從性能方面考慮,還有一些能夠優化的點:性能

  • 有兩種方式能夠選擇來管理state對象的建立和銷燬。第一種是當state對象被須要的時候才建立並隨後銷燬;另外一種是一開始就建立好全部的狀態對象,而且始終不銷燬它們。若是state對象比較大,能夠用第一種方式來節省內存。若是狀態改變很頻繁,則最好是將state對象都建立出來,也沒有必要銷燬它們。
  • 咱們爲每一個Context對象都建立了一組state對象,實際上這些state對象之間是能夠共享的,各個Context對象能夠共享一個state對象,這也是享元模式的應用場景之一。

狀態模式 VS 策略模式

狀態模式和策略模式像一對雙胞胎,它們都封裝了一系列的算法或者行爲,他們的類圖看起來幾乎如出一轍,可是從意圖上看它們有很大不一樣。

它們的相同點是,都有一個上下文、一些策略類或者狀態類,上下文把請求委託給這些類來執行。它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何關係,因此客戶必須熟知這些策略類的做用,以便客戶本身能夠隨時主動切換算法。可是在狀態模式中,狀態和狀態對應的行爲早已被封裝好,狀態之間的切換也早就被規定,「改變行爲」這件事發生在狀態模式的內部,對於客戶來講,不須要了解這些細節。優化

JavaScript版本的狀態機

上面咱們使用的是傳統的面向對象的方式實現狀態模式,在JavaScript中,沒有規定狀態對象必定要從類中建立而來。另外,JavaScript能夠很是方便利用委託技術,不須要事先讓一個對象持有另外一個對象,咱們能夠經過Function.prototype.call方法直接把請求委託給某個對象字面來執行。下面看下實現的代碼:ui

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log('關燈')
      this.currentState = FSM.on
    }
  },  
  on: {
    buttonWasPressed: function () {
      console.log('開燈')
      this.currentState = FSM.off
    }
  }
}

var Light = function () {
  this.currentState = FSM.off // 設置初始狀態
  this.button = null
}

Light.prototype.init = function () {
  var self = this

  var button = document.createElement('button')
  this.button = document.body.appendChild(button)
  this.button.innerHTML = '開關'

  this.button.onclick = function () {
    self.currentState.buttonWasPressed.call(self)  // 把請求委託給狀態機FSM
  }
}

const light = new Light()
light.init()
複製代碼

咱們還可使用閉包來編寫這個例子,咱們須要實現一個delegate函數:

var delegate = function (client, delegation) {
  return {
    buttonWasPressed: function () {  // 將客戶的請求委託給delegation對象
      return delegation.buttonWasPressed.apply(client, arguments)
    }
  }
}

var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log('關燈')
      this.currentState = FSM.on
    }
  },  
  on: {
    buttonWasPressed: function () {
      console.log('開燈')
      this.currentState = FSM.off
    }
  }
}

var Light = function () {
  this.offState = delegate(this, FSM.off)
  this.onState = delegate(this, FSM.on)
  this.currentState = this.offState // 設置初始狀態
  this.button = null
}

Light.prototype.init = function () {
  var self = this

  var button = document.createElement('button')
  this.button = document.body.appendChild(button)
  this.button.innerHTML = '開關'

  this.button.onclick = function () {
    self.currentState.buttonWasPressed()
  }
}
複製代碼

總結

在文章中,咱們經過各類方式來實現狀態模式,而且對比了使用狀態模式先後程序的優缺點,從中咱們也能夠得出狀態模式的優勢和缺點。它的優勢以下:

  • 狀態模式定義了狀態和行爲之間的關係,並它們封裝在一個類裏,使得添加新的狀態和狀態間的切換更容易。
  • 避免了Context無限膨脹,狀態切換的邏輯分佈在狀態類中,也避免了大量的if-else語句。
  • 用對象代替字符串來記錄當前狀態,使得狀態的切換更加一目瞭然。
  • Context中的請求動做和狀態類中封裝的行爲相互獨立切互不影響,也使得修改更加容易。

狀態模式的缺點:第一,咱們須要在系統中定義許多狀態類,編寫不少的狀態類是一項枯燥泛味的工做,這樣也會致使系統中增長不少對象。第二,由於邏輯分散中狀態類中,雖然避開了不受歡迎的條件語句,但也形成了邏輯分散的問題,咱們沒法在一個地方就看清整個狀態機的邏輯。

相關文章
相關標籤/搜索