「補課」進行時:設計模式(19)——狀態模式


1. 前文匯總

「補課」進行時:設計模式系列

2. LOL 中的狀態

感覺我天天在用 LOL 舉例子,沒辦法,都已經 S11 了,而我依然在玩這個游戲。

LOL 中的英雄有很多狀態,有正常狀態,有吃了偉哥一樣的加速狀態,有被對方套了虛弱的虛弱狀態,還有被對方控制的眩暈狀態。

下面來看下,在 LOL 中,初始的英雄狀態:

public class Hero {
    //正常狀態
    public static final int COMMON = 1;
    //加速狀態
    public static final int SPEED_UP = 2;
    //減速狀態
    public static final int SPEED_DOWN = 3;
    //眩暈狀態
    public static final int SWIM = 4;
    //默認是正常狀態
    private int state = COMMON;
    //跑動線程
    private Thread runThread;
    //設置狀態
    public void setState(int state) {
        this.state = state;
    }
    //停止跑動
    public void stopRun() {
        if (isRunning()) runThread.interrupt();
        System.out.println("--------------停止跑動---------------");
    }
    //開始跑動
    public void startRun() {
        if (isRunning()) {
            return;
        }
        final Hero hero = this;
        runThread = new Thread(new Runnable() {
            public void run() {
                while (!runThread.isInterrupted()) {
                    try {
                        hero.run();
                    } catch (InterruptedException e) {
                        break;
                    }
                }
            }
        });
        System.out.println("--------------開始跑動---------------");
        runThread.start();
    }
    private boolean isRunning(){
        return runThread != null && !runThread.isInterrupted();
    }
    //英雄類開始奔跑
    private void run() throws InterruptedException{
        if (state == SPEED_UP) {
            System.out.println("--------------加速跑動---------------");
            Thread.sleep(2000);//假設加速持續2秒
            state = COMMON;
            System.out.println("------加速狀態結束,變為正常狀態------");
        }else if (state == SPEED_DOWN) {
            System.out.println("--------------減速跑動---------------");
            Thread.sleep(2000);//假設減速持續2秒
            state = COMMON;
            System.out.println("------減速狀態結束,變為正常狀態------");
        }else if (state == SWIM) {
            System.out.println("--------------不能跑動---------------");
            Thread.sleep(1000);//假設眩暈持續2秒
            state = COMMON;
            System.out.println("------眩暈狀態結束,變為正常狀態------");
        }else {
            //正常跑動則不打印內容
        }
    }
}

場景類:

public class Client {
    public static void main(String[] args) throws InterruptedException {
        Hero hero = new Hero();
        hero.startRun();
        hero.setState(Hero.SPEED_UP);
        Thread.sleep(2000);
        hero.setState(Hero.SPEED_DOWN);
        Thread.sleep(2000);
        hero.setState(Hero.SWIM);
        Thread.sleep(2000);
        hero.stopRun();
    }
}

可以看到,我們的英雄在跑動過程中隨着狀態的改變,我們的英雄會以不同的狀態進行跑動。

但是問題也隨之而來,我們的英雄類當中有明顯的 if else 結構,這並不是我們希望看到的,接下來,我們看下狀態模式。

3. 狀態模式

3.1 定義

狀態模式的定義如下:

Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.(當一個對象內在狀態改變時允許其改變行為, 這個對象看起來像改變了其類。)

3.2 通用類圖

  • State 抽象狀態角色:接口或抽象類, 負責對象狀態定義, 並且封裝環境角色以實現狀態切換。
  • ConcreteState 具體狀態角色:每一個具體狀態必須完成兩個職責: 本狀態的行為管理以及趨向狀態處理, 通俗地說,就是本狀態下要做的事情, 以及本狀態如何過渡到其他狀態。
  • Context 環境角色:定義客戶端需要的接口, 並且負責具體狀態的切換。

狀態模式從類圖上看比較簡單,實際上還是比較復雜的,它提供了一種對物質運動的另一個觀察視角, 通過狀態變更促使行為的變化。

類似水的狀態變更一樣, 一碗水的初始狀態是液態, 通過加熱轉變為、氣態, 狀態的改變同時也引起體積的擴大, 然后就產生了一個新的行為: 鳴笛或頂起壺蓋,瓦特就是這么發明蒸汽機的。

3.3 通用代碼:

抽象環境角色:

public abstract class State {
    // 定義一個環境角色,提供子類訪問
    protected Context context;
    // 設置環境資源
    public void setContext(Context context) {
        this.context = context;
    }
    // 行為1
    abstract void handle1();
    // 行為2
    abstract void handle2();
}

具體環境角色:

public class ConcreteState1 extends State {
    @Override
    void handle1() {
        //本狀態下必須處理的邏輯
    }

    @Override
    void handle2() {
        //設置當前狀態為stat2
        super.context.setCurrentState(Context.STATE2);
        //過渡到state2狀態, 由Context實現
        super.context.handle2();
    }
}

