概念
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
描述
通常谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯,我们通常都会这么写代码。
【初始版本】
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、违反开放——封闭原则的,每次新增或者修改light的状态,buttonWasPressed成为了一个非常不稳定的方法。如果以后这个电灯又增加了强强光、超强光和终极强光,那将无法预计这个方法将膨胀到什么地步。
2、状态的切换非常不明显,仅仅表现为对state变量赋值,比如this.state='weakLight'
,在实际开发中,这样的操作很容易被程序员不小心漏掉,也没有办法一目了然地明白电灯一共有多少种状态,除非耐心地读完buttonWasPressed方法里的所有代码。
【状态模式】
下面进入状态模式的代码编写阶段,首先将定义3个状态类,分别是offLightState
、WeakLightState
、strongLightState
。这3个类都有一个原型方法buttonWasPressed,代表在各自状态下,按钮被按下时将发生的行为,代码如下:
// 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类,现在使用更加立体化的状态对象来记录当前的状态而不再使用一个字符串。在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.prototype.setState方法,状态对象可以通过这个方法来切换light对象的状态。状态的切换规律事先被完好定义在各个状态类中。在Context中再也找不到任何一个跟状态切换相关的条件分支语句:
Light.prototype.setState = function( newState ){
this.currState = newState;
}
现在可以进行一些测试:
var light = new Light()
light.init()
使用状态模式代码量增加了好多,但好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。另外,状态之间的切换都被分布在状态类内部,这使得无需编写过多的if、else条件分支语言来控制状态之间的转换。
应用
不论是文件上传,还是音乐、视频播放器,都可以找到一些明显的状态区分。比如文件上传程序中有扫描、正在上传、暂停、上传成功、上传失败这几种状态,音乐播放器可以分为加载中、正在播放、暂停、播放完毕这几种状态。点击同一个按钮,在上传中和暂停状态下的行为表现是不一样的,同时它们的样式class也不同。
相对于电灯的例子,文件上传不同的地方在于,现在将面临更加复杂的条件切换关系。在电灯的例子中,电灯的状态总是从关到开再到关,或者从关到弱光、弱光到强光、强光再到关。看起来总是循规蹈矩的A→B→C→A,所以即使不使用状态模式来编写电灯的程序,而是使用原始的if、else来控制状态切换,也不至于在逻辑编写中迷失自己,因为状态的切换总是遵循一些简单的规律。而文件上传的状态切换相比要复杂得多,控制文件上传的流程需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件。
文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态。上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。
浏览器插件来帮助完成文件上传。插件类型根据浏览器的不同,有可能是ActiveXObject,也有可能是WebkitPlugin。上传是一个异步的过程,所以控件会不停地调用JS提供的一个全局函数window.external.upload
,来通知JS目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload。
我们用状态模式来模拟文件的上传过程,无法提供一个完整的上传插件,window.external.upload函数在此例中只负责打印一些log:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
window.external.upload = function( state ){
console.log( state ) // 可能为sign、uploading、done、error
}
let plugin = (function(){
let 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
})()
class Upload {
constructor(fileName) {
this.plugin = plugin
this.fileName = fileName
this.button1 = null
this.button2 = null
this.signState = new SignState( this ) // 设置初始状态为waiting
this.uploadingState = new UploadingState( this )
this.pauseState = new PauseState( this )
this.doneState = new DoneState( this )
this.errorState = new ErrorState( this )
this.currState = this.signState // 设置当前状态
}
init() {
this.dom = document.createElement( 'div' )
this.dom.innerHTML =
`<span>文件名称:${this.fileName}</span>
<button data-action="button1">扫描中</button>
<span>文件名称:${this.fileName}</span>
<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()
}
bindEvent() {
let _this = this
this.button1.onclick = function(){
_this.currState.clickHandler1()
}
this.button2.onclick = function(){
_this.currState.clickHandler2()
}
}
sign() {
this.plugin.sign()
this.currState = this.signState
}
uploading() {
this.button1.innerHTML = '正在上传,点击暂停'
this.plugin.uploading()
this.currState = this.uploadingState
}
pause() {
this.button1.innerHTML = '已暂停,点击继续上传'
this.plugin.pause()
this.currState = this.pauseState
}
done() {
this.button1.innerHTML = '上传完成'
this.plugin.done()
this.currState = this.doneState
}
error() {
this.button1.innerHTML = '上传失败'
this.currState = this.errorState
}
del() {
this.plugin.del()
this.dom.parentNode.removeChild( this.dom )
}
}
let StateFactory = (function(){
let State = function(){}
State.prototype.clickHandler1 = function(){
throw new Error( '子类必须重写父类的clickHandler1 方法' )
}
State.prototype.clickHandler2 = function(){
throw new Error( '子类必须重写父类的clickHandler2 方法' )
}
return function( param ){
let F = function( uploadObj ){
this.uploadObj = uploadObj
}
F.prototype = new State()
for ( let i in param ){
F.prototype[ i ] = param[ i ]
}
return F
}
})()
let SignState = StateFactory({
clickHandler1: function(){
console.log( '扫描中,点击无效...' )
},
clickHandler2: function(){
console.log( '文件正在上传中,不能删除' )
}
})
let UploadingState = StateFactory({
clickHandler1: function(){
this.uploadObj.pause()
},
clickHandler2: function(){
console.log( '文件正在上传中,不能删除' )
}
})
let PauseState = StateFactory({
clickHandler1: function(){
this.uploadObj.uploading()
},
clickHandler2: function(){
this.uploadObj.del()
}
})
let DoneState = StateFactory({
clickHandler1: function(){
console.log( '文件已完成上传, 点击无效' )
},
clickHandler2: function(){
this.uploadObj.del()
}
})
let ErrorState = StateFactory({
clickHandler1: function(){
console.log( '文件上传失败, 点击无效' );
},
clickHandler2: function(){
this.uploadObj.del()
}
})
let 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 )
</script>
</body>
</html>
小结
从上面的两个例子可以总结出状态模式的优缺点。
状态模式的优点:
1、状态模式定义了状态与行为之间的关系,并将它们封装在一个类里,通过增加新的状态类,很容易增加新的状态和转换.
2、避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
3、用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
4、Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
状态模式的缺点是会在系统中定义许多状态类,编写状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,无法在一个地方就看出整个状态机的逻辑。
参考文献
《JavaScript设计模式与开发实践》