狀態模式(State Pattern)允許一個對象在其內部狀態改變時改變它的行爲,對象看起來似乎修改了它的類,類的行爲隨着它的狀態改變而改變。
當程序需要根據不同的外部情況來做出不同操作時,最直接的方法就是使用 switch-case 或 if-else 語句將這些可能發生的情況全部兼顧到,但是這種做法應付複雜一點的狀態判斷時就有點力不從心,開發者得找到合適的位置添加或修改代碼,這個過程很容易出錯,這時引入狀態模式可以某種程度上緩解這個問題。
1. 你曾見過的狀態模式
等紅綠燈的時候,紅綠燈的狀態和行人汽車的通行邏輯是有關聯的:
1. 紅燈亮:行人通行,車輛等待;
2. 綠燈亮:行人等待,車輛通行;
3. 黃燈亮:行人等待,車輛等待;
還有下載文件的時候,就有好幾個狀態,比如下載驗證、下載中、暫停下載、下載完畢、失敗,文件在不同狀態下表現的行爲也不一樣,比如下載中時顯示可以暫停下載和下載進度,下載失敗時彈框提示並詢問是否重新下載等等。類似的場景還有很多,比如電燈的開關狀態、電梯的運行狀態等,女生作爲你的朋友、好朋友、女朋友、老婆等不同狀態的時候,行爲也不同 。
在這些場景中,有以下特點:
1. 對象有有限多個狀態,且狀態間可以相互切換;
2. 各個狀態和對象的行爲邏輯有比較強的對應關係,即在不同狀態時,對應的處理邏輯不一樣;
2. 實例的代碼實現
我們使用 JavaScript 來將上面的交通燈例子實現一下。
先用 IIFE 的方式:
// 反模式,不推介
var trafficLight = (function () {
// 閉包緩存狀態
var state = '綠燈' ;
return {
// 設置交通燈狀態
setState: function (target) {
if (target === '紅燈') {
state = '紅燈';
console.log('交通燈顏色變爲 紅色,行人通行 & 車輛等待');
} else if (target === '黃燈') {
state = '黃燈';
console.log('交通燈顏色變爲 黃色,行人等待 & 車輛等待');
} else if (target === '綠燈') {
state = '綠燈';
console.log('交通燈顏色變爲 綠色,行人等待 & 車輛通行');
} else {
console.error('交通燈還有這顏色?');
}
},
// 獲取交通燈狀態
getState: function () {
return state
}
}
})()
trafficLight.setState('紅燈');
// 交通燈顏色變爲 紅色,行人通行 & 車輛等待
trafficLight.setState('黃燈');
// 交通燈顏色變爲 黃色,行人等待 & 車輛等待
trafficLight.setState('綠燈');
// 交通燈顏色變爲 綠色,行人等待 & 車輛通行
trafficLight.setState('紫燈');
// 交通燈還有這顏色?
在模塊模式裏面通過 if-else 來區分不同狀態的處理邏輯,也可以使用 switch-case,如果對模塊模式不瞭解的,可以看一下本專欄第 27 篇,專門對模塊模式進行了探討。
但是這個實現存在有問題,這裏的處理邏輯還不夠複雜,如果複雜的話,在添加新的狀態時,比如增加了 藍燈、紫燈 等顏色及其處理邏輯的時候,需要到 setState 方法裏找到對應地方修改。在實際項目中,if-else 伴隨的業務邏輯處理通常比較複雜,找到要修改的狀態就不容易,特別是如果是別人的代碼,或者接手遺留項目時,需要看完這個 if-else 的分支處理邏輯,新增或修改分支邏輯的過程中也很容易引入 Bug。
那有沒有什麼方法可以方便地維護狀態及其對應行爲,又可以不用維護一個龐大的分支判斷邏輯呢。這就引入了狀態模式的理念,狀態模式把每種狀態和對應的處理邏輯封裝在一起(後文爲了統一,統稱封裝到狀態類中),比如下面我們用一個類實例將邏輯封裝起來:
// 抽象狀態類
var AbstractState = function() {};
// 抽象方法
AbstractState.prototype.employ = function() {
throw new Error('抽象方法不能調用!');
};
//////////////////////////////////////////////////////////
// 交通燈狀態類
var State = function(name, desc) {
this.color = { name, desc };
}
State.prototype = new AbstractState();
State.prototype.employ = function(trafficLight) {
console.log('交通燈顏色變爲 ' + this.color.name + ',' + this.color.desc);
trafficLight.setState(this);
};
//////////////////////////////////////////////////////////
// 交通燈類
var TrafficLight = function() {
this.state = null;
};
// 獲取交通燈狀態
TrafficLight.prototype.getState = function() {
return this.state;
};
// 設置交通燈狀態
TrafficLight.prototype.setState = function(state) {
this.state = state;
};
// 實例化一個紅綠燈
var trafficLight = new TrafficLight();
//////////////////////////////////////////////////////////
// 實例化紅綠燈可能有的三種狀態
var redState = new State('紅色', '行人等待 & 車輛等待');
var greenState = new State('綠色', '行人等待 & 車輛通行');
var yellowState = new State('黃色', '行人等待 & 車輛等待');
redState.employ(trafficLight) ;
// 交通燈顏色變爲 紅色,行人通行 & 車輛等待
yellowState.employ(trafficLight);
// 交通燈顏色變爲 黃色,行人等待 & 車輛等待
greenState.employ(trafficLight);
// 交通燈顏色變爲 綠色,行人等待 & 車輛通行
這裏的不同狀態是同一個類的類實例,比如 redState這個類實例,就把所有紅燈狀態處理的邏輯封裝起來,如果要把狀態切換爲紅燈狀態,那麼只需要 redState.employ() 把交通燈的狀態切換爲紅色,並且把交通燈對應的行爲邏輯也切換爲紅燈狀態。
如果你看過前面的策略模式,是不是感覺到有那麼一絲似曾相識,策略模式把可以相互替換的策略算法提取出來,而狀態模式把事物的狀態及其行爲提取出來。
// 抽象狀態類
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 redState = new State('紅色', '行人等待 & 車輛等待');
const greenState = new State('綠色', '行人等待 & 車輛通行');
const yellowState = new State('黃色', '行人等待 & 車輛等待');
redState.employ(trafficLight);
// 交通燈顏色變爲 紅色,行人通行 & 車輛等待
yellowState.employ(trafficLight);
// 交通燈顏色變爲 黃色,行人等待 & 車輛等待
greenState.employ(trafficLight);
// 交通燈顏色變爲 綠色,行人等待 & 車輛通行
如果要新建狀態,不用修改原有代碼,只要加上下面的代碼:
// 接上面
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 RedState extends AbstractState {
constructor() {
super()
this.colorState = '紅色'
}
// 覆蓋抽象方法
employ() {
console.log('交通燈顏色變爲 ' + this.colorState + ',行人通行 & 車輛等待');
// 業務相關操作
// const redDom = document.getElementById('color-red');
// redDom.click();
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.yellowState)
}
}
// 交通燈類-綠燈
class GreenState extends AbstractState {
constructor() {
super()
this.colorState = '綠色'
}
// 覆蓋抽象方法
employ() {
console.log('交通燈顏色變爲 ' + this.colorState + ',行人等待 & 車輛通行');
// 業務相關操作
// const greenDom = document.getElementById('color-green');
// greenDom.click();
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.redState);
}
}
// 交通燈類-黃燈
class YellowState extends AbstractState {
constructor() {
super()
this.colorState = '黃色'
}
// 覆蓋抽象方法
employ() {
console.log('交通燈顏色變爲 ' + this.colorState + ',行人等待 & 車輛等待');
// 業務相關操作
// const yellowDom = document.getElementById('color-yellow');
// yellowDom.click();
}
changeState(trafficLight) {
trafficLight.setState(trafficLight.greenState)
}
}
// 交通燈類
class TrafficLight {
constructor() {
this.redState = new RedState();
this.greenState = new GreenState();
this.yellowState = new YellowState();
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();
// 交通燈顏色變爲 綠色,行人等待 & 車輛通行
代碼和預覽參見:Codepen - 狀態模式traffic-light
效果如下:
如果我們要增加新的交通燈顏色,也是很方便的:
// 接上面
// 交通燈類-藍燈
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);
// 交通燈顏色變爲 藍色,行人倒立 & 車輛飛起
對原來的代碼沒有修改,非常符合開閉原則了。
3. 狀態模式的原理
所謂對象的狀態,通常指的就是對象實例的屬性的值。行爲指的就是對象的功能,行爲大多可以對應到方法上。狀態模式把狀態和狀態對應的行爲從原來的大雜燴代碼中分離出來,把每個狀態所對應的功能處理封裝起來,這樣選擇不同狀態的時候,其實就是在選擇不同的狀態處理類。
也就是說,狀態和行爲是相關聯的,它們的關係可以描述總結成:決定行爲。由於狀態是在運行期被改變的,因此行爲也會在運行期根據狀態的改變而改變,看起來,同一個對象,在不同的運行時刻,行爲是不一樣的,就像是類被修改了一樣。
爲了提取不同的狀態類共同的外觀,可以給狀態類定義一個共同的狀態接口或抽象類,正如之前最後的兩個代碼示例一樣,這樣可以面向統一的接口編程,無須關心具體的狀態類實現。
4. 狀態模式的優缺點
狀態模式的優點:
1. 結構相比之下清晰,避免了過多的 switch-case 或 if-else 語句的使用,避免了程序的複雜性提高系統的可維護性;
2. 符合開閉原則,每個狀態都是一個子類,增加狀態只需增加新的狀態類即可,修改狀態也只需修改對應狀態類就可以了;
3. 封裝性良好,狀態的切換在類的內部實現,外部的調用無需知道類內部如何實現狀態和行爲的變換。
狀態模式的缺點:
1. 引入了多餘的類,每個狀態都有對應的類,導致系統中類的個數增加。
5. 狀態模式的適用場景
1. 操作中含有龐大的多分支的條件語句,且這些分支依賴於該對象的狀態,那麼可以使用狀態模式來將分支的處理分散到單獨的狀態類中;
2. 對象的行爲隨着狀態的改變而改變,那麼可以考慮狀態模式,來把狀態和行爲分離,雖然分離了,但是狀態和行爲是對應的,再通過改變狀態調用狀態對應的行爲;
6. 其他相關模式
6.1. 狀態模式和策略模式
狀態模式和策略模式在之前的代碼就可以看出來,看起來比較類似,他們的區別:
1. 狀態模式: 重在強調對象內部狀態的變化改變對象的行爲,狀態類之間是平行的,無法相互替換;
2. 策略模式: 策略的選擇由外部條件決定,策略可以動態的切換,策略之間是平等的,可以相互替換;
狀態模式的狀態類是平行的,意思是各個狀態類封裝的狀態和對應的行爲是相互獨立、沒有關聯的,封裝的業務邏輯可能差別很大毫無關聯,相互之間不可替換。但是策略模式中的策略是平等的,是同一行爲的不同描述或者實現,在同一個行爲發生的時候,可以根據外部條件挑選任意一個實現來進行處理。
6.2. 狀態模式和發佈-訂閱模式
這兩個模式都是在狀態發生改變的時候觸發行爲,不過發佈-訂閱模式的行爲是固定的,那就是通知所有的訂閱者,而狀態模式是根據狀態來選擇不同的處理邏輯。
1. 狀態模式:根據狀態來分離行爲,當狀態發生改變的時候,動態地改變行爲;
2. 發佈-訂閱模式:發佈者在消息發生時通知訂閱者,具體如何處理則不在乎,或者直接丟給用戶自己處理;
這兩個模式是可以組合使用的,比如在發佈-訂閱模式的發佈消息部分,當對象的狀態發生了改變,觸發通知了所有的訂閱者後,可以引入狀態模式,根據通知過來的狀態選擇相應的處理。
6.3. 狀態模式和單例模式
之前的示例代碼中,狀態類每次使用都 new 出來一個狀態實例,實際上使用同一個實例即可,因此可以引入單例模式,不同的狀態類可以返回的同一個實例。