public class ConcreteState2 extends State {
    @Override
    void handle1() {
        //設置當前狀態為stat2
        super.context.setCurrentState(Context.STATE1);
        //過渡到state2狀態, 由Context實現
        super.context.handle1();
    }

    @Override
    void handle2() {
        // 本狀態下必須處理的邏輯
    }
}

具體環境角色:

public class Context {
    final static State STATE1 = new ConcreteState1();
    final static State STATE2 = new ConcreteState2();

    private State concreteState;

    public State getCurrentState() {
        return concreteState;
    }
    //設置當前狀態
    public void setCurrentState(State currentState) {
        this.concreteState = currentState;
        //切換狀態
        this.concreteState.setContext(this);
    }
    public void handle1(){
        this.concreteState.handle1();
    }
    public void handle2(){
        this.concreteState.handle2();
    }
}

環境角色有兩個不成文的約束:

  • 把狀態對象聲明為靜態常量, 有幾個狀態對象就聲明幾個靜態常量。
  • 環境角色具有狀態抽象角色定義的所有行為, 具體執行使用委托方式。
public class Client {
    public static void main(String[] args) {
        //定義環境角色
        Context context = new Context();
        //初始化狀態
        context.setCurrentState(new ConcreteState1());
        //行為執行
        context.handle1();
        context.handle2();
    }
}

這里我們已經隱藏了狀態的變化過程, 它的切換引起了行為的變化。 對外來說, 我們只看到行為的發生改變, 而不用知道是狀態變化引起的。

3.4 優點

  • 避免了過多的 if else 語句的使用,避免了程序的復雜性,提高系統的可維護性。
  • 使用多態代替了條件判斷,這樣我們代碼的擴展性更強,比如要增加一些狀態,會非常的容易。
  • 狀態是可以被共享的,狀態都是由 static final 進行修飾的。

3.5 缺點

有優點的同事也會產生缺點,有時候,優點和缺點的產生其實是同一個事實:

狀態模式最主要的一個缺點是:子類會太多,也就是類膨脹。因為一個事物有很多個狀態也不稀奇,如果完全使用狀態模式就會有太多的子類,不好管理。

4. 案例完善

前面那個 LOL 的例子,如果使用狀態模式重寫一下,會是這樣的:

首先創建一個跑動的接口:

public interface RunState {
    void run(Hero hero);
}

接下來是4個實現類,分別實現不同狀態的跑動結果:

public class CommonState implements RunState {
    @Override
    public void run(Hero hero) {
        // 正常跑動則不打印內容,否則會刷屏
    }
}

public class SpeedUpState implements RunState{
    @Override
    public void run(Hero hero) {
        System.out.println("--------------加速跑動---------------");
        try {
            Thread.sleep(2000);//假設加速持續2秒
        } catch (InterruptedException e) {}
        hero.setState(Hero.COMMON);
        System.out.println("------加速狀態結束,變為正常狀態------");
    }
}

public class SpeedDownState implements RunState{
    @Override
    public void run(Hero hero) {
        System.out.println("--------------減速跑動---------------");
        try {
            Thread.sleep(2000);//假設減速持續2秒
        } catch (InterruptedException e) {}
        hero.setState(Hero.COMMON);
        System.out.println("------減速狀態結束,變為正常狀態------");
    }
}

public class SwimState implements RunState {
    @Override
    public void run(Hero hero) {
        System.out.println("--------------不能跑動---------------");
        try {
            Thread.sleep(1000);//假設眩暈持續1秒
        } catch (InterruptedException e) {}
        hero.setState(Hero.COMMON);
        System.out.println("------眩暈狀態結束,變為正常狀態------");
    }
}

最后是一個 Hero(Context) 類:

public class Hero {
    public static final RunState COMMON = new CommonState();//正常狀態

    public static final RunState SPEED_UP = new SpeedUpState();//加速狀態

    public static final RunState SPEED_DOWN = new SpeedDownState();//減速狀態

    public static final RunState SWIM = new SwimState();//眩暈狀態

    private RunState state = COMMON;//默認是正常狀態

    private Thread runThread;//跑動線程
    //設置狀態
    public void setState(RunState state) {
        this.state = state;
    }
    //停止跑動
    public void stopRun() {
        if (isRunning()) runThread.interrupt();
        System.out.println("--------------停止跑動---------------");
    }
    //開始跑動
    public void startRun() {
        if (isRunning()) {
            return;
        }
        final Hero hero = this;
        runThread = new Thread(new Runnable() {
            public void run() {
                while (!runThread.isInterrupted()) {
                    state.run(hero);
                }
            }
        });
        System.out.println("--------------開始跑動---------------");
        runThread.start();
    }

    private boolean isRunning(){
        return runThread != null && !runThread.isInterrupted();
    }
}

可以看到,這段代碼和開頭那段代碼雖然完成了一樣的功能,但是整個代碼的復雜度缺以肉眼可見的級別提高了,一般而言,我們犧牲復雜性去換取的高可維護性和擴展性是相當值得的,除非增加了復雜性以后,對於后者的提升會乎其微。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM