1. 程式人生 > 實用技巧 >十、狀態模式(State Pattern)《HeadFirst設計模式》讀書筆記

十、狀態模式(State Pattern)《HeadFirst設計模式》讀書筆記

  假設有一個需求:要求實現一個糖果機的功能,糖果機的功能如下,用一個狀態圖來表示:

  

  上圖描述了糖果機在不同狀態下可能會執行的操作和狀態間的轉換關係,圖中每個圓圈代表一種狀態。

  下面我們按照常規的思路來實現一個糖果機,建立一個糖果機類,用int常量分別代表四種狀態,另外定義int變數state代表當前狀態,定義int變數count代表糖果機當前的糖果數目,還有其它狀態轉換時需要的一些方法。

public class GumballMachine {
    //糖果機的四種狀態
    private static final int SOLD_OUT = 0;
    private
static final int NO_25 = 1; private static final int HAS_25 = 2; private static final int SOLD = 3; //當前狀態,初始值為售罄 private int state = SOLD_OUT; //當前糖果數量 private int count = 0; //構造方法,傳入初始糖果數量 public GumballMachine(int count) { this.count = count; } //投入25的方法,需要判斷當前狀態,且投入成功要轉換到HAS_25的狀態
public void insert25(int state){ if (state == SOLD_OUT) { System.out.println("糖果售罄了,目前不能購買"); return; } if (state == NO_25) { this.state = HAS_25; System.out.println("投幣成功"); return; } if (state == HAS_25) { System.out.println(
"已經投過幣了"); return; } if (state == SOLD) { System.out.println("已經投過幣了,正在準備發放糖果,請稍後再投幣"); } } //同理其它方法省略... }

  可以看到,上面的程式碼並不容易擴充套件,各種if語句,比如需要增加狀態時,每個方法又要加一個判斷,這就需要狀態模式登場了。

  經過之前各種模式的學習,其實不同的設計模式也只是應用場景不同,但其實每個模式的原理都是通過一些類似的手法去實現的。如果我們自己設計一個狀態模式,首先可以想到要將狀態抽象出一個介面,然後由不同的具體狀態去實現狀態介面,實現狀態之間互相轉換的方法。

  首先定義一個介面MyState,在介面中有所有的抽象方法。

public interface MyState {
    void insert25();
    void rollBack25();
    void turnHandle();
    void giveCandy();
}

  接著實現狀態介面,寫出每一個具體的狀態類,這裡以沒有25分錢的狀態為例。

public class No25State implements MyState {
    //具體的狀態內部維護了MyGumballMachine物件,是為了呼叫它的setState方法,和get下一個狀態來改變狀態
    private MyGumballMachine myGumballMachine;

    public No25State(MyGumballMachine myGumballMachine) {
        this.myGumballMachine = myGumballMachine;
    }

    @Override
    public void insert25() {
        //投幣成功,將狀態設定為有25分錢狀態
        myGumballMachine.setState(myGumballMachine.getHas25State());
        System.out.println("投幣成功");
    }

    @Override
    public void rollBack25() {
        System.out.println("請先投幣");
    }

    @Override
    public void turnHandle() {
        System.out.println("請先投幣");
    }

    @Override
    public void giveCandy() {
        System.out.println("請先投幣");
    }
}

  可以看到具體的狀態類內部維護了一個糖果機的變數,這麼做的原因是想更方便的在具體的狀態類中進行狀態的轉變,上面例子中就呼叫了糖果機的getHas25state獲取了下一個要轉變的狀態,並呼叫了setState方法完成了狀態的轉換。下面來具體看一下糖果機的具體實現。

public class MyGumballMachine {

    //所有的狀態
    private MyState no25State;
    private MyState has25State;
    private MyState soldState;
    private MyState soldOutState;

    //構造方法,初始化所有的狀態,如果糖果數>0,將當前狀態設定為no25State
    public MyGumballMachine(int initCount) {
        this.count = initCount;
        this.no25State = new No25State(this);
        this.has25State = new Has25State(this);
        this.soldState = new SoldState(this);
        this.soldOutState = new SoldOutState(this);
        if (initCount > 0) {
            this.state = no25State;
        }
    }

    //當前狀態
    private MyState state;
    //當前糖果數量
    private int count;
    
    //將具體的動作委託給當前的狀態物件去執行
    public void insert25(){
        this.state.insert25();
    }

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

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

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

    //當前狀態的set方法,允許狀態物件將糖果機轉換到不同的狀態
    void setState(MyState state) {
        this.state = state;
    }

    //一些get方法...
    MyState getHas25State() {
        return has25State;
    }
}

  可以看到狀態模式通過定義不同的狀態類免去了複雜的if-else語句,在擴充套件時也可以直接實現狀態介面方便地實現新的功能,同時將具體動作委託給狀態物件去執行,當狀態發生改變時,這些行為也會相應改變。

  狀態模式的定義:允許物件在內部狀態改變時改變它的行為,物件看起來好像給修改了它的類。

  也就是說,在糖果機內部狀態改變了的時候,雖然還是呼叫糖果機相同的方法,但是卻有不同的執行結果,這是因為糖果機委託給了具體的狀態物件去執行。

  下面是狀態模式的類圖,圖中的Context就對應例子中的糖果機,request()方法內部會委託給狀態物件的handle()方法執行。

關於狀態模式的小例子,再做一個小總結:

  1.例子中是在具體的狀態類中去改變Context狀態的,實際上在Context和具體的狀態類中都可以改變狀態,如果狀態改變是固定的情況,一般直接在Context類中去改變狀態;如果狀態改變比較動態,比如需要一些條件判斷,則一般在具體狀態類中去改變,但這樣的缺點就是不同的狀態類之間會產生依賴關係,例子中也是通過呼叫Context的get方法來儘量減小依賴。

  2.客戶類不會去改變Context的具體狀態,因此可以注意到例子中的setState方法是(defalt)而不是public。

  3.例子中的Context類中定義了所有的狀態,方便統一獲取,當有很多Context例項想要共享這些變數時,也可以將狀態指定為是靜態的。

  4.當在具體的狀態中要用到Context中的方法或變數,則要在handle方法中傳入Context的引用,或者像例子中的直接在具體的狀態類中定義Context變數。

關於狀態模式的總結:

  其實狀態模式和一些設計模式也很相似,比如策略模式、命令模式等,只要記住它的使用場景是不同狀態互相轉換,不同狀態下會有不同的行為。但狀態模式也有它的缺點,就是會產生很多具體的狀態類,這就需要在設計時對程式碼的擴充套件性和簡潔性之間進行權衡。