設計之禪——狀態模式

前言

之前我寫過一篇策略模式的文章,講的是如何靈活地改變對象的行爲,今天要講的模式和策略模式非常像,它也是讓你設計出如何靈活改變對象行爲的一個模式,與策略模式不同的是它是根據自身狀態而自行地改變行爲,它就是狀態模式。

詳解

普通實現

首先我們來分析一個實例:現在的遊戲基本都有自動打怪做任務的功能,如果讓你實現這個功能你會怎麼做呢?
本篇講解的是狀態模式,當然首先應該分析其應有狀態和行爲,下面是我畫的一個簡單的狀態圖:
state
橢圓代表的是所處狀態,指引線代表執行的行爲。一開始角色處於初始狀態,什麼也不做,當玩家開啓自動任務功能時,角色就自動的接受任務,當接到殺怪的任務後,發現周圍沒有怪,就把“初始狀態”改爲“未發現怪物”狀態並開始四處遊走尋找怪物,走啊走,走啊走,發現了目標怪物就將狀態修改爲“發現怪物”,然後開始攻擊打怪,直到殺怪數量達到任務指定數量後,就停止打怪並將狀態修改爲“任務達成”狀態,最後回到接任務那裏提交任務,角色狀態又重置爲初始狀態(這裏只是爲了方便理解該模式,不要太糾結功能細節)。不難發現,在該實例中,我們包含了四個狀態和四個行爲,任何一個行爲是隨時都有可能進行的,但是其表現結果卻會因爲狀態的不同而有不一樣的結果,按照我們面向過程的編程方式也是非常容易實現的:

public class Character {

    // 停止
    private final static int STOP = 0;
    // 附近有怪
    private final static int HASMONSTER = 1;
    // 附近沒有怪
    private final static int NOMONSTER = 2;
    // 任務條件達成
    private final static int MISSIONCLEAR = 4;

    // 當前狀態
    private int state = STOP;
    // 還需殺怪數量
    private int count = 0;

    public void accept(int count) {
        if (state == STOP) {
            this.count = count;
            state = NOMONSTER;
            // move to find the monster
            move();
        } else if (state == HASMONSTER) {
            System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
        } else if (state == NOMONSTER) {
            System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
        } else if (state == MISSIONCLEAR) {
            System.out.println("Sorry!You must submit the current task!");
        }
    }

    private void move() {
        if (state == STOP) {
            System.out.println("Moving....");
            state = HASMONSTER;
            attack();
        } else if (state == HASMONSTER) {
            System.out.println("Moving to find new monster");
            attack();
        } else if (state == NOMONSTER) {
            System.out.println("Moving to find monster");
            state = HASMONSTER;
            attack();
        } else if (state == MISSIONCLEAR) {
            System.out.println("Moving to submit");
            submit();
        }
    }

    private void attack() {
        
    }

    private void submit() {
        
    }

}

最後兩個方法我沒有給出具體實現,相信難不倒你,當全部實現后角色就能自動接任務打怪了:

Accept the task.Need to kill monster:10
Moving to find monster
need to kill:9
need to kill:8
need to kill:7
need to kill:6
need to kill:5
need to kill:4
need to kill:3
need to kill:2
need to kill:1
need to kill:0
Moving to submit
Congratulations on completing the task!

不過,功能雖然實現了,但是這樣寫代碼冗長不說,還非常難於理解維護,想象一下這裏只假設了4種狀態,當如果有非常多的狀態,那就是滿篇的if else了,而且如果未來需要增加新的狀態,那麼當前的實現無疑是違反了open-close原則的,我們沒有封裝變化的那部分。那應該如何做呢?這就需要我們的狀態模式了。

使用狀態模式重構代碼

往下看之前,不妨先仔細思考一下,既然該功能中狀態是會隨時改變的,而行爲又會受到狀態的影響,那何不將狀態抽離出來成爲一個體系呢?比如定義一個狀態接口(爲什麼這裏需要定義所有的行爲方法呢?):

public interface State {

    void accept(int count);

    void move();

    void attack();

    void submit();

}

