Head First 設計模式 —— 12. 狀態 (State) 模式

思考題

public class GumballMachine {
    final static int SOLD_OUT = 0;
    final static int NO_QUARTER = 1;
    final static int HAS_QUARTER = 2;
    final static SOLD = 3;
    
    int state = SOLD_OUT;
    int count = 0;
    
    public GumballMachine(int count) {
        this.count = count;
        if(count > 0) {
            state = NO_QUARTER;
        }
    }
    
    public void insertQuarter() {
        if(state == HAS_QUARTER) {
            // print error message
        } else if(state == NO_QUARTER) {
            state = HAS_QUARTER;
            // print success message
        } else if(state == SOLD_OUT) {
            // print error message
        } else if(state == SOLD) {
            // print error message
        }
    }
    
    public void ejectQuarter() {
        // ...
    }
    
    public void turnCrank() {
        // ...
    }
    
    public void dispense() {
        // ...
    }
}

下列哪一項描述了我們實現的狀態?(多選) P396

  • [x] A. 這份程式碼確實沒有遵守開放-關閉原則
    • 當新增狀態時,必須在所有方法中加上對新狀態的條件判斷,所以沒有遵守開放-關閉原則
  • [ ] B. 這份程式碼會讓 Fortran 程式設計師感到驕傲
    • 不知道為什麼
    • 【答案有此選項】
  • [x] C. 這個設計其實不符合面向對象
    • 這個設計是面向過程的,所有的操作都通過條件判斷,沒有封裝狀態
  • [x] D. 狀態轉換被埋藏在條件語句中,所以並不明顯
    • 狀態轉換是在行為方法內的條件語句中,要找到狀態轉換前後的狀態需要閱讀行為方法內的全部程式碼,難以快速了解某種狀態會如何轉換
  • [x] E. 我們還沒有把會改變的那部分包起來
    • 狀態和行為都會改變,但行為比較固定且與實際相對應,狀態是抽象出來的,所以應該將狀態封裝起來
  • [x] F. 未來加入的程式碼很有可能會導致 bug
    • 由於所有行為方法內都有不同狀態的條件判斷,所以在任何狀態發生變化時,都要對所有行為方法進行修改進行處理,很容易遺忘對某行為方法的修改

思考題

public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    
    State state = soldOutState;
    int count = 0;
    
    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        this.count = numberGumballs;
        if (numberGumballs > 0) {
            state = noQuarterState;
        }
    }
    
    public void insertQuarter() {
        state.insertQuarter();
    }
    
    public void ejectQuarter() {
        state.ejectQuarter();
    }
    
    public void turnCrank() {
        state.turnCrank();
        state.dispense();
    }
    
    void setState(State state) {
        this.state = state;
    }
    
    void releaseBall() {
        // print success message
        if(count != 0) {
            count = count - 1;
        }
    }
}

讓我們來回頭看看糖果機的實現。如果曲柄被轉動了,但是沒有成功(比方說顧客沒有先投入25分錢的硬幣)。在這種情況下,儘管沒有必要,但我們還是會調用 dispense() 方法。對於這個問題你要如何修改呢? P405

  • State 介面的 turnCrank() 方法增加返回值以表示是否正確處理,只有在正確處理時,才調用 dispense() 方法

狀態模式

允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了它的類。 P410
狀態模式

特點

  • 將每個狀態行為局部化到自己的狀態類中 P407
  • 讓每個狀態「對修改更換比」,讓上下文「對擴展開放」,因為可以加入新的狀態類 P407

缺點

  • 通常會導致設計中類的數目大量增加 P423

狀態模式和策略模式的區別

狀態模式

  • 將一群行為封裝在狀態對象中,上下文的行為隨時可委託到那些狀態對象中的一個。當前狀態會在狀態對象集合中遊走改變,以反應出上下文內部的狀態,因此,上下文的行為也會跟著改變。但時客戶對於上下文的狀態對象了解不多,甚至根本是渾然不知 P411
  • 是不用在上下文中放置許多條件判斷的替代方案,通過將行為包裝進狀態對象中,在上下文內簡單地改變狀態對象來改變上下文的行為 P411

策略模式

  • 客戶通常主動指定上下文所要組合的策略對象 P411
  • 是除繼承之外的一種彈性替代方案,可以通過組合不同的對象來改變行為 P411

思考題

應該由狀態類還是上下文決定狀態轉換的流向? P412

  • 當狀態轉換是固定的時候,適合放在上下文中(此時狀態類之間不相互依賴,是對狀態類修改封閉) P412
  • 當狀態轉換是更動態的時候,通常就會放在狀態類中(此時狀態類之間產生了依賴,是對上下文修改封閉) P412

思考題

我們需要你為糖果機寫一個重填糖果的 refill() 方法。這個方法需要一個變數——所要填入機器中的糖果數目。它應該能更新糖果機內的糖果數目,並重設機器的狀態。 P421

void refill(int num) {
    this.count += num;
    if(state instanceof SoldOutState) {
        state = noQuarterState;
    }
}

思考題

配對下列模式和描述: P422
狀態模式:封裝基於狀態的行為,並將行為委託到當前狀態
策略模式:將可以互換的行為封裝起來,然後使用委託的方法,決定使用哪一個行為
模板方法模式:由子類決定如何實現演算法中的某些步驟

本文首發於公眾號:滿賦諸機(點擊查看原文) 開源在 GitHub :reading-notes/head-first-design-patterns