状态模式是一种常用的面向对象设计模式,多见于对象的状态会影响对象行为的场景。本篇以汽车加速、升/减档为例,介绍状态模式在实际开发中的实现思路以及注意事项。
根据状态机的5要素(状态、动作、事件、迁移、条件),画出目标对象的状态迁移图,和/或用清晰的文字准确描述5要素。以本篇为例:
相对而言,外部事件并不是那么重要。在状态模式中,外部事件并不会直接接触到状态对象,而是通过环境对象来接收事件并通知状态对象。状态对象内部一般不要添加会导致自发性迁移的事件或方法,最好由外部环境对象或其他状态系列的对象负责。
在这里,升档和减档是状态的迁移过程,而不是动作;升档和减档并不会改变“汽车行驶”这个环境对象特有的动作,只是影响行驶的速度
根据状态模式的规范,划分环境对象/类(此处为汽车对象/类,以下简称环境对象)和状态对象/类(此处为档位对象/类,以下简称状态对象)
将环境对象的动作方法中受状态影响的这部分方法的实现(具体代码)转移到状态对象中,并通过调用状态对象的同名方法来执行相应的动作;而与状态无关的动作则保留在环境对象中
在状态对象的动作方法中,对条件进行判定,并在验证通过后执行相应的具体行为,以及在验证失败后提示
对外部事件的监听则需要绑定在环境对象中,并调用环境对象的对应迁移方法
按顺序表述:
可以看出,环境对象原本的方法可以分成三大类:
本例以typescript编写,仅使用一个模块Car.ts和一个主模块index.ts。
index.ts
import { Car } from './scripts/modules/Car'; let car: Car = new Car(); // 类的设计中没有持续加/减速的功能,这里简单替代一下 // time表示加速持续时间,单位是秒,每秒汽车会加速1km/h function faster(time: number) { for (let index = 0; index < time; index++) { car.faster(); console.log('当前速度是:' + car.speed); } } function slower(time: number) { for (let index = 0; index < time; index++) { car.slower(); console.log('当前速度是:' + car.speed); } } // 加速过程 faster(20); // 一档速度上限是15km/h,因此加速20秒只能到15,之后继续踩油只会报警提示 car.upGear(); // 升二档 faster(20); car.upGear(); // 升三档 faster(20); car.upGear(); // 升四档 faster(20); car.upGear(); // 升五档 faster(20); // 五档上限为120km/h,因此这里不会报警提示 // 减速过程 slower(25); car.downGear(); // 减到四挡 slower(60); // 一直减速至停车,不换挡
Car.ts
export class Car { private gear: CarGear = new FirGear(this); speed: number = 0; setGear(gear: CarGear) { this.gear = gear; } // 减速 // 由于减速机制与档位无关,不论速度和档位如何,都可以通过踩刹车减至停车, // 因此不需要将减速方法转移到表示状态的档位类 slower(): void { if (this.speed > 0) { this.speed -= 1; } else { console.warn('汽车已停止'); } } // 加速、升档、减档三个方法都受到档位的限制: // 加速到档位上限后,需要升档才能继续加速,否则会报警提示 // 升档和减档则是分别要求速度达到档位的下限和上限,才允许升档和减档操作成功,否则会报警提示 // 加速 faster() { this.gear.faster(); } // 升档 upGear() { this.gear.upGear(); } // 减档 downGear() { this.gear.downGear(); } } // 表示状态的档位类 class CarGear { protected gearName: string = ''; constructor(protected car: Car, readonly lowerLimit: number = 0, readonly upperLimit: number = 0) { } // 加速 faster(): void { if (this.car.speed < this.upperLimit) { this.car.speed += 1; } else { console.error(`汽车已加速至${this.gearName}上限,请升档`); } } // 升档 upGear(): void { } // 减档 downGear(): void { } } // 一档 class FirGear extends CarGear { constructor(protected car: Car) { super(car, 0, 15); this.gearName = '一档' } // 加速 faster() { if (this.car.speed === 0) { console.warn('汽车启动'); } super.faster(); } // 升档 upGear() { // super.checkUpGear(new SndGear(this.car)); // super.upGear(); let sndGear = new SndGear(this.car); if (this.car.speed >= sndGear.lowerLimit) { this.car.setGear(sndGear); console.info('档位已换至二挡'); } } } // 二挡 class SndGear extends CarGear { constructor(protected car: Car) { super(car, 10, 25) } // 加速 faster() { super.faster(); } // 升档 upGear() { // super.upGear(new TrdGear(this.car)); let trdGear = new TrdGear(this.car); if (this.car.speed >= trdGear.lowerLimit) { this.car.setGear(trdGear); console.log('档位已换至三挡'); } } // 减档 downGear() { // super.upGear(new FirGear(this.car)); let firGear = new FirGear(this.car); if (this.car.speed <= firGear.upperLimit) { this.car.setGear(firGear); console.log('档位已换至一挡'); } } } // 三挡 class TrdGear extends CarGear { constructor(protected car: Car) { super(car, 20, 45) } // 加速 faster() { super.faster(); } // 升档 upGear() { // super.upGear(new FourthGear(this.car)); let fourthGear = new FourthGear(this.car); if (this.car.speed >= fourthGear.lowerLimit) { this.car.setGear(fourthGear); console.log('档位已换至四挡'); } } // 减档 downGear() { // super.upGear(new SndGear(this.car)); let sndGear = new SndGear(this.car); if (this.car.speed <= sndGear.upperLimit) { this.car.setGear(sndGear); console.log('档位已换至二挡'); } } } // 四挡 class FourthGear extends CarGear { constructor(protected car: Car) { super(car, 40, 60) } // 加速 faster() { super.faster(); } // 升档 upGear() { // super.upGear(new FifthGear(this.car)); let fifthGear = new FifthGear(this.car); if (this.car.speed >= fifthGear.lowerLimit) { this.car.setGear(fifthGear); console.log('档位已换至五挡'); } } // 减档 downGear() { // super.upGear(new TrdGear(this.car)); let trdGear = new TrdGear(this.car); if (this.car.speed <= trdGear.upperLimit) { this.car.setGear(trdGear); console.log('档位已换至三挡'); } } } // 五挡 class FifthGear extends CarGear { constructor(protected car: Car) { super(car, 60, 120) } // 加速 faster() { super.faster(); } // 减档 downGear() { // super.upGear(new FourthGear(this.car)); let fourthGear = new FourthGear(this.car); if (this.car.speed <= fourthGear.upperLimit) { this.car.setGear(fourthGear); console.log('档位已换至四挡'); } } }
优点部分:传统的实现思路或者是将状态迁移通过条件分支的形式嵌套起来,通过比较前后状态并选择相应的分支;或者是直接将每一种迁移封装成一个方法。前一种的扩展性很差,每次新增状态或新增迁移路径都需要修改分支或新增分支,而且结构复杂;而后一种方式则是在新增状态或新增迁移路径时,需要新增对应数量的方法,容易导致方法数暴增。这两种方法都只适合状态数和迁移路径较少的场景。
本例中的迁移路径较少,从一档到五档和从五档到一档,可以视作只有两条迁移路径,每条路径有四段变化,可能还看不出状态模式的好处。但若是五个档位间可以任意切换,同时条件也独立判定,此时仍然使用传统模式的话会导致判定结构过于复杂。而使用状态模式就能将条件判定状态迁移的执行分摊到五个状态类(也就是让状态类承担条件判定和状态迁移的职责),降低了环境对象的复杂程度,而每个状态类也只需要负责向其他四个状态类的迁移即可。
比如本例中的汽车遭受撞击,导致档位失灵或者无法换挡,相当于增加了一类状态系列(正常、破损)。此时如果对原有状态对象做修改,增加限制条件,这不符合开闭原则;可能更好的扩展方式是修改环境对象,使其调用新增的正常/破损状态对象,再由该对象去调用档位状态对象。
比如一个播放器具有停止状态(或者说准备状态)、播放状态和暂停状态,三种状态具有的动作是不同的:停止状态只有监听并迁移到播放状态;播放状态具有监听暂停和停止事件,以及处理播放源的方法;暂停状态具有监听(继续)播放和停止播放的事件,以及清除播放资源占用的方法。三种状态的可迁移方向以及拥有的动作均不相同,此时可以认为这是三个子状态系列,因此不能共用一个抽象类(或者抽象类不能包含所有方法,一般而言不需要给暂停状态添加处理播放源的方法)。或者这类场景可能需要结合其他设计模式。