那麼角色類中就可以如下定義了:

public class Character {

    // 當前狀態
    private State current = new StopState(this);
    // 所需殺怪數量
    private int count = 0;

    public void accept(int count) {
        // 注意這裏不能直接將值賦給成員變量
        current.accept(count);
    }

    public void move() {
        current.move();
    }

    public void attack() {
        current.attack();
    }

    public void submit() {
        current.submit();
    }

    public void killOne() {
        this.count--;
    }

    public void setCurrent(State current) {
        this.current = current;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public State getCurrent() {
        return current;
    }

    public int getCount() {
        return count;
    }
}

相比較之前,新的類只保留了當前狀態,並增加了getter和setter方法,而角色的行爲則全都委託給了具體的狀態類來實現,那具體的狀態類應該如何實現呢?

// 初始狀態
public class StopState implements State {
    private Character c;

    public StopState(Character c) {
        this.c = c;
    }

    @Override
    public void accept(int count) {
        c.setCount(count);
        c.setCurrent(new NoMonsterState(c));
        c.move();
    }

    @Override
    public void move() {
        System.out.println("Moving....");
        c.setCurrent(new HasMonsterState(c));
        c.attack();
    }

    @Override
    public void attack() {
        System.out.println("Sorry!You must accept the task!");
    }

    @Override
    public void submit() {
        System.out.println("You don't have task to submit!");
    }
}

// 附近沒有怪物
public class NoMonsterState implements State {
    private Character c;

    public NoMonsterState(Character c) {
        this.c = c;
    }

    @Override
    public void accept(int count) {
        System.out.println("Sorry!You are doing the task,so you can't accept the new task!!");
    }

    @Override
    public void move() {
        System.out.println("Moving to find monster!");
        c.setCurrent(new HasMonsterState(c));
        c.attack();
    }

    @Override
    public void attack() {
        c.move();
    }

    @Override
    public void submit() {
        System.out.println("Please complete the task!");
    }
}

這裏我也只給出了兩個實現類,其它的相信你能很容實現它們。通過狀態模式重構後,代碼清晰了很多,沒有滿屏的if else,角色也能夠根據當前所處的狀態表現出相應的行爲,同時如果需要增加新的狀態時,只需要實現State接口就行了,看起來相當完美。但是,沒有什麼模式是完美的,使用狀態模式的缺點我們很容易發現,原來一個類就能解決的,現在裂變爲了四個類,系統結構複雜了很多,但這樣的犧牲是非常有必要和值得的。

思考

剛剛我們已經實現了狀態模式,但是還有個細節問題不知你注意到了沒有?比如:

    public void move() {
        System.out.println("Moving....");
        c.setCurrent(new HasMonsterState(c));
        c.attack();
    }

在我的實現中,都是由狀態來控制下一個狀態是什麼,這樣狀態之間就形成了強依賴,當然你可以將狀態轉換放到context(Character)類中,不過這種更適合狀態轉換是固定的,而在我們這個例子中,狀態的變更是動態的。還需要注意的是我這裏調用 c.setCurrent(new HasMonsterState©)時,狀態是硬編碼傳入的,這樣當系統進化時可能就需要更改此處的代碼,如何解決這種情況呢?在《Head First設計模式》書中有提到,在Context類中定義所有的狀態並提供getter方法,這裏則調用getter獲取後再傳入,但區別只在於是context類還是狀態類對修改封閉:

c.setCurrent(c.getHasMonsterState());

對此我有點疑問,即使使用getter獲取,那未來系統進化導致狀態的改變後難道不需要修改getter方法名麼?

總結

狀態模式允許對象在內部狀態改變時改變它的行爲,如果需要在多個對象間共享狀態,那麼只需要定義靜態域即可。
狀態模式與策略模式具有相同的類圖,但它們本質的意圖是不同的。前者是封裝基於狀態的行爲,並將行爲委託到當前的狀態,用戶不需要知道有哪些狀態;而後者是將可以互換的行爲封裝起來,然後使用委託,由客戶決定需要使用哪種行爲,客戶需要知道所有的行爲類。

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