狀態模式(state pattern)

begin 2020年11月14日20:19:59

狀態模式(state pattern)

引子

鐵扇公主:以前陪我看月亮的時候,叫人家小甜甜,現在新人勝舊人了,叫人家牛夫人!

定義

Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.

當一個對象的內在狀態改變時允許改變其行為,這個對象看起來像是改變了其類。 ——《設計模式:可復用面向對象軟體的基礎》

狀態模式是一個行為型設計模式。

概述

狀態模式的概念和有限狀態機(finite-state machine)相似。有限狀態機:表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學計算模型。

如TCP連接幾個固定的狀態(部分狀態省略):Established,Listening,Closed。

Closed表示初始狀態;Listening表示伺服器端的某個SOCKET處於監聽狀態,可以接受連接;Established表示連接已經建立了。

TCP也有幾個固定的行為(部分行為省略):open,close,synchronize,acknowledge。

open表示開啟連接,close表示關閉連接,synchronize表示同步,acknowledge表示確認。

每個行為根據當前狀態的不一樣,行為表現也是不一樣的,有的是有效行為,有的是無效行為。如果當前狀態是closed狀態,open為有效行為,其他為無效行為。如果當前狀態是Listening狀態,synchronize,acknowledge為有效行為,其他為無效行為。如果當前狀態為established狀態,synchronize,acknowledge,close為有效行為,其他為無效行為。

如果我現在有TCPConnection類,接收請求後需要表現什麼行為,那麼可能是著這樣子:

public class TCPConnection {
    private String state;

    void open() {
        switch (state) {
            case "closed":
                // open創建連接
                state = "established";
                break;
            case "established":
            case "listening":
                // 無效行為
                break;
        }

    }

    void close() {
        switch (state) {
            case "closed":
                // 無效行為
                break;
            case "established":
            case "listening":
                // close關閉連接
                break;
        }
    }

    // ...(省略)

}

狀態模式將狀態抽象出來,封裝狀態相對應的操作,當狀態轉換時,操作也跟著轉換。每個具體狀態只需要一個,具體狀態類提供

// TCP連接
public class TCPConnectionUseStatePattern {
    private TCPState tcpState;
    private ClosedState closedState;
    private EstablishedState establishedState;
    
    TCPConnectionUseStatePattern() {
        closedState = new ClosedState();
        establishedState = new EstablishedState();
        tcpState = closedState;
    }

    void open() {
        tcpState.open(this);
    }

    void close() {
        tcpState.close(this);
    }

    void changeState(TCPState tcpState) {
        this.tcpState = tcpState;
    }

    public ClosedState getClosedState() {
        return closedState;
    }

    public EstablishedState getEstablishedState() {
        return establishedState;
    }
}
// TCP狀態
public interface TCPState {
    void open(TCPConnectionUseStatePattern tcpConnectionUseStatePattern);
    void close(TCPConnectionUseStatePattern tcpConnectionUseStatePattern);
    // ...(省略)
}
// 關閉狀態
public class ClosedState implements TCPState {
    @Override
    public void open(TCPConnectionUseStatePattern tcpConnectionUseStatePattern) {
        System.out.println("創建連接");
        System.out.println("更新狀態");
        tcpConnectionUseStatePattern.changeState(tcpConnectionUseStatePattern.getEstablishedState());
    }

    @Override
    public void close(TCPConnectionUseStatePattern tcpConnectionUseStatePattern) {
        System.out.println("已關閉,無需重複關閉");
    }
}
// 已連接狀態
public class EstablishedState implements TCPState {
    @Override
    public void open(TCPConnectionUseStatePattern tcpConnectionUseStatePattern) {
        System.out.println("已連接,無需重複連接");
    }

    @Override
    public void close(TCPConnectionUseStatePattern tcpConnectionUseStatePattern) {
        System.out.println("關閉連接");
        System.out.println("更新狀態");
        tcpConnectionUseStatePattern.changeState(tcpConnectionUseStatePattern.getClosedState());
    }
}

圖示

狀態模式結構圖:

state-pattern-structure-diagram

角色

上下文角色(Context):

  • 定義客戶端使用的介面方法
  • 維護一個具體狀態類實例,定義當前狀態

抽象狀態類(State):定義封裝了與Context狀態相關的行為的一組介面

具體狀態類(Concrete State):實現State,實現與Context狀態相關的具體行為

程式碼示例

假設有一個自動售賣機,需要投幣,按按鈕,飲料就會滾出來。

自動售賣機有四個狀態,分別是:沒有硬幣沒有飲料(No Coin,No Drinks)、有硬幣沒有飲料(Has Coin,No Drinks)、有硬幣有飲料(Has Coin And Drinks)、沒有硬幣有飲料(No Coin,Has Drinks),有是兩個行為:投幣(Insert Coin)、按按鈕(Press Button)、添加飲料(Add Drinks)。

