天天看點

設計之禅——狀态模式前言詳解總結

前言

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

詳解

普通實作

首先我們來分析一個執行個體:現在的遊戲基本都有自動打怪做任務的功能,如果讓你實作這個功能你會怎麼做呢?

本篇講解的是狀态模式,當然首先應該分析其應有狀态和行為,下面是我畫的一個簡單的狀态圖:

設計之禅——狀态模式前言詳解總結

橢圓代表的是所處狀态,指引線代表執行的行為。一開始角色處于初始狀态,什麼也不做,當玩家開啟自動任務功能時,角色就自動的接受任務,當接到殺怪的任務後,發現周圍沒有怪,就把“初始狀态”改為“未發現怪物”狀态并開始四處遊走尋找怪物,走啊走,走啊走,發現了目标怪物就将狀态修改為“發現怪物”,然後開始攻擊打怪,直到殺怪數量達到任務指定數量後,就停止打怪并将狀态修改為“任務達成”狀态,最後回到接任務那裡送出任務,角色狀态又重置為初始狀态(這裡隻是為了友善了解該模式,不要太糾結功能細節)。不難發現,在該執行個體中,我們包含了四個狀态和四個行為,任何一個行為是随時都有可能進行的,但是其表現結果卻會因為狀态的不同而有不一樣的結果,按照我們面向過程的程式設計方式也是非常容易實作的:

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方法名麼?

總結

狀态模式允許對象在内部狀态改變時改變它的行為,如果需要在多個對象間共享狀态,那麼隻需要定義靜态域即可。

狀态模式與政策模式具有相同的類圖,但它們本質的意圖是不同的。前者是封裝基于狀态的行為,并将行為委托到目前的狀态,使用者不需要知道有哪些狀态;而後者是将可以互換的行為封裝起來,然後使用委托,由客戶決定需要使用哪種行為,客戶需要知道所有的行為類。