跳到主要內容

狀態模式 (State Pattern)

        如果今天你要設計一台如下圖的糖果機,你會怎麼設計呢?



        有上過資訊相關課程的人,應該不難從上圖聯想到狀態圖,上圖中每個圓圈都是一個狀態,而每個箭頭就代表狀態的轉換。有了這個概念後,把它轉成程式就不難了:

public class CandyMachine {

    // 以下四個值表示糖果機會用到的狀態
    final static int SOLD_OUT = 0;
    final static int NO_COIN = 1;
    final static int HAS_COIN = 2;
    final static int SOLD = 3;

    // 需要有一個變數來記錄目前的狀態
    // 初始設為賣完, 因為一開始機器裡沒糖果
    int mState = SOLD_OUT;

    // 也要有另一個變數記錄目前剩多少顆糖果
    int mCount = 0;

    public CandyMachine(int count)
    {
        mCount = count;

        // 機器內有糖果的話就跳到沒投錢的狀態,
        // 表示糖果機在等人投錢
        if(mCount > 0)
        {
            mState = NO_COIN;
        }
    }

    // 當投錢時會執行這個方法
    public void insertCoin()
    {
        if(mState == HAS_COIN)
        {
            System.out.println("You can't insert another coin");
        }
        else if(mState == NO_COIN)
        {
            mState == HAS_COIN;
            System.out.println("You inserted a coin");
        }
        else if(mState == SOLD_OUT)
        {
            System.out.println("It's sold out")
        }
        else if(mState == SOLD)
        {
            System.out.println("Please wait for a candy.")
        }
    }

    // 當使用者要退錢時
    public void ejectCoin()
    {
        // 省略實作細節
    }

    // 當使用者轉動曲柄時
    public void turnCrank()
    {
        // 省略實作細節
    }

    // 機器要給使用者糖果時執行的方法
    public void dispense()
    {
        // 省略實作細節
    }
}
        這樣的寫法看起來好像不錯,但要加新功能時就完了。假如現在糖果機想加一個新功能,是有 10% 的機率會給使用者兩顆糖果,上述的程式就要全部改寫了。除了要加一個新的狀態 WINNER = 4,而且所有方法都還要判斷 if(mState == WINNER),這樣就不遵守「對修改關閉,對擴充開放」的設計原則。所以上述的程式勢必要重構。新的架構可以由下列想法開始:
  1. 定義一個狀態介面,在此介面內,糖果機的每個動作都有一個對應的方法。
  2. 為糖果機中的每個狀態實作狀態類別,這些類別會負責在對應的狀態下進行機器的行為。
  3. 改寫舊的程式碼,將要做的事情都轉到狀態類別裡。
       其實照著上面的步驟,不但能遵守設計守則,也實作了這次要說的狀態模式,先來看程式碼吧:
// State 是所有狀態類別都要實作的介面,
// State 也定義了所有次類別需要實作的方法
public class NoCoinState implements State {

    private CandyMachine mCandyMachine;

    public NoCoinState(CandyMachine machine)
    {
        // 保存糖果機的參考
        mCandyMachine = machine;
    }

    @Override
    public void insertCoin()
    {
        System.out.println("You inserted a coin");

        // 切換狀態到已投錢狀態
        mCandyMachine.setState(mCandyMachine.getHasCoinState());
    }

    @Override
    public void ejectCoin()
    {
        // 沒投錢就不能退錢
        System.out.pritnln("You haven't inserted a coin");
    }

    @Override
    public void turnCrank()
    {
        // 沒投錢就不能轉曲柄
        System.out.println("No coin no candy");
    }

    @Override
    public void dispanse()
    {
        // 沒投錢當然沒糖果
        System.out.println("Please insert coin first");
    }
}

public class CandyMachine {

    // 這是糖果機會用到的所有狀態
    private State mSoldOutState;
    private State mNoCoinState;
    private State mHasCoinState;
    private State mSoldState;

    private State mState = mSoldOutState;
    private int mCount = 0;

    public CandyMachine(int numOfCandy)
    {
        // 建立所有狀態實體
        mSoldOutState = new mSoldOutState(this);
        mNoCoinState = new mNoCoinState(this);
        mHasCoinState = new mHasCoinState(this);
        mSoldState = new mSoldState(this);
        mCount = numOfCandy;

        if(mCount > 0)
        {
            mState = mNoCoinState;
        }
    }

