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中的请求动做和状态类中封装的行为相互独立切互不影响,也使得修改更加容易。

状态模式的缺点:第一,咱们须要在系统中定义许多状态类,编写不少的状态类是一项枯燥泛味的工做,这样也会致使系统中增长不少对象。第二,由于逻辑分散中状态类中,虽然避开了不受欢迎的条件语句,但也形成了逻辑分散的问题,咱们没法在一个地方就看清整个状态机的逻辑。

相关文章
相关标签/搜索