13 状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变;
13.1 初识状态模式-电灯程序
- 开关控制电灯的打开关闭状态;
var Light = function(){
this.state = 'off'; // 给电灯设置初始状态 off
this.button = null; // 电灯开关按钮
};
// 定义 Light.prototype.init 方法
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();
上面的例子使用一个变量 state
来记录按钮的当前状态,在事件发生时,再根据这个状态来决定下一步的行为;不过当电灯的状态增加时(如强光,弱光状态等),需要手动修改 buttonWasPressed
函数,这样就是违反程序的开放-封闭原则
,状态之间的切换关系是在 buttonWasPressed
函数增加 if-else
判断,当状态很多时, buttonWasPressed
函数会更加难以阅读和维护;
- 状态模式改进电灯程序:
状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以 button
被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为;
![](https://img.haomeiwen.com/i14756387/19ea8fe01c74d7b4.png)
// 首先将定义 3 个状态类,分别是 offLightState(关灯状态)、WeakLightState(弱光状态)、 strongLightState(强光状态) ,每个类都有一个原型方法 buttonWasPressed,代表在各自状态下点击按钮发送的行为;
var OffLightState = function( light ){ this.light = light; };
OffLightState.prototype.buttonWasPressed = function(){
console.log( '弱光' ); // offLightState 对应的行为
this.light.setState( this.light.weakLightState ); // 切换状态到 weakLightState
};
var WeakLightState = function( light ){ this.light = light; };
WeakLightState.prototype.buttonWasPressed = function(){
console.log( '强光' ); // weakLightState 对应的行为
this.light.setState( this.light.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;
};
// Light 初始化方法
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 对象的状态,状态的切换规律事先被完好定义在各个状态类中;
Light.prototype.setState = function( newState ){
this.currState = newState;
};
// 测试效果
var light = new Light();
light.init();
状态模式可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码;状态之间的切换都被分布在状态类内部,这使得我们无需编写过多的 if-else
条件分支语言来控制状态之间的转换;
上面例子中若需要为 light
对象增加一种新的状态时,只需要增加一个新的状态类,再稍稍改变一些现有的代码即可;若现在 light 对象多了一种超强光的状态,代码如下:
// 先增加 SuperStrongLightState 类;
var SuperStrongLightState = function( light ){ this.light = light; };
SuperStrongLightState.prototype.buttonWasPressed = function(){
console.log( '关灯' );
this.light.setState( this.light.offLightState );
};
// 再在 Light 构造函数里新增一个 superStrongLightState 对象:
var Light = function(){
this.offLightState = new OffLightState( this );
this.weakLightState = new WeakLightState( this );
this.strongLightState = new StrongLightState( this );
this.superStrongLightState = new SuperStrongLightState( this ); // 新增 superStrongLightState 对象
this.button = null;
};
// 最后改变状态类之间的切换规则,从 StrongLightState---->OffLightState 变为 StrongLightState---->SuperStrongLightState ---->OffLightState:
StrongLightState.prototype.buttonWasPressed = function(){
console.log( '超强光' ); // strongLightState 对应的行为
this.light.setState( this.light.superStrongLightState ); // 切换状态到 offLightState
};
13.2 状态模式的通用结构
在电灯的例子中,首先定义了 Light
类, Light类在这里也被称为上下文( Context
);随后在 Light
的构造函数中创建每一个状态类的实例对象, Context
将持有这些状态对象的引用,以便把请求委托给状态对象;用户的请求,即点击 button
的动作也是实现在 Context
中的,代码如下:
var Light = function(){
this.offLightState = new OffLightState( this ); // 持有状态对象的引用
...
this.button = null;
};
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
对象的引用,以便调用 light
中的方法或者直接操作 light
对象:
var OffLightState = function( light ){ this.light = light; };
OffLightState.prototype.buttonWasPressed = function(){
console.log( '弱光' );
this.light.setState( this.light.weakLightState );
};
...
13.3 状态模式示例——文件上传
文件上传中,包括有扫描、正在上传、暂停、上传成功、上传失败这几种状态,点击同一个按钮,在上传中和
暂停状态下的行为表现是不一样的,如上传中,点击按钮暂停,暂停中,点击按钮继续播放;
文件上传中,设置 暂停/继续 和 删除两个按钮,点击这两个按钮的发生行为如下:
- 文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的 md5 值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态;
- 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传;
- 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件;
- 文件上传基本实现:
// 上传是一个异步的过程,定义全局函数 window.external.upload 来通知上传进度,把当前的文件状态作为参数state 传入函数中
window.external.upload = function( state ){
console.log( state ); // 可能为 sign、 uploading、 done、 error
};
// 上传的插件对象
var plugin = (function(){
var 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;
})();
// 定义控制上传过程的对象 Upload 类
var Upload = function( fileName ){
this.plugin = plugin;
this.fileName = fileName;
this.button1 = null;
this.button2 = null;
this.state = 'sign'; // 设置初始状态为 waiting
};
// 初始化 Upload 类函数
Upload.prototype.init = function(){
var that = 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(){
var self = this;
this.button1.onclick = function(){
if ( self.state === 'sign' ){ // 扫描状态下,任何操作无效
console.log( '扫描中,点击无效...' );
}else if ( self.state === 'uploading' ){ // 上传中,点击切换到暂停
self.changeState( 'pause' );
}else if ( self.state === 'pause' ){ // 暂停中,点击切换到上传中
self.changeState( 'uploading' );
}else if ( self.state === 'done' ){
console.log( '文件已完成上传, 点击无效' );
}else if ( self.state === 'error' ){
console.log( '文件上传失败, 点击无效' );
}
};
this.button2.onclick = function(){
if ( self.state === 'done' || self.state === 'error' || self.state === 'pause' ){
// 上传完成、上传失败和暂停状态下可以删除
self.changeState( 'del' );
}else if ( self.state === 'sign' ){
console.log( '文件正在扫描中,不能删除' );
}else if ( self.state === 'uploading' ){
console.log( '文件正在上传中,不能删除' );
}
};
};
// Upload.prototype.changeState 方法,负责切换状态之后的具体行为:
Upload.prototype.changeState = function( state ){
switch( state ){
case 'sign':
this.plugin.sign();
this.button1.innerHTML = '扫描中,任何操作无效';
break;
case 'uploading':
this.plugin.uploading();
this.button1.innerHTML = '正在上传,点击暂停';
break;
case 'pause':
this.plugin.pause();
this.button1.innerHTML = '已暂停,点击继续上传';
break;
case 'done':
this.plugin.done();
this.button1.innerHTML = '上传完成';
break;
case 'error':
this.button1.innerHTML = '上传失败';
break;
case 'del':
this.plugin.del();
this.dom.parentNode.removeChild( this.dom );
console.log( '删除完成' );
break;
}
this.state = state;
};
// 测试上传文件
var uploadObj = new Upload( 'JavaScript 设计模式与开发实践' );
uploadObj.init();
window.external.upload = function( state ){ // 插件调用 JavaScript 的方法
uploadObj.changeState( state );
};
window.external.upload( 'sign' ); // 文件开始扫描
setTimeout(function(){
window.external.upload( 'uploading' ); // 1 秒后开始上传
}, 1000 );
setTimeout(function(){
window.external.upload( 'done' ); // 5 秒后上传完成
}, 5000 );
- 状态模式重构文件上传程序:
// 第一步:提供 window.external.upload 函数
window.external.upload = function( state ){
console.log( state ); // 可能为 sign、 uploading、 done、 error
};
var plugin = (function(){
var 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;
})();
// 第二步:改造 Upload 构造函数,在构造函数中为每种状态子类都创建一个实例对象
var Upload = function( 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; // 设置当前状态
};
// 第三步:实现 Upload.prototype.init 方法
Upload.prototype.init = function(){
var that = 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(){
var 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.uploading();
this.currState = this.uploadingState;
};
Upload.prototype.pause = 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 );
};
// 第五步:编写各个状态类的实现
var StateFactory = (function(){
var State = function(){};
State.prototype.clickHandler1 = function(){
throw new Error( '子类必须重写父类的 clickHandler1 方法' );
}
State.prototype.clickHandler2 = function(){
throw new Error( '子类必须重写父类的 clickHandler2 方法' );
}
return function( param ){
var F = function( uploadObj ){ this.uploadObj = uploadObj; };
F.prototype = new State();
for ( var i in param ){
F.prototype[ i ] = param[ i ];
}
return F;
}
})();
var SignState = StateFactory({
clickHandler1: function(){ console.log( '扫描中,点击无效...' ); },
clickHandler2: function(){ console.log( '文件正在上传中,不能删除' ); }
});
var UploadingState = StateFactory({
clickHandler1: function(){ this.uploadObj.pause(); },
clickHandler2: function(){ console.log( '文件正在上传中,不能删除' ); }
});
var PauseState = StateFactory({
clickHandler1: function(){ this.uploadObj.uploading(); },
clickHandler2: function(){ this.uploadObj.del(); }
});
var DoneState = StateFactory({
clickHandler1: function(){ console.log( '文件已完成上传, 点击无效' ); },
clickHandler2: function(){ this.uploadObj.del(); }
});
var ErrorState = StateFactory({
clickHandler1: function(){ console.log( '文件上传失败, 点击无效' ); },
clickHandler2: function(){ this.uploadObj.del(); }
});
// 最后测试
var 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 );
13.4 状态模式的优缺点及性能优化点
- 状态模式的优点:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里,通过增加新的状态类,很容易增加新的状态和转换;
- 避免
Context
无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context
中原本过多的条件分支; - 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然;
-
Context
中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响;
- 状态模式的缺点:
状态模式会在系统中定义许多状态类,并且生成许多对象;同时由于逻辑分散在状态类中,虽然减少了 if-else
分支语句,但也造成了逻辑分散的问题;
- 状态模式性能优化点:
- 有两种选择来管理
state
对象的创建和销毁,第一种是仅当state
对象被需要时才创建并随后销毁,能有效的节省内存;另一种是一开始就创建好所有的状态对象,并且始终不销毁它们,适用于状态的改变很频繁的场景中; - 本章的例子中,为每个
Context
对象都创建了一组state
对象,实际上这些state
对象之间是可以共享的,各Context
对象可以共享一个state
对象,这也是享元模式的应用场景之一;
13.5 状态模式和策略模式的关系
状态模式和策略模式都封装了一系列的算法或者行为,它们的类图看起来来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式;策略模式和状态模式的相同点是都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行;区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以必须熟知这些策略类的作用,以便可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部,因此我们不需要了解这些细节;
13.6 JavaScript 版本的状态机
前面示例都是模拟传统面向对象语言的状态模式实现,为每种状态都定义一个状态子类,然后在 Context
中持有这些状态对象的引用,以便把 currState
设置为当前的状态对象;在 JavaScript 这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外, JavaScript 可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象。
下面的状态机选择了通过 Function.prototype.call
方法直接把请求委托给某个字面量对象来执行:
// 方式1
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();
// 方式2:利用下面的 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();
13.7 状态模式小结
状态模式是非常有效的模式之一,通过状态模式重构代码之后,会让代码会变得清晰。虽然状态模式一开始并不是非常容易理解,但有必须去好好掌握这种设计模式。
系列链接
- JavaScript 设计模式(上)——基础知识
- JavaScript 设计模式(中)——1.单例模式
- JavaScript 设计模式(中)——2.策略模式
- JavaScript 设计模式(中)——3.代理模式
- JavaScript 设计模式(中)——4.迭代器模式
- JavaScript 设计模式(中)——5.发布订阅模式
- JavaScript 设计模式(中)——6.命令模式
- JavaScript 设计模式(中)——7.组合模式
- JavaScript 设计模式(中)——8.模板方法模式
- JavaScript 设计模式(中)——9.享元模式
- JavaScript 设计模式(中)——10.职责链模式
- JavaScript 设计模式(中)——11. 中介者模式
- JavaScript 设计模式(中)——12. 装饰者模式
- JavaScript 设计模式(中)——13.状态模式
- JavaScript 设计模式(中)——14.适配器模式
- JavaScript 设计模式(下)——设计原则
- JavaScript 设计模式练习代码
本文主要参考了《JavaScript设计模式和开发实践》一书
网友评论