状态模式(State)
Jul 27, 2022 ·
10 Min Read
状态模式的关键是区分事物内部状态,内部状态的改变带来事物的行为改变。
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
优点:
- 容易新增新的状态。
- 避免 上下文 过多的条件分支。
- 用对象代替字符串来记录当前状态,更加一目了然。
- 上下文的请求动作 和 状态类中封装的行为 互相独立互不影响。
缺点:
- 会激增对象。
- 逻辑分散。
性能优化:
- 对象仅当被需要时才创建并销毁。(适用于对象比较庞大)
- 一开始创建好所有状态对象,并且始终不销毁。(适用于状态的改变比较频繁)
案例
比如:有一个电灯,电灯上有个开关。当电灯开着的时候,按一次会关,再按一次则被打开。同一个按钮,在不同的状态下,表现出来的行为是不一样的。
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
,违反了开放-封闭原则,并且状态的切换很不明显,使代码难以阅读和维护。
使用状态模式改进
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
// 关灯状态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
),靠程序员新增一种状态时自觉添加此方法是不可靠的,难免有时会忘掉。建议解决方案是和 模板方法模式 一样,让抽象父类的抽象方法直接抛出一个异常。
比如:上传文件,首先会进行文件扫描,扫描完成后进行上传,上传过程中可以点击暂停,点一次暂停,再点一次恢复上传;还有一个删除按钮,只有上传完成和上次失败时候点击删除,其余状态点击无效。
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版状态机
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()
表驱动的状态机
状态模式和策略模式的关系
共同点:都有一个上下文、状态或策略类,上下文把请求委托给这些类来执行。
不同点:
- 策略模式中每个策略类之间是平等又平行的,它们之间没有任何联系,需要使用者知道该策略类的作用,以便随机切换算法。
- 状态模式中,状态和状态对应的行为都早被封装好,切换也是被规定好的,“改变行为”这事件发生在状态模式内部。对使用者来说并不需要了解这细节。
Last edited Feb 15