状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
状态模式并不是一种简单到一目了然的模式(它往往还会带来代码量的增加),但你一旦明白了状态模式的精髓,以后一定会感谢它带给你的无与伦比的好处。
入门案例:电灯
普通电灯
一个电灯,点击开关,切换开闭两个状态,用代码表述就是
代码语言:javascript复制class Light { constructor() { this.btn = null; this.state = 'off'; } init() { this.btn = document.createElement('button'); this.btn.innerHTML = '按我'; document.body.appendChild(this.btn); this.btn.addEventListener('click', () => { this.press(); }); } press() { this.state = this.state == 'off' ? 'on' : 'off'; console.log(this.state); }}
const light = new Light();light.init();
现在就有了一个强大的状态机。
但是现在除了一种新型电灯,表现为按第一下开弱光,第二下强光,第三下关灯。你可能会说,无脑写多一段ifelse就行了。
在前面的文章中,我们已经狠狠地批判了ifelse:写的难看是一方面,更重要的是违反了开放封闭原则,日后有需求调整,press方法将变成一个非常不稳定的方法。
状态模式的电灯
很多时候,当谈到"封装"时,针对的往往是行为。但这次不同,这次封装的是状态。每种状态都有自己的处理方法,被按下时只管调用即可:
代码语言:javascript复制class Off{ constructor(light){ this.light=light; } press(){ console.log('weak light'); this.light.setState(this.light.weak) }}
class Weak{ constructor(light){ this.light=light; } press(){ console.log('strong light'); this.light.setState(this.light.strong) }}
class Strong{ constructor(light){ this.light=light; } press(){ console.log('off light'); this.light.setState(this.light.off) }}
抽象类:
代码语言:javascript复制class Light { constructor() { this.btn = null; this.weak=new Weak(this); this.strong=new Strong(this); this.off=new Off(this); } init() { this.btn = document.createElement('button'); this.btn.innerHTML = '按我'; document.body.appendChild(this.btn);
// 设置初始状态 this.state=this.off; this.btn.addEventListener('click', () => { // 委托状态类来处理逻辑 this.state.press(); }); }
// 设置状态 setState(newState){ this.state=newState }}
// 调用const light = new Light();light.init();
改造后数据的流向很像是链表。已经没有一个ifelse语句了。
使用小结
GoF中对状态模式的定义是:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
上半句意思是说,状态统统封装为单独的类。并把请求委托给这个状态对象;下半句意思是说,从外部看,Light类在不同的状态下表现为不同的行为。
"高级灯"的例子的Light就是上下文。Light每次操作委托的就是状态类的press方法。也就是说无论增加了多少类,都必须实现press。如果忘了实现,建议trycatch抛异常。
优缺点
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
- Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
状态模式的缺点是会在系统中定义许多状态类,编写20个状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑.