状态模式
1. 定义
状态模式 (State Pattern)允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类,类的行为随着它的状态改变而改变。
当程序需要根据不同的外部情况来做出不同操作时,最直接的方法就是使用 switch-case
或 if-else
语句将这些可能发生的情况全部兼顾到,但是这种做法应付复杂一点的状态判断时就有点力不从心,开发者得找到合适的位置添加或修改代码,这个过程很容易出错,这时引入状态模式可以某种程度上缓解这个问题。
简单地说就是:
- 对象有自己的状态
- 不同状态下执行的逻辑不一样
- 用来减少if...else子句
2. 通俗的示例
1)等红绿灯的时候,红绿灯的状态和行人汽车的通行逻辑是有关联的:
- 红灯亮:行人通行,车辆等待;
- 绿灯亮:行人等待,车辆通行;
- 黄灯亮:行人等待,车辆等待;
2)下载文件的时候,就有好几个状态;比如下载验证、下载中、暂停下载、下载完毕、失败,文件在不同状态下表现的行为也不一样,比如
- 下载中时显示可以暂停下载和下载进度,
- 下载失败时弹框提示并询问是否重新下载等等。
3. 类图
在上面的那些场景中,有以下特点:
- 对象有有限多个状态,且状态间可以相互切换;
- 各个状态和对象的行为逻辑有比较强的对应关系,即在不同状态时,对应的处理逻辑不一样;
Context(环境类):环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类 State 的实例,这个实例定义当前状态,在具体实现时,它是一个 State 子类的对象。
State(抽象状态类):它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
ConcreteState(具体状态类):它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
4. 案例
4.1 手机电池
4.1.1 大多人的写法
class Battery{
constructor() {
this.amount='high';
}
show() {
if (this.amount == 'high') {
console.log('绿色');
this.amount='middle';
}else if (this.amount == 'middle') {
console.log('黄色');
this.amount='low';
}else{
console.log('红色');
}
}
}
let battery=new Battery();
battery.show();
battery.show();
battery.show();
存在的问题
- show违反开放-封闭原则
- show方法(胖函数)逻辑太多太复杂
- 颜色状态切换不明显
- 过多的 if/else 让代码不可维护
4.1.2 优化一
class SuccessState{
show(){console.log('绿色');}
}
class WarningState{
show(){console.log('黄色');}
}
class ErrorState{;'l show(){console.log('红色');}
}
class WorstErrorState{
show(){console.log('深红色');}
}
class Battery{
constructor(){
this.amount = 'high';
this.state = new SuccessState();//绿色状态,满电的状态
}
show(){
this.state.show();//把显示的逻辑委托给了状态对象
//内部还要维护状态的变化
if(this.amount == 'high'){
this.amount = 'middle';
this.state = new WarningState();
}else if(this.amount == 'middle'){
this.amount = 'low';
this.state = new ErrorState();
}else if(this.amount == 'low'){
this.amount = 'superlow';
this.state = new WorstErrorState();
}
}
}
let battery = new Battery();
battery.show();
battery.show();
battery.show();
battery.show();
4.1.3 优化二
class SuccessState {
constructor(private battery: Battery) { }
show() {
console.log("绿色", this.battery.amount)
this.battery.setState(new WarningState(this.battery))
}
}
class WarningState {
constructor(private battery: Battery) { }
show() {
console.log("黄色", this.battery.amount)
this.battery.setState(new ErrorState(this.battery))
}
}
class ErrorState {
constructor(private battery: Battery) { }
show() {
console.log("红色", this.battery.amount)
// this.battery.setState(new WorstErrorState(this.battery))
}
}
// class WorstErrorState {
// constructor(private battery: Battery) { }
// show() {
// console.log("深红色", this.battery.amount)
// }
// }
class Battery {
amount
private state
constructor() {
this.amount = "high"
this.state = new SuccessState(this) //绿色状态,满电的状态
}
setState(newState: any) {
this.state = newState
}
show() {
this.state.show() //把显示的逻辑委托给了状态对象
}
}
let battery = new Battery()
battery.show()
battery.show()
battery.show()
battery.show()
4.2 交通灯
使用 JavaScript 来将上面的交通灯例子实现一下。
如果通过 if-else 或 switch-case 来区分不同状态的处理逻辑,会存在这样的问题:在添加新的状态时,比如增加了 蓝灯、紫灯 等颜色及其处理逻辑的时候,需要到每个状态里找到相应的地方修改。业务处理逻辑越复杂,找到要修改的状态就不容易,特别是如果是别人的代码,或者接手遗留项目时,需要看完这个 if-else 的分支处理逻辑,新增或修改分支逻辑的过程中也很容易引入 Bug。
因此我们可以把每种状态和对应的处理逻辑封装在一起,放到一个状态类中:
/* 抽象状态类 */
class AbstractState {
constructor() {
if (new.target === AbstractState) {
throw new Error('抽象类不能直接实例化!');
}
}
/* 抽象方法 */
employ() {
throw new Error('抽象方法不能调用!');
}
}
/* 交通灯状态类 */
class State extends AbstractState {
constructor(name, desc) {
super();
this.color = { name, desc };
}
/* 覆盖抽象方法 */
employ(trafficLight) {
console.log('交通灯颜色变为 ' + this.color.name + ',' + this.color.desc);
trafficLight.setState(this);
}
}
/* 交通灯类 */
class TrafficLight {
constructor() {
this.state = null;
}
/* 获取交通灯状态 */
getState() {
return this.state;
}
/* 设置交通灯状态 */
setState(state) {
this.state = state;
}
}
const trafficLight = new TrafficLight();
const greenState = new State('绿色', '可以通行');
const yellowState = new State('黄色', '大家等一等');
const redState = new State('红色', '都给我停下来');
greenState.employ(trafficLight); // 输出: 交通灯颜色变为 绿色,可以通行
yellowState.employ(trafficLight); // 输出: 交通灯颜色变为 黄色,大家等一等
redState.employ(trafficLight); // 输出: 交通灯颜色变为 红色,都给我停下来
这里的不同状态是同一个类的类实例,比如 redState 这个类实例,就把所有红灯状态处理的逻辑封装起来,如果要把状态切换为红灯状态,那么只需要 redState.employ() 把交通灯的状态切换为红色,并且把交通灯对应的行为逻辑也切换为红灯状态。
如果要新建状态,不用修改原有代码,只要加上下面的代码:
// 接上面
const blueState = new State('蓝色', '这是要干啥')
blueState.employ(trafficLight) // 输出: 交通灯颜色变为 蓝色,这是要干啥
传统的状态区分一般是基于状态类扩展的不同状态类,如何实现看需求具体了,比如逻辑比较复杂,通过新建状态实例的方法已经不能满足需求,那么可以使用状态类的方式。
这里提供一个状态类的实现,同时引入状态的切换逻辑:
/* 抽象状态类 */
class AbstractState {
constructor() {
if (new.target === AbstractState) {
throw new Error('抽象类不能直接实例化!');
}
}
/* 抽象方法 */
employ() {
throw new Error('抽象方法不能调用!');
}
changeState() {
throw new Error('抽象方法不能调用!');
}
}
/* 交通灯类-绿灯 */
class GreenState extends AbstractState {
constructor() {
super();
this.colorState = '绿色';
}
/* 覆盖抽象方法 */
employ() {
console.log('交通灯颜色变为 ' + this.colorState + ',可以通行');
// 省略业务相关操作
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.yellowState);
}
}
/* 交通灯类-黄灯 */
class YellowState extends AbstractState {
constructor() {
super();
this.colorState = '黄色';
}
/* 覆盖抽象方法 */
employ() {
console.log('交通灯颜色变为 ' + this.colorState + ',大家等一等');
// 省略业务相关操作
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.redState);
}
}
/* 交通灯类-红灯 */
class RedState extends AbstractState {
constructor() {
super();
this.colorState = '红色';
}
/* 覆盖抽象方法 */
employ() {
console.log('交通灯颜色变为 ' + this.colorState + ',都给我停下来');
// 省略业务相关操作
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.greenState);
}
}
/* 交通灯类 */
class TrafficLight {
constructor() {
this.greenState = new GreenState();
this.yellowState = new YellowState();
this.redState = new RedState();
this.state = this.greenState;
}
/* 设置交通灯状态 */
setState(state) {
state.employ(this);
this.state = state;
}
changeState() {
this.state.changeState(this);
}
}
const trafficLight = new TrafficLight();
trafficLight.changeState(); // 输出:交通灯颜色变为 黄色,大家等一等
trafficLight.changeState(); // 输出:交通灯颜色变为 红色,都给我停下来
trafficLight.changeState(); // 输出:交通灯颜色变为 绿色,可以通行
如果我们要增加新的交通灯颜色,也是很方便的:
// 接上面
/* 交通灯类-蓝灯 */
class BlueState extends AbstractState {
constructor() {
super();
this.colorState = '蓝色';
}
/* 覆盖抽象方法 */
employ() {
console.log('交通灯颜色变为 ' + this.colorState + ',这是要干啥');
const redDom = document.getElementById('color-blue');
redDom.click();
}
}
const blueState = new BlueState();
trafficLight.employ(blueState); // 输出:交通灯颜色变为 蓝色,这是要干啥
对原来的代码没有修改,非常符合开闭原则了。
5. 场景
- 操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态,那么可以使用状态模式来将分支的处理分散到单独的状态类中;
- 对象的行为随着状态的改变而改变,那么可以考虑状态模式,来把状态和行为分离,虽然分离了,但是状态和行为是对应的,再通过改变状态调用状态对应的行为;
5.1 Promise
class Promise {
constructor(fn) {
this.state = "initial" //先维护一下初始状态
this.successes = []
this.errors = []
let resolve = (data) => {
this.state = "fulfilled"
this.successes.forEach((item) => item(data))
}
let reject = (error) => {
this.state = "failed"
this.errors.forEach((item) => item(error))
}
fn(resolve, reject)
}
then(success, error) {
this.successes.push(success)
this.errors.push(error)
}
}
let p = new Promise(function (resolve, reject) {
setTimeout(function () {
let num = Math.random()
if (num > 0.5) {
resolve(num)
} else {
reject(num)
}
}, 500)
})
p.then(
(data) => {
console.log("成功", data)
},
(error) => {
console.log("失败", error)
}
)
5.2 React导航
import { Button } from 'antd';
import { useState } from 'react';
const Banner = () => {
const [state, setState] = useState<'show' | 'hide'>('show');
// map映射
const States = {
show: function () {
console.log('banner显示,点击可以关闭');
//....
setState('hide');
},
hide: function () {
console.log('banner隐藏,点击可以打开');
//.....
setState('show');
},
};
const toggle = () => {
States[state]();
};
return (
<div>
{state === 'show' && <nav>导航</nav>}
<Button onClick={toggle}>{state === 'show' ? '隐藏' : '展示'}</Button>
</div>
);
};
export default Banner;
5.3 有限状态机
- 事物拥有多种状态,任一时间只会处于一种状态不会处于多种状态;
- 动作可以改变事物状态,一个动作可以通过条件判断,改变事物到不同的状态,但是不能同时指向多个状态,一个时间,就一个状态
- 状态总数是有限的;
- javascript-state-machine
- form:当前行为从哪个状态来
- to:当前行为执行完会过渡到哪个状态
- name:当前行为的名字
- fsm.can(t) - return true 如果过渡方法t可以从当前状态触发
- fsm.cannot(t) - return true 如果当前状态下不能发生过渡方法t
- fsm.transitions() - 返回从当前状态可以过渡到的状态的列表
- fsm.allTransitions() - 返回所有过渡方法的列表
- fsm.allStates() - 返回状态机有的所有状态的列表
- onBefore 在特定动作TRANSITION前触发
- onLeaveState 离开任何一个状态的时候触发
- onEnter 进入一个特定的状态STATE时触发
- onLeave 在离开特定状态STATE时触发
- onTransition 在任何动作发生期间触发
- onEnterState 当进入任何状态时触发
- on onEnter的简写
- onAfterTransition 任何动作触发后触发
- onAfter 在特定动作TRANSITION后触发
- on onAfter的简写
// let StateMachine = require("javascript-state-machine")
class StateMachine {
constructor(options) {
//init定义初状态 transitions定义转换规则 methods定义监听 函数
let { init = "", transitions = [], methods = {} } = options
this.state = init
transitions.forEach((transition) => {
let { from, to, name } = transition
this[name] = function () {
if (this.state == from) {
this.state = to
let onMethod = "on" + name.slice(0, 1).toUpperCase() + name.slice(1) //onMelt
methods[onMethod] && methods[onMethod]()
}
}
})
}
}
var fsm = new StateMachine({
init: "solid",
transitions: [
{ name: "melt", from: "solid", to: "liquid" },
{ name: "freeze", from: "liquid", to: "solid" },
{ name: "vaporize", from: "liquid", to: "gas" },
{ name: "condense", from: "gas", to: "liquid" },
],
methods: {
onMelt: function () {
console.log("I melted")
},
onFreeze: function () {
console.log("I froze")
},
onVaporize: function () {
console.log("I vaporized")
},
onCondense: function () {
console.log("I condensed")
},
},
})
fsm.melt()
fsm.freeze()
6. 状态模式的优缺点
6.1 优点
- 结构相比之下清晰,避免了过多的 switch-case 或 if-else 语句的使用,避免了程序的复杂性提高系统的可维护性;
- 符合开闭原则,每个状态都是一个子类,增加状态只需增加新的状态类即可,修改状态也只需修改对应状态类就可以了;
- 封装性良好,状态的切换在类的内部实现,外部的调用无需知道类内部如何实现状态和行为的变换。
6.2 缺点
- 引入了多余的类,每个状态都有对应的类,导致系统中类的个数增加。
7. 其他相关模式
7.1 状态模式和策略模式
状态模式和策略模式在之前的代码就可以看出来,看起来比较类似,他们的区别:
- 状态模式:重在强调对象内部状态的变化改变对象的行为,状态类之间是平行的,无法相互替换。
- 策略模式:策略的选择由外部条件决定,策略可以动态的切换,策略之间是平等的,可以相互替换。
- 状态模式的状态类是平行的,意思是各个状态类封装的状态和对应的行为是相互独立、没有关联的,封装的业务逻辑可能差别很大毫无关联,相互之间不可替换。但是策略模式中的策略是平等的,是同一行为的不同描述或者实现,在同一个行为发生的时候,可以根据外部条件挑选任意一个实现来进行处理
7.2 状态模式和观察者模式
这两个模式都是在状态发生改变的时候触发行为,不过观察者模式的行为是固定的,那就是通知所有的订阅者,而状态模式是根据状态来选择不同的处理逻辑。
- 状态模式:根据状态来分离行为,当状态发生改变的时候,动态地改变行为。
- 观察者模式:发布者在消息发生时通知订阅者,具体如何处理则不在乎,或者直接丢给用户自己处理。
这两个模式是可以组合使用的,比如在观察者模式的发布消息部分,当对象的状态发生了改变,触发通知了所有的订阅者后,可以引入状态模式,根据通知过来的状态选择相应的处理。
7.3 状态模式和单例模式
状态类每次使用都 new 出来一个状态实例,实际上使用同一个实例即可,因此可以引入单例模式,不同的状态类可以返回的同一个实例。