    // 以下的動作變得很容易實作
    // 我們只是將動作轉介到目前的狀態
    public void insertCoin()
    {
        mState.insertCoin();
    }

    public void ejectCoin();
    {
        mState.ejectCoin();
    }

    public void turnCrank()
    {
        mState.turnCrank();
        mState.dispense();
    }

    public void setState(State state)
    {
        mState = state;
    }

    public void releaseCandy()
    {
        System.out.println("A candy comes out!");
        if(mCount != 0)
        {
            mCount = mCount - 1;
        }
    }

    // 其他方法像是 getHasCoinState() 等
}

        其他狀態的實作也大同小異,都是在特定狀態做對應的事情。由上面的程式碼,我們可以知道:
  1. 將每個狀態的行為定義在自己的類別裡。
  2. 將容易產生問題的 if 敘述拿掉,以方便日後維護。
  3. 讓每個狀態「對修改關閉」。讓糖果機「對擴充開放」,因為容易加入新的狀態。
  4. 較舊程式碼更容易閱讀理解。
        修改後的程式碼已經是狀態模式的實作了,我們就來看看此模式的正式定義吧:

狀態模式允許物件隨著內在的狀態改變而改變行為,好像物件的類別改變了一樣。
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

        定義的前半部可以從程式碼來理解。當糖果機在 NoCoinState 或是 HasCoinState 兩種不同狀態時,你投入硬幣,就會得到不同的行為 (機器接受硬幣,以及機器拒絕硬幣)。而後半部定義「好像物件改變了」是什麼意思?這要從客戶的觀點來看:如果說使用的物件能夠完全改變它的行為,客戶會覺得這個物件是從別的類別所實體化出來的。但實際上我們是用合成的方式,將動作導到其他的狀態物件,造成行為改變的假象。


        從類別圖來看,Context (工作環境) 是一個類別,它可以擁有一些內部的狀態。在我們的例子中,糖果機就是這個 Context。State定義了一個所有具象狀態共同的介面;任何狀態都要實作這個共同介面,這樣一來,狀態之間可以互相替換。state.handle() 就是 Context 有呼叫到 request() 時,就會轉介到真正的狀態進行處理。

        不知道看到這裡有沒有人發現,狀態模式的類別圖跟策略模式的類別圖竟然一模一樣!雖然類別圖一樣,但這兩個模式的差別,是在於它們的「意圖」。以狀態模式而言,我們將一群行為封裝在狀態物件中,Context 的行為隨時能委派到其中一個狀態物件中,而狀態改變,Context 的行為也跟著改變,但是 Context 的客戶對狀態物件所知不多,甚至根本不知道有狀態物件。

        而對策略模式而言,客戶通常主動指定 Context 所要合成的策略物件為何者。策略模式雖然有彈性,能在執行時期改變策略,但對於某個 Context 物件來說,經常只會有一個最適當的策略物件。比如有些鴨子被設定成一般的飛行行為,而有些鴨子(如橡皮鴨)卻要另外使用不同的飛行行為。

        一般來說,要把策略模式想成是除了繼承之外,更有彈性的替代方案,因為可以組合不同的物件改變行為。而把狀態模式想成是不用在 Context 中放置許多條件判斷的替代方案,因為可以在 Context 內簡單改變狀態物件,來改變 Context 的行為。

參考資料:

        深入淺出設計模式(Head First Design Patterns)

留言

  1. 依你的類別圖來看,State 應該不會知道 Context,但是你的程式實作卻違反了這個類別圖,State 去瞭解了 Context(CandyMachine)。我認為應該要去掉這層相依關係比較好吧

    回覆刪除
    回覆
    1. @洪彥彬 感謝你的留言。文中的類別圖是照著「深入淺出設計模式」這本書畫出來的。我個人認為書中所要強調的觀念是 Context 「知道」State (程式上實作就是 Context 類別有「有」一個 State 實體)。User 要透過 Context 做的事情,都會委派給 State 去處理。

      你提的問題跟如何「更新」State 比較有關係。文中的範例只是一種做法而已。你要用其他符合類別圖的方法來達成更新 Context 裡的 State,如 Callback 或是 State 的 method 都帶入 Context 當參數都是可以的,看你的程式如何設計。雖然程式碼跟類別圖有一點不同,但基本觀念是不變的。以上是個人的一點淺見

      刪除
  2. 這個 NoCoinState 類別本身就違反了 [對修改關閉,對擴充開放] 原則了吧,對於任何新的狀態都要修改這個類別才能有新功能。

    回覆刪除

