Skip to content

状态模式(State)

状态模式的关键是区分事物内部状态,内部状态的改变带来事物的行为改变

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部

优点

  • 容易新增新的状态。
  • 避免 上下文 过多的条件分支。
  • 用对象代替字符串来记录当前状态,更加一目了然。
  • 上下文的请求动作 和 状态类中封装的行为 互相独立互不影响。

缺点

  • 会激增对象。
  • 逻辑分散。

性能优化

  1. 对象仅当被需要时才创建并销毁。(适用于对象比较庞大)
  2. 一开始创建好所有状态对象,并且始终不销毁。(适用于状态的改变比较频繁)

案例

比如:有一个电灯,电灯上有个开关。当电灯开着的时候,按一次会关,再按一次则被打开。同一个按钮,在不同的状态下,表现出来的行为是不一样的。

js
const Light = function () {
    this.state = 'off' // 电灯的初始状态
    this.button = null // 电灯开关按钮
}

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

const light = new Light()
light.init()

完成!实现这个需求不难,而且逻辑简单,使用 state 保存当前按钮状态,当事件发生时,根据这个状态来决定下一步行为。由此看来是没有问题,但如果当这个按钮的状态增加起来的时候,我们就必须改造上面的代码继续添加 if...else,违反了开放-封闭原则,并且状态的切换很不明显,使代码难以阅读和维护。

使用状态模式改进

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部

javascript
// 关灯状态
const OffLightState = function (light) {
  this.light = light
}

OffLightState.prototype.buttonWasPressed = function () {
  console.log('弱光')
  this.light.setState(this.light.weakLightState)
}

// 弱光状态
const WeakLightState = function (light) {
  this.light = light
}

WeakLightState.prototype.buttonWasPressed = function () {
  console.log('强光')
  this.light.setState(this.light.strongLightState)
}

// 强光状态
const StrongLightState = function (light) {
  this.light = light
}

StrongLightState.prototype.buttonWasPressed = function () {
  console.log('关灯')
  this.light.setState(this.light.offLightState)
}

// 电灯
const Light = function () {
  this.offLightState = new OffLightState(this)
  this.weakLightState = new WeakLightState(this)
  this.strongLightState = new StrongLightState(this)
  this.button = null
}