狀態模式示例

圖中部分行為未畫全,畫出了正常的流程,有飲料時投幣按按鈕分派飲料(沒有硬幣有飲料<->有硬幣有飲料),無飲料時投幣按按鈕返還硬幣(沒有飲料沒有硬幣<->有硬幣沒有飲料)。還有其他狀態的行為,如在有硬幣有飲料的時候,在投幣,會拒絕,直接返還硬幣等。

對應不同的狀態,封裝了不同的行為,當改變狀態時,相當於改變了其行為。有限個狀態,並且彼此知道彼此的存在。

talk is cheap, show you code.

上下文角色(Context):

// 飲料自動售賣機
public class DrinksVendingMachine {
    private DrinksVendingMachineState state;
    private NoCoinHasDrinks noCoinHasDrinks;
    private NoCoinNoDrinks noCoinNoDrinks;
    private HasCoinAndDrinks hasCoinAndDrinks;
    private HasCoinNoDrinks hasCoinNoDrinks;
    private int size = 0; // 當前飲料數
    private int capacity = 3; // 最大飲料數

    public DrinksVendingMachine() {
        this.noCoinHasDrinks = new NoCoinHasDrinks();
        this.noCoinNoDrinks = new NoCoinNoDrinks();
        this.hasCoinAndDrinks = new HasCoinAndDrinks();
        this.hasCoinNoDrinks = new HasCoinNoDrinks();
        this.state = noCoinNoDrinks;
    }

    public void insertCoin() {
        state.insertCoin(this);
    }

    public void pressButton() {
        state.pressButton(this);
    }

    public void addDrinks() {
        state.addDrinks(this, capacity - size); // 加滿
    }

    public void changeState(DrinksVendingMachineState state) {
        this.state = state;
    }

    public boolean isEmpty() {
        return this.size == 0;
    }

    public boolean isFull() {
        return this.size == this.capacity;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public int getSize() {
        return this.size;
    }

    public NoCoinHasDrinks getNoCoinHasDrinks() {
        return noCoinHasDrinks;
    }

    public NoCoinNoDrinks getNoCoinNoDrinks() {
        return noCoinNoDrinks;
    }

    public HasCoinAndDrinks getHasCoinAndDrinks() {
        return hasCoinAndDrinks;
    }

    public HasCoinNoDrinks getHasCoinNoDrinks() {
        return hasCoinNoDrinks;
    }
}

抽象狀態類(State):

// 自動售賣機狀態
public interface DrinksVendingMachineState {
    void insertCoin(DrinksVendingMachine drinksVendingMachine);
    void pressButton(DrinksVendingMachine drinksVendingMachine);
    void addDrinks(DrinksVendingMachine drinksVendingMachine, int num);
}

具體抽象類(ConcreteState):

// 有硬幣有飲料
public class HasCoinAndDrinks implements DrinksVendingMachineState {
    @Override
    public void insertCoin(DrinksVendingMachine drinksVendingMachine) {
        System.out.println("已有硬幣,請取走你的硬幣並按按鈕,謝謝!");
    }

    @Override
    public void pressButton(DrinksVendingMachine drinksVendingMachine) {
        drinksVendingMachine.setSize(drinksVendingMachine.getSize() - 1);
        if (drinksVendingMachine.isEmpty()) {
            drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinNoDrinks());
        } else {
            drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinHasDrinks());
        }
        System.out.println("請取走您的飲料,謝謝!");
    }

    @Override
    public void addDrinks(DrinksVendingMachine drinksVendingMachine, int num) {
        if (drinksVendingMachine.isFull()) {
            System.out.println("自動售賣機飲料已滿,等待賣出後再添加!");
        }
        drinksVendingMachine.setSize(drinksVendingMachine.getSize() + num);
        drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinHasDrinks());
        System.out.println("添加飲料成功!");
    }
}
// 有硬幣沒有飲料
public class HasCoinNoDrinks implements DrinksVendingMachineState {
    @Override
    public void insertCoin(DrinksVendingMachine drinksVendingMachine) {
        System.out.println("已有硬幣,請取走你的硬幣並按按鈕,謝謝!");
    }

    @Override
    public void pressButton(DrinksVendingMachine drinksVendingMachine) {
        drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinNoDrinks());
        System.out.println("暫時無飲料,請取走您的硬幣,謝謝!");
    }

    @Override
    public void addDrinks(DrinksVendingMachine drinksVendingMachine, int num) {
        drinksVendingMachine.setSize(drinksVendingMachine.getSize() + num);
        drinksVendingMachine.changeState(drinksVendingMachine.getHasCoinAndDrinks());
        System.out.println("添加飲料成功!");
    }
}
// 沒有硬幣有飲料
public class NoCoinHasDrinks implements DrinksVendingMachineState {
    @Override
    public void insertCoin(DrinksVendingMachine drinksVendingMachine) {
        drinksVendingMachine.changeState(drinksVendingMachine.getHasCoinAndDrinks());
        System.out.println("投幣成功,請按按鈕!");
    }

