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-machineform:目前行為從哪個狀态來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 出來一個狀态執行個體,實際上使用同一個執行個體即可,是以可以引入單例模式,不同的狀态類可以傳回的同一個執行個體。