状态模式
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变
将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果
例子
一个电灯开关,按一下开灯,再按一下关灯
1class Light {2 constructor() {3 this.state = 'off'4 this.dom = null5 this.init()6 }78 init() {9 // ...10 }1112 trigger() {13 if (this.state === 'off') {14 this.state = 'on'15 // ...16 } else if (this.state === 'on') {17 this.state = 'off'18 // ...19 }20 }21}
现在我们编写了一个状态机,简单而缜密,可惜世界上不止一种电灯,有的灯按一下开关是强光,再按一下是弱光,再按一下才关闭,如果我们进行修改,就会发现之前的代码不符合开放封闭原则
重构
通常我们谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部
1class OffLightState {2 constructor(light) {3 this.light = light4 }5 trigger() {6 console.log('弱光')7 this.light.setState(this.light.weakLightState)8 }9}1011class WeakLightState {12 constructor(light) {13 this.light = light14 }15 trigger() {16 console.log('强光')17 this.light.setState(this.light.strongLightState)18 }19}2021class StrongLightState {22 constructor(light) {23 this.light = light24 }25 trigger() {26 console.log('关灯')27 this.light.setState(this.light.offLightState)28 }29}3031class Light {32 constructor() {33 this.offLightState = new OffLightState()34 this.weakLightState = new WeakLightState()35 this.strongLightState = new StrongLightState()36 this.state = this.offLightState37 }3839 setState(newState) {40 this.state = newState41 }42}
继续优化,console.log 所代表的一系列操作,可以使用代理模式,当访问新的状态时,由代理(Proxy 的 get)触发 console.log 等一系列操作
State 抽象类
JavaScript 中没有抽象类,我们只能这样实现
1class State {2 constructor(light) {3 this.light = light4 }5 trigger() {6 throw new Error('抽象类 triggle 方法必须重写')7 }8}910class OffLightState extends State {11 constructor(light) {12 super(light)13 }14 trigger() {15 this.light.setState(this.light.weakLightState)16 }17}
状态模式和策略模式
策略模式和状态模式的相同点是,它们都有一个上下文、一些策略或者状态类,上下文把请求委托给这些类来执行
它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在
JavaScript 的状态模式
使用强制更改 this 指向,将状态对象的方法进行委托
1const FSM = {2 off: {3 trigger() {4 console.log('开灯')5 this.state = FSM.on6 }7 },8 on: {9 trigger() {10 console.log('关灯')11 this.state = FSM.off12 }13 },14}1516class Light {17 constructor() {18 this.state = FSM.off19 }20 trigger() {21 this.state.trigger.call(this)22 }23}
优缺点
优点:
定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换
避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支
用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响
缺点:
会在系统中定义许多状态类
由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,我们无法在一个地方就看出整个状态机的逻辑