張貼留言

這個網誌中的熱門文章

整理設計模式

        依據 GOF 的書,可以將經典的設計模式分為以下三類:生成、行為、結構。 生成模式 :牽涉到 將物件實體化 。這類模式都提供一個方法,將客戶從所需要實體化的物件中鬆綁出來。 獨體模式 (Singleton Pattern) 工廠方法模式 (Factory Method Pattern) 抽象工廠模式 (Abstract Factory Pattern) 建立者模式 (Builder Pattern) 原型模式 (Prototype Pattern) 結構模式 :讓你 合成類別或物件到大型的結構 。 裝飾者模式 (Decorator Pattern) 轉接器模式 (Adapter Pattern) 表象模式 (Facade Pattern) 合成模式 (Composite Pattern) 代理人模式 (Proxy Pattern) 橋接模式 (Bridge Pattern) 享元模式 (Flyweight Pattern) 行為模式 :模述 類別和物件如何互動 ,以及 各自的責任 。 策略模式 (Strategy Pattern) 觀察者模式 (Observer Pattern) 命令模式 (Command Pattern) 樣板方法模式 (Template Method Pattern) 反覆器模式 (Iterator Pattern) 狀態模式 (State Pattern) 責任鏈模式 (Chain of Responsibility Pattern) 解譯器模式 (Interpreter Pattern) 中介者模式 (Mediator Pattern) 備忘錄模式 (Memento Pattern) 訪問者模式 (Visitor Pattern)         有人可能會覺得裝飾者模式明明有替物件增加行為,為什麼不算是行為模式呢?我們可以從上面的結構模式得知, 結構模式用來描述類別或物件如何被合成,以建立新的結構或功能 。裝飾者模式允許你透過「 將某物件包裝進另一個物件的方式 」,將物件合成以提供新功能,因此焦點應該放在「 動態合成物件,以取得某功能 」,而不是物件之間的溝通。         設入淺出設計模式也有提到一些使用設計模式的

訪問者模式 (Visitor Pattern)

        假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 Java 中最直接的做法就是使用 instanceof 關鍵字來判斷,如以下的簡單範例: public interface CarComponent { public void printMessage(); } public class Wheel implements CarComponent { @Override public void printMessage() { System.out.println("This is a wheel"); } // 這是 Wheel 跟 Engine 不同的方法 public void doWheel() { System.out.println("Checking wheel..."); } } public class Engine implements CarComponent { @Override public void printMessage() { System.out.println("This is a engine"); } // 這是 Wheel 跟 Engine 不同的方法 public void doEngine() { System.out.println("Testing this engine..."); } } public class Car { private List mComponents; public Car() { mComponents = new ArrayList<carcomponent>(); } // 有些時候我們還是需要針對不同類別去做不同的事情 public void setComponent(CarCompon

裝飾者模式 (Decorator Pattern)

        假如你有一間飲料店, 目前只有賣幾種咖啡。因為生意很好, 因此想更換菜單…         以下是目前菜單的類別圖:         簡單說明此類別圖, cost() 是抽象的, 子類別要實作自己的 cost() 來告知飲料的價格。         買咖啡時, 也可要求要加料, 例如牛奶(Milk)、摩卡(Mocha,就是巧克力口味)。這樣的新類別要如何設計呢 ? 看起來是不能直接新增所需的子類別, 例如 EspressoWithMilk, EspressoWithMilkAndMocha, DarkRoastWithMilk, DarkRoastWithMilkAndMocha… 這樣加下去, 日後飲料跟配料越來越多時, 類別也就越多, 這實在不是個好設計。         換個方式設計呢, 在 Beverage 裡面加入所有的配料如何 ? 這樣好像也不太好, 未來要是配料有更動, Beverage 程式碼就要重寫, 而未來要是有新口味的飲料時, 有些配料就不太合理 ( 薑茶加摩卡 ? ), 更麻煩的是, 無法應付機車的客人 (例如要加 3 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來正式介紹裝飾者模式的定義:  裝飾者模式動態地將責任加諸於物件上。若要擴充功能,裝飾者模提供了比繼承更有彈性的選擇 Attach additional responsibilities to an object dynamically. Decorators provide a flexible alt