【一起學系列】之狀態模式:你聽過“流程”模式嗎?

意圖

允許一個對象在其內部狀態改變時改變它的行爲

說人話:允許對象在改變自身狀態時候,更改綁定的特定方法

狀態模式的誕生

產品】:Hello,開發小哥,我們需要開發一款 娃娃機,你可以提前想想怎麼設計它啦。

開發】:娃娃機?我想想奧,它需要投幣,用戶移動,確認抓取,結束這幾個動作,好像很好做欸,用一個變量維護它當前的階段,然後寫四個 if 語句就好啦。

BOSS】:你準備用一個主方法,四個子方法配合 if 語句外加一個狀態變量去做嗎?

// 僞代碼
public void handle() {
	if (flag == A) {
		a();
	}
	
	if (flag == B) {
		b();
	}
}

開發】:對啊,老大,你真是我肚子裏的蛔蟲!

BOSS】:蛔你個頭,這樣做 大錯特錯! ,你難道想對 投幣口,按鈕,搖桿都綁定同一個方法嗎?

開發】:對哦,它們應該是 不同的方法,同時暴露給用戶,我再思考思考

HeadFirst 核心代碼

定義狀態接口,同時封裝變化,利用default關鍵字封裝默認方法

public interface State {

    /** 投幣 **/
    default void giveMoney() {
        System.out.println("無法投幣");
    }

    /** 移動滑桿 **/
    default void move() {
        System.out.println("無法移動滑桿");
    }

    /** 抓取 **/
    default void grab() {
        System.out.println("無法抓取");
    }

    void changeState();
}

投幣狀態 狀態的其中之一

public class MoneyState implements State{

    Context context;

    public MoneyState(Context context) {
        this.context = context;
    }

    @Override
    public void giveMoney() {
        System.out.println("已投幣!");
        changeState();
    }

    @Override
    public void changeState() {
        context.setExecute(new MoveState(context));
    }
}

爲了儘量減少代碼,只展示了其中一種狀態,我們可以看到在 MoneyState 狀態類執行所屬的業務方法時,更改了上下文持有的狀態類,這就產生了 狀態的變更 ,同時上下文更加清晰,即:我只用考慮我下一個狀態是什麼

狀態模式的設計思路:

  • Context 上下文環境,持有狀態
  • State 狀態頂層接口
  • ConcreteState 具體的狀態

簡單來說,

  1. 必須清晰的認識到共有多少種不同的狀態,並通過接口定義其核心方法,封裝變化
  2. 狀態類持有 Context 上下文,在覈心方法處理後更改其狀態

如果看着有點模棱兩可,建議看完本文後,訪問專題設計模式開源項目,裏面有具體的代碼示例,鏈接在最下面

狀態模式的關鍵

  • 明確所有可能發生的狀態,及其轉換關係
  • 明確狀態模式中的各個狀態是有可能同時暴露給用戶的

就好像娃娃機運作的多種狀態, 投幣,移動搖桿,按下確認按鈕等等可能不按先後順序觸發

整一個 “流程” 模式

每個狀態的方法名都一樣會如何?

上文中我們大概知道了狀態模式的特點,把狀態封裝成類,在調用狀態-核心方法時候更改其狀態本身,此時考慮的多種狀態方法名可能各不相同,假設我們都起一樣的名字會如何?

我們會首先遇到一個問題,我們無法得知它需要調用幾次方法(因爲可能有重複性 A - B 的情況),但如果無限循環,在適當的地方控制其結束點,和是否繼續執行的標識,好像就可以解決了。

來一個流程案例

簡單描述下即:開始處理訂單

  • 正常則進入成功狀態,入庫,結束執行
  • 失敗則進入失敗狀態,檢測是否重新執行,扭轉狀態爲處理訂單

上代碼

Context 上下文

public class Context {

    /**
     * 最大執行次數
     */
    public static final Integer FAIL_NUM = 3;

    /***
     * 失敗次數
     */
    private int failNum;

    /**
     * 是否繼續執行的標識
     */
    private boolean isAbandon;

    /***
     * 當前狀態
     */
    private StateInterface stateInterface;

    public Context() {
        this.stateInterface = new HandleOrder();
        this.failNum = 1;
        this.isAbandon = false;
    }

    /***
     * 處理方法
     */
    public void handle () {
        stateInterface.doAction(this);
    }
    
    // 省略無用代碼...
}

處理訂單狀態

public class HandleOrder implements StateInterface {

    @Override
    public void doAction(Context context) {
        printCurrentState();

        // do somethings
        int num = (int) (Math.random() * 11);
        if (num >= 8) {
            System.out.println("處理訂單完成, 進入成功狀態...");
            context.setStateInterface(new SuccessOrder());
        } else {
            System.out.println("處理訂單失敗, 進入失敗狀態...");
            context.setStateInterface(new FailOrder());
        }

        CodeUtils.spilt();
    }

    @Override
    public StateEnums getCurrentState() {
        return StateEnums.HANDLE_ORDER;
    }
}

客戶端調用方法

public class App {
    
    public static void main(String[] args) {
        // 模擬從隊列中取任務按流程循環執行
        Context context = new Context();
        while (true) {

            // 校驗是否爲廢棄 | 已完成任務
            if (context.isAbandon()) {
                System.out.println("此條任務不再執行... ");
                break;
            }
            
            context.handle();
        }
    }
}

測試結果輸出:

當前狀態:訂單處理
處理訂單失敗, 進入失敗狀態...
------------------------

當前狀態:處理訂單失敗
訂單處理失敗... 當前執行次數: 1
------------------------

當前狀態:訂單處理
處理訂單失敗, 進入失敗狀態...
------------------------

當前狀態:處理訂單失敗
訂單處理失敗... 當前執行次數: 2
------------------------

當前狀態:訂單處理
處理訂單完成, 進入成功狀態...
------------------------

當前狀態:處理訂單成功
訂單處理完成 -> 進入入庫邏輯...
入庫處理完成
------------------------

此條任務不再執行... 

如果看着有點模棱兩可,建議看完本文後,訪問專題設計模式開源項目,裏面有具體的代碼示例,鏈接在最下面

“流程” 模式適用的場景

在這樣的設計中,與其說是狀態的變更,不如說是 “流程” 的變更更爲貼切,因此它可以作爲諸多後臺任務的解決方案,尤其是面臨很多業務流程場景時,可以極大的提高代碼的可維護性: 我只用考慮和我有關的 “流程”

遵循的設計原則

  • 封裝變化:在父級接口中提供 default 方法,子類實現其對應的狀態方法即可
  • 多用組合,少用繼承:狀態模式經常和策略模式做對比,它們都是利用組合而非繼承增強其變化和能力

什麼場景適合使用狀態模式

  • 一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變其行爲
  • 一個操作中含有龐大的多分支條件語句,且這些分支依賴於該對象的狀態

最後

附上GOF一書中對於狀態模式的UML圖:

相關代碼鏈接

GitHub地址

  • 兼顧了《HeadFirst》以及《GOF》兩本經典書籍中的案例
  • 提供了友好的閱讀指導
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章