JavaScript 設計模式學習第二十篇-狀態模式

狀態模式(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 出來一個狀態實例,實際上使用同一個實例即可,因此可以引入單例模式,不同的狀態類可以返回的同一個實例。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章