設計模式(十一)一文搞懂明白狀態模式

關於狀態模式的定義,我就直接引用Head First了:狀態模式允許對象在內部狀態改變時改變它的行爲,對象看起來好像修改了它的類。狀態模式的意圖是封裝狀態,將狀態封裝成獨立的類,並將動作委託給當前的狀態對象,讓當前狀態在狀態對象間遊走改變,從而使我們能夠通過組合的方式引用不同的狀態對象來改變類的行爲。

狀態模式

以地鐵站自動售票機爲例(與真實流程會有出入,此處重點在於封裝狀態的思想),用戶可執行如下操作:投幣、退款、購買、出票4個動作,而執行這些動作會因用戶處於不同狀態而具有不同的行爲。如:

1、在未投幣狀態,用戶可選擇投幣並進入已投幣狀態,而不能退款、購票、打印車票;
2、在已投幣狀態,用戶可繼續投幣,或退款回到未投幣狀態,或購票進入購票狀態,但不能直接打印車票。
3、在購票狀態,也可理解成提交訂單狀態,購票成功則自動出票,並回到未投幣狀態。
在這裏插入圖片描述
如果我們不使用狀態模式,直接將流程描述成業務代碼,我們會發現每個動作都會伴隨着大量的 if 判斷,假設我們又擴展一個“非運營時間狀態”呢?甚至多個呢?是不是非常不彈性,不符合開閉原則。

public class TicketMachine {
    static final NoSlotStateType = 0;
    static final SlotStateType = 1;
    static final BuyingTicketStateType = 2;
    /**
     * 投幣的動作。
     * 
     * <p>退款、購票、打印車票的動作就不展示了。
     * 同樣需要判斷當前是何種狀態。
     * 
     * <p>如果我們再增加其他狀態呢?
     * 是不是每個方法都需要重新添加判斷。
     */
    public void toSlot(int money) {
        // 當前是未投幣狀態
        if (curState == NoSlotStateType) 
            curState = SlotState;
        // 當前是已投幣狀態
        else if (curState == SlotStateType) 
            System.out.println("已再次投幣!");
        // 當前是購票狀態
        else if (curState == BuyingTicketStateType) 
            System.out.println("此狀態不支持投幣!");
    }
    ... // 其他方法邏輯
}

而狀態模式就是一種可以當作不需要多條件判斷的替代方案。我們將每個狀態單獨抽離成類,而在每個狀態對象中,各自響應着用戶的動作。下面採用狀態模式實現的自動售票機類

public class TicketMachine {
    /**
     * 這是狀態類的具體實現。
     * 詳情見下{@code NoSlotState}。
     */
    NoSlotState noSlotState; // 未投幣狀態
    SlotState slotState; // 已投幣狀態
    BuyingTicketState btState; // 購票中狀態
    
    TicketState curState; // 當前狀態
    
    /**
     * 在構造中初始化當前狀態爲未投幣狀態。
     */
    public TicketMachine() {
        this.curState = noSlotState;
        noSlotState = new NoSlotState(this);
        slotState = new SlotState(this);
        btState = new BuyingTicketState(this);
    }
    /**
     * 將動作委託給當前的狀態對象處理。
     */
    public void toSlot(int money) {
        curState.toSlot();
    }
    public void refund() {
        curState.refund();
    }
    public void tickets() {
        curState.tickets();
        // 當購買請求成功後,會自動出票
        printTicket()}
    public void printTicket() {
        curState.printTicket();
    }
    /**
     * 當前狀態在狀態對象間遊走改變。
     */
    public void setState(TicketState state) {
        this.curState = state;
    }
    /**
     * 提供了get方法,防止狀態對象間彼此耦合。
     */
    public TicketState getNoSlotState() {
        return noSlotState;
    }
    public TicketState getSlotState() {
        return slotState;
    }
    public TicketState getBuyingState() {
        return btState;
    }
}

這是我們的狀態接口類。之所以使用抽象類,是因爲我們可以將每個狀態內複用的處理邏輯,放在超類中。

public abstract class TicketState {
    /**
     * 這是投幣動作。
     * 
     * <p>我們將用戶的操作抽離出來。
     * 其實狀態模式就是多條件判斷的替代方案。
     * 在每個狀態下,用戶都有可能會執行下面
     * 的動作,在不合規時要提醒用戶。
     */
    abstract void toSlot(int money);
    /**
     * 這是退款動作。
     */
    abstract void refund();
    /**
     * 這是購票動作。
     */
    abstract void tickets();
    /**
     * 這是打印車票動作。
     */
    abstract void printTicket();
    ... // 複用邏輯省略
}

這是我們的未投幣首頁類。可以看到在此類中,處理着在“未投幣狀態”下用戶所有可能會觸發的操作,並隨時更新自動售票機的狀態。

public NoSlotState implements TicketState {
    TicketMachine tm;
    
    public NoSlotState(TicketMachine tm) {
        this.tm = tm;
    }
    /**
     * 在未投幣狀態,執行投幣操作後,變成已投幣狀態。
     */
    @Override public void toSlot(int money) {
        tm.setState(tm.getSlotState());
    }
    /**
     * 在未投幣狀態,不能退款等操作。
     */
    @Override public void refund() {
        System.out.println("您還未投幣!");
    }
    @Override public void tickets() {
        System.out.println("需投幣後操作!");
    }
    @Override public void printTicket() {
        System.out.println("需投幣後操作!");
    }
}

這是我們的已投幣狀態類

public SlotState implements TicketState {
    TicketMachine tm;
    
    public SlotState(TicketMachine tm) {
        this.tm = tm;
    }
    /**
     * 在購買前支持多次投幣。
     */
    @Override public void toSlot(int money) {
        tm.setState(tm.getSlotState());
    }
    /**
     * 購票前可以申請退款。
     */
    @Override public void refund() {
        tm.setState(tm.getNoSlotState());
    }
    /**
     * 如果金額足夠,則發起購票請求。
     */
    @Override public void tickets() {
        if (isEnough)
            tm.setState(tm.getBuyingState());
        else
            System.out.println("金額不夠!");
    }
    @Override public void printTicket() {
        System.out.println("夠票後可操作!");
    }
}

這是我們的購票狀態類

public BuyingTicketState implements TicketState {
    TicketMachine tm;
    
    public BuyingTicketState(TicketMachine tm) {
        this.tm = tm;
    }
    /**
     * 出票中,不支持投幣、退款、重複提交等操作。
     */
    @Override public void toSlot(int money) {
        System.out.println("出票中,不支持投幣!");
    }
    @Override public void refund() {
        System.out.println("出票中,不支持退款!");
    }
    @Override public void tickets() {
        System.out.println("不支持重複提交打印請求");
    }
    /**
     * 出票成功後,切換狀態爲未投票狀態。
     */
    @Override public void printTicket() {
        System.out.println("已出票!");
        tm.setState(tm.getNoSlotState());
    }
}

通過上面代碼我們能發現,我們不僅避免了在售票機類TicketMachine 中大量的 if 判斷,還將“主要的變化”封裝並抽離了出來,讓當前已有的狀態對修改關閉。當然狀態模式也會造成大量的小類,這是爲了彈性而付出的代價,但這絕對是值得的。其實真正重要的是我們暴露給客戶的類數目,而不是我們自己的類數目,我們完全可以將這些額外的狀態類對外隱藏起來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章