js之状态模式
level01:电灯程序
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script> var Light = function(){ this.state = 'off'; // 给电灯设置初始状态 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.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(); </script> </body> </html>
缺点:
- 很明显 buttonWasPressed 方法是违反开放封闭原则的,每次新增或者修改 light 的状态,都需要改动buttonWasPressed 方法中的代码,这使得 buttonWasPressed 成为了一个非常不稳定的方法。
- 在状态多的情况下buttonWasPressed方法可能会很庞大。
- 状态之间的切换关系,不过是往 buttonWasPressed 方法里堆砌 if 、 else 语句,增加或者修改一个状态可能需要改变若干个操作,这使 buttonWasPressed 更加难以阅读和维护。
level02:状态模式改进电灯程序
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script> // 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 }; var Light = function(){ this.offLightState = new OffLightState( this ); this.weakLightState = new WeakLightState( this ); this.strongLightState = new StrongLightState( 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.prototype.setState = function( newState ){ this.currState = newState; }; var light = new Light(); light.init(); </script> </body> </html>
tips:
在前面的电灯例子中,我们完成了一个状态模式程序的编写。首先定义了 Light 类, Light类在这里也被称为上下文(Context)。随后在 Light 的构造函数中,我们要创建每一个状态类的实例对象,Context 将持有这些状态对象的引用,以便把请求委托给状态对象。
我们要编写各种状态类, light 对象被传入状态类的构造函数,状态对象也需要持有 light 对象的引用,以便调用 light 中的方法或者直接操作 light 对象:
var OffLightState = function( light ){ this.light = light; }; OffLightState.prototype.buttonWasPressed = function(){ console.log( '弱光' ); this.light.setState( this.light.weakLightState ); };
状态模式的定义:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
我们以逗号分割,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。电灯的例子足以说明这一点,在 off和 on这两种不同的状态下,我们点击同一个按钮,得到的行为反馈是截然不同的。
level03:状态模式改进电灯程序
憾的是,JavaScript既不支持抽象类,也没有接口的概念。所以在使用状态模式的时候要格外小心,如果我们编写一个状态子
类时,忘记了给这个状态子类实现 buttonWasPressed 方法,则会在状态切换的时候抛出异常。因为 Context总是把请求委托给状态对象的 buttonWasPressed 方法。
不论怎样严格要求程序员,也许都避免不了犯错的那一天,毕竟如果没有编译器的帮助,只依靠程序员的自觉以及一点好运气,是不靠谱的。这里建议的解决方案跟《模板方法模式》中一致,让抽象父类的抽象方法直接抛出一个异常,这个异常至少会在程序运行期间就被发现:
var State = function(){}; State.prototype.buttonWasPressed = function(){ throw new Error( '父类的 buttonWasPressed 方法必须被重写' ); }; var SuperStrongLightState = function( light ){ this.light = light; }; SuperStrongLightState.prototype = new State(); // 继承抽象父类 SuperStrongLightState.prototype.buttonWasPressed = function(){ // 重写 buttonWasPressed 方法 console.log( '关灯' ); this.light.setState( this.light.offLightState ); };
状态模式的优缺点
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中原本过多的条件分支。
- Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
状态模式中的性能优化点
- 有两种选择来管理 state 对象的创建和销毁。第一种是仅当 state 对象被需要时才创建并随后销毁,另一种是一开始就创建好所有的状态对象,并且始终不销毁它们。如果 state对象比较庞大,可以用第一种方式来节省内存,这样可以避免创建一些不会用到的对象并及时地回收它们。但如果状态的改变很频繁,最好一开始就把这些 state 对象都创建出来,也没有必要销毁它们,因为可能很快将再次用到它们。
- 在本章的例子中,我们为每个 Context 对象都创建了一组 state 对象,实际上这些 state对象之间是可以共享的,各Context 对象可以共享一个 state 对象,这也是享元模式的应
用场景之一。
状态模式和策略模式的关系
状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎一模一样,但在意图上有很大不同,因此它们是两种迥然不同的模式。
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
level02:JavaScript 版本的状态机
状态模式是状态机的实现之一,但在 JavaScript这种“无类”语言中,没有规定让状态对象一定要从类中创建而来。另外一点,JavaScript 可以非常方便地使用委托技术,并不需要事先让一个对象持有另一个对象。下面的状态机选择了通过Function.prototype.call 方法直接把请求委托给某个字面量对象来执行。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script> var Light = function(){ this.button = null; }; Light.prototype.init = function(){ var button = document.createElement( 'button' ), self = this; button.innerHTML = '已关灯'; this.currState = FSM.off; // 设置当前状态 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(); </script> </body> </html>