Light.prototype.init = function () {
  const 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.prototype.setState = function (newState) {
  this.currState = newState
}

const light = new Light()
light.init()

// 执行结果:
// 第一次点击:弱光
// 第二次点击:强光
// 第三次点击:关灯

现在它的每一种状态和它的行为之间的关系局部化,都封装在各种的状态类中。无需编写过多的 if...else 来切换状态。

注意:每个状态类中都定义了一些共同的行为方法(如 buttonWasPressed ),靠程序员新增一种状态时自觉添加此方法是不可靠的,难免有时会忘掉。建议解决方案是和 模板方法模式 一样,让抽象父类的抽象方法直接抛出一个异常。

比如:上传文件,首先会进行文件扫描,扫描完成后进行上传,上传过程中可以点击暂停,点一次暂停,再点一次恢复上传;还有一个删除按钮,只有上传完成和上次失败时候点击删除,其余状态点击无效。

javascript
window.external.upload = function (state) {
  console.log(state);
}

const plugin = (function () {
  const plugin = document.createElement('embed')
  plugin.style.display = 'none'
  plugin.type = 'application/txftn-webkit'

  plugin.sign = function () {
    console.log('开始文件扫描');
  }

  plugin.pause = function () {
    console.log('暂停文件上传');
  }

  plugin.uploading = function () {
    console.log('开始文件上传');
  }

  plugin.del = function () {
    console.log('删除文件上传');
  }

  plugin.done = function () {
    console.log('文件上传完成');
  }

  document.body.appendChild(plugin)
  return plugin
})()

const Upload = function (fileName) {
  this.plugin = plugin
  this.fileName = fileName // 上传的文件名
  this.button1 = null // 第一个按钮
  this.button2 = null // 第二个按钮
  this.signState = new SignState(this) // 扫描状态
  this.uploadingState = new UploadingState(this) // 上传中状态
  this.pauseState = new PauseState(this) // 暂停状态
  this.doneState = new DoneState(this) // 上传完成状态
  this.errorState = new ErrorState(this) // 错误状态
  this.currState = this.signState // 当前状态
}

// 初始化方法
Upload.prototype.init = function () {
  const _self = this
  this.dom = document.createElement('div')
  this.dom.innerHTML = `
    <span>文件名称:${this.fileName}</span>
    <button data-action="button1">扫描中</button>
    <button data-action="button2">删除</button>
  `

  document.body.appendChild(this.dom)
  this.button1 = this.dom.querySelector('[data-action="button1"]')
  this.button2 = this.dom.querySelector('[data-action="button2"]')
  // 绑定事件
  this.bindEvent()
}

// 为按钮绑定事件
Upload.prototype.bindEvent = function () {
  const _self = this
  this.button1.onclick = function () {
  // 当前的状态的点击方法
     _self.currState.clickHandler1()
  }

  this.button2.onclick = function () {
    _self.currState.clickHandler2()
  }
}
Upload.prototype.sign = function () {
  this.plugin.sign()
  this.currState = this.signState
}

Upload.prototype.uploading = function () {
  this.button1.innerHTML = '正在上传,点击暂停'
  this.plugin.pause()
  this.currState = this.pauseState
}

Upload.prototype.done = function () {
  this.button1.innerHTML = '上传完成'
  this.plugin.done()
  this.currState = this.doneState
}

Upload.prototype.error = function () {
  this.button1.innerHTML = '上传失败'
  this.currState = this.errorState
}

Upload.prototype.del = function () {
  this.plugin.del()
  this.dom.parentNode.removeChild(this.dom)
}

// 状态工厂函数
const StateFactory = (function () {
  const State = function () {}
  State.prototype.clickHandler1 = function () {
	throw new Error('子类必须重写父类的clickHandler1方法')
  }

  State.prototype.clickHandler2 = function () {
	throw new Error('子类必须重写父类的clickHandler2方法')
  }

  return function (param) {
	const F = function (uploadObj) {
	  this.uploadObj = uploadObj
	}

	F.prototype = new State()

	for (let key in param) {
	  F.prototype[key] = param[key]
	}

	return F
  }
})()

// 以下都是状态类
const SignState = StateFactory({
  clickHandler1: function () {
	console.log('扫描中,点击无效...');
  },
  clickHandler2: function () {
	console.log('文件正在上传中,不能删除');
  }
})

const UploadingState = StateFactory({
  clickHandler1: function () {
	this.uploadObj.pause()
  },
  clickHandler2: function () {
	console.log('文件正在上传中,不能删除');
  }
})

const PauseState = StateFactory({
  clickHandler1: function () {
	this.uploadObj.uploading()
  },
  clickHandler2: function () {
	this.uploadObj.del()
  }
})

const DoneState = StateFactory({
  clickHandler1: function () {
	console.log('文件已完成上传,点击无效');
  },
  clickHandler2: function () {
	this.uploadObj.del()
  }
})

const ErrorState = StateFactory({
  clickHandler1: function () {
	console.log('文件上传失败, 点击无效')

  },
  clickHandler2: function () {
	this.uploadObj.del()
  }
})

// 实例化一个上传对象,并执行初始化
const uploadObj = new Upload('JavaScript设计模式与开发实践')
uploadObj.init()

window.external.upload = function (state) {
  uploadObj[state]();
}

window.external.upload('sign');

setTimeout(function () {
  window.external.upload('uploading') // 1秒后开始上传
}, 1000)

setTimeout(function () {
  window.external.upload('done') // 5秒后上传完成
}, 5000)

以上两个案例都是模拟传统面向对象语言的状态模式实现的。但在 javascript 语言中,没有规定让状态对象一定要从类中创建而来。

JavaScript版状态机

javascript
const Light = function () {
    this.currState = FSM.off
    this.button = null
}

Light.prototype.init = function () {
    const button = document.createElement('button'),
          self = this
    button.innerHTML = '已关灯'
    this.button = document.body.appendChild(button)
    this.button.onclick = function () {
        self.currState.buttonWasPressed.call(self)
    }
}

const 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
        }
    }
}

const light = new Light()
light.init()

表驱动的状态机

javascript-state-machine

状态模式和策略模式的关系

共同点:都有一个上下文、状态或策略类,上下文把请求委托给这些类来执行。

不同点

  • 策略模式中每个策略类之间是平等又平行的,它们之间没有任何联系,需要使用者知道该策略类的作用,以便随机切换算法。
  • 状态模式中,状态和状态对应的行为都早被封装好,切换也是被规定好的,“改变行为”这事件发生在状态模式内部。对使用者来说并不需要了解这细节。

Released under the MIT License.