状态模式

331 阅读5分钟

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变

1. 电灯程序例子

var Light = function(){ 
 this.state = 'off'; // 给电灯设置初始状态 off 
 this.button = null; // 电灯开关按钮
};

Light.prototype.init = function(){ 
 var button = document.createElement( 'button' ), 
 self = this; 
 button.innerHTML = '开关'; 
 this.button = document.body.appendChild( button ); 
 this.button.onclick = function(){ 
 self.buttonWasPressed(); 
 } 
};

Light.prototype.buttonWasPressed = function(){ 
 if ( this.state === 'off' ){ 
 console.log( '开灯' ); 
 this.state = 'on'; 
 }else if ( this.state === 'on' ){ 
 console.log( '关灯' ); 
 this.state = 'off'; 
 } 
}; 
var light = new Light(); 
light.init();

令人遗憾的是,这个世界上的电灯并非只有一种。许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。现在必须改造上面的代码来完成这种新型电灯的制造:

Light.prototype.buttonWasPressed = function(){ 
 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'; 
 } 
};

总结以上例子的缺点:

  1. 很明显 buttonWasPressed 方法是违反开放封闭原则的,每次新增或者修改 light 的状态,都需要改动 buttonWasPressed 方法中的代码,这使得 buttonWasPressed 成为了一个非常不稳定的方法。
  2. 所有跟状态有关的行为,都被封装在 buttonWasPressed 方法里,如果以后这个电灯又增加了强强光、超强光和终极强光,那我们将无法预计这个方法将膨胀到什么地步。当然为了简化示例,此处在状态发生改变的时候,只是简单地打印一条 log 和改变 button 的innerHTML。在实际开发中,要处理的事情可能比这多得多,也就是说,buttonWasPressed方法要比现在庞大得多。
  3. 状态的切换非常不明显,仅仅表现为对 state 变量赋值,比如 this.state = 'weakLight'。在实际开发中,这样的操作很容易被程序员不小心漏掉。我们也没有办法一目了然地明白电灯一共有多少种状态,除非耐心地读完 buttonWasPressed 方法里的所有代码。当状态的种类多起来的时候,某一次切换的过程就好像被埋藏在一个巨大方法的某个阴暗角落里。
  4. 状态之间的切换关系,不过是往 buttonWasPressed 方法里堆砌 if、else 语句,增加或者修改一个状态可能需要改变若干个操作,这使 buttonWasPressed 更加难以阅读和维护。

2. 状态模式改进电灯程序

通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以 button 被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

// OffLightState:
var OffLightState = function (light) {
  this.light = light
}
OffLightState.prototype.buttonWasPressed = function () {
  console.log('弱光') // offLightState 对应的行为
  this.light.setState(this.light.weakLightState) // 切换状态到 weakLightState
}
// WeakLightState:
var WeakLightState = function (light) {
  this.light = light
}
WeakLightState.prototype.buttonWasPressed = function () {
  console.log('强光') // weakLightState 对应的行为
  this.light.setState(this.light.strongLightState) // 切换状态到 strongLightState
}
// StrongLightState:
var StrongLightState = function (light) {
  this.light = light
}
StrongLightState.prototype.buttonWasPressed = function () {
  console.log('关灯') // strongLightState 对应的行为
  this.light.setState(this.light.offLightState) // 切换状态到 offLightState
}

//接下来改写 Light 类,现在不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象。
var Light = function () {
  this.offLightState = new OffLightState(this)
  this.weakLightState = new WeakLightState(this)
  this.strongLightState = new StrongLightState(this)
  this.button = null
}

//在 button 按钮被按下的事件里,Context 也不再直接进行任何实质性的操作,而是通过self.currState.buttonWasPressed()将请求委托给当前持有的状态对象去执行
Light.prototype.init = function () {
  var button = document.createElement('button'),
    self = this
  this.button = document.body.appendChild(button)
  this.button.innerHTML = '开关'
  this.currState = this.offLightState // 设置当前状态
  this.button.onclick = function () {
    self.currState.buttonWasPressed()
  }
}

//状态对象可以通过这个方法来切换 light对象的状态。
Light.prototype.setState = function( newState ){ 
 this.currState = newState; 
};

var light = new Light(); 
light.init();

3. 状态模式的优缺点

  1. 优点:
  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  • 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
  1. 缺点:会在系统中定义许多状态类,另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑。

4. JavaScript 版本的状态机

var Light = function () {
  this.currState = FSM.off // 设置当前状态
  this.button = null
}
Light.prototype.init = function () {
  var button = document.createElement('button'),
    self = this
  button.innerHTML = '已关灯'
  this.button = document.body.appendChild(button)
  this.button.onclick = function () {
    self.currState.buttonWasPressed.call(self) // 把请求委托给 FSM 状态机
  }
}
var FSM = {
  off: {
    buttonWasPressed: function () {
      console.log('关灯')
      this.button.innerHTML = '下一次按我是开灯'
      this.currState = FSM.on
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log('开灯')
      this.button.innerHTML = '下一次按我是关灯'
      this.currState = FSM.off
    },
  },
}
var 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.button.innerHTML = '下一次按我是开灯'
      this.currState = this.onState
    },
  },
  on: {
    buttonWasPressed: function () {
      console.log('开灯')
      this.button.innerHTML = '下一次按我是关灯'
      this.currState = this.offState
    },
  },
}
var Light = function () {
  this.offState = delegate(this, FSM.off)
  this.onState = delegate(this, FSM.on)
  this.currState = this.offState // 设置初始状态为关闭状态
  this.button = null
}
Light.prototype.init = function () {
  var button = document.createElement('button'),
    self = this
  button.innerHTML = '已关灯'
  this.button = document.body.appendChild(button)
  this.button.onclick = function () {
    self.currState.buttonWasPressed()
  }
}
var light = new Light()
light.init()