    @Override
    public void pressButton(DrinksVendingMachine drinksVendingMachine) {
        System.out.println("請先投幣,謝謝!");
    }

    @Override
    public void addDrinks(DrinksVendingMachine drinksVendingMachine, int num) {
        if (drinksVendingMachine.isFull()) {
            System.out.println("自動售賣機飲料已滿,等待賣出後再添加!");
        }
        drinksVendingMachine.setSize(drinksVendingMachine.getSize() + num);
        drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinHasDrinks());
        System.out.println("添加飲料成功!");
    }
}
// 沒有硬幣沒有飲料
public class NoCoinNoDrinks implements DrinksVendingMachineState {
    @Override
    public void insertCoin(DrinksVendingMachine drinksVendingMachine) {
        drinksVendingMachine.changeState(drinksVendingMachine.getHasCoinNoDrinks());
        System.out.println("投幣成功,請按按鈕!");
    }

    @Override
    public void pressButton(DrinksVendingMachine drinksVendingMachine) {
        System.out.println("飲料已空,請聯繫工作人員添加飲料!");
    }

    @Override
    public void addDrinks(DrinksVendingMachine drinksVendingMachine, int num) {
        drinksVendingMachine.setSize(drinksVendingMachine.getSize() + num);
        drinksVendingMachine.changeState(drinksVendingMachine.getNoCoinHasDrinks());
        System.out.println("添加飲料成功!");
    }
}

測試類(StatePatternTest):

public class StatePatternTest {
    public static void main(String[] args) {
        // 1.初始化狀態:NoCoinNoDrinks
        DrinksVendingMachine drinksVendingMachine = new DrinksVendingMachine();
        // 無效行為,不會改變狀態
        drinksVendingMachine.pressButton();
        // 2.狀態:NoCoinNoDrinks -> HasCoinNoDrinks
        drinksVendingMachine.insertCoin();
        drinksVendingMachine.insertCoin();
        // 3.狀態:HasCoinNoDrinks -> NoCoinNoDrinks
        drinksVendingMachine.pressButton();
        // 4.狀態:NoCoinNoDrinks -> NoCoinHasDrinks
        drinksVendingMachine.addDrinks();
        // 5、狀態:(NoCoinHasDrinks <-> HasCoinAndDrinks){多次} -> NoCoinNoDrinks
        while (drinksVendingMachine.getSize() > 0) {
            drinksVendingMachine.insertCoin();
            drinksVendingMachine.insertCoin();
            drinksVendingMachine.pressButton();
            drinksVendingMachine.pressButton();
        }
    }
}

測試結果截圖:
狀態模式測試結果圖

使用場景

當遇到下面任意一種場景都可以使用狀態模式:

  • 一個對象的行為由它的狀態決定,而且它必須在運行時根據自身狀態改變它的行為。如示例中,自動售賣機狀態可以在運行時從有硬幣有飲料變成沒有硬幣沒有飲料
  • 程式碼中包含大量與對象狀態有關的條件語句,這些條件語句的出現,會導致程式碼的可維護性和靈活性變差,不能方便地增加和刪除狀態,使客戶類與類庫之間的耦合增強。在這些條件語句中包含了對象的行為,而且這些條件對應於對象的各種狀態。如概述裡面提供,TCP連接通過Switch語句來判斷執行什麼行為,不好維護。

與策略模式對比

如果狀態模式提供外部介面,使得其他對象可以知道Context包含的狀態,且能改變Context的狀態,狀態模式就變成了一個策略模式,每種狀態就是一種策略。

但是狀態模式的狀態一般是不變的,策略模式是可以隨意添加策略的,而不影響其他策略。

優點

  • 符合設計模式六大原則中的單一職責原則
  • 封裝與狀態的行為到一個類中,降低耦合,增加可維護性

缺點

  • 增加系統類個數,增加系統複雜性

總結

狀態模式是一個行為型設計模式,角色有三個:上下文角色、抽象狀態類、具體狀態類。

上下文角色定義客戶端使用的介面方法,維護一個具體狀態類實例,定義當前狀態。抽象狀態類定義封裝了與上下文角色相關的行為的一組介面,具體狀態類則實現不同的狀態對應的相同行為的不同表現。

狀態模式符合單一職責規則,降低耦合,提高可維護性。

參考

[1]//springframework.guru/gang-of-four-design-patterns/state-pattern/

[2]//refactoring.guru/design-patterns/state

[3]//en.wikipedia.org/wiki/State_pattern

end 2020年11月22日16:14:18