状态模式(State)
状态模式的关键是区分事物内部状态,内部状态的改变带来事物的行为改变。
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
优点:
- 容易新增新的状态。
- 避免 上下文 过多的条件分支。
- 用对象代替字符串来记录当前状态,更加一目了然。
- 上下文的请求动作 和 状态类中封装的行为 互相独立互不影响。
缺点:
- 会激增对象。
- 逻辑分散。
性能优化:
- 对象仅当被需要时才创建并销毁。(适用于对象比较庞大)
- 一开始创建好所有状态对象,并且始终不销毁。(适用于状态的改变比较频繁)
案例
比如:有一个电灯,电灯上有个开关。当电灯开着的时候,按一次会关,再按一次则被打开。同一个按钮,在不同的状态下,表现出来的行为是不一样的。
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()
表驱动的状态机
状态模式和策略模式的关系
共同点:都有一个上下文、状态或策略类,上下文把请求委托给这些类来执行。
不同点:
- 策略模式中每个策略类之间是平等又平行的,它们之间没有任何联系,需要使用者知道该策略类的作用,以便随机切换算法。
- 状态模式中,状态和状态对应的行为都早被封装好,切换也是被规定好的,“改变行为”这事件发生在状态模式内部。对使用者来说并不需要了解这细节。