1. 程式人生 > >(二十一)狀態模式詳解(DOTA版)

(二十一)狀態模式詳解(DOTA版)

作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原部落格,為保證新部落格中博文的完整性,特複製到此留存,如需轉載請註明新部落格地址即可。

               本次LZ給各位介紹狀態模式,之前在寫設計模式的時候,引入了一些小故事,二十章職責連模式是故事版的最後一篇,之後還剩餘四個設計模式,LZ會依照原生的方式去解釋這幾個設計模式,特別是原型模式和直譯器模式,會包含一些其它的內容。

               好了,接下來,我們先來看看狀態模式的定義吧。

               定義:(源於Design Pattern):當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類。

               上述是百度百科中對狀態模式的定義,定義很簡單,只有一句話,請各位形象的去理解這句話,它說當狀態改變時,這個物件的行為也會變,而看起來就像是這個類改變了一樣。

               這正是應驗了我們那句話,有些人一旦發生過什麼事以後,就像變了個人似的,這句話其實與狀態模式有異曲同工之妙。

               我們仔細體會一下定義當中的要點。

               1、有一個物件,它是有狀態的。

               2、這個物件在狀態不同的時候,行為不一樣。

               3、這些狀態是可以切換的,而非毫無關係。

               前兩點比較好理解,第3點有時候容易給人比較迷惑的感覺,什麼叫這些狀態是可以切換的,而非毫無關係?

               舉個例子,比如一個人的狀態,可以有很多,像生病和健康,這是兩個狀態,這是有關係並且可以轉換的兩個狀態。再比如,睡覺、上班、休息,這也算是一組狀態,這三個狀態也是有關係的並且可以互相轉換。

               那如果把生病和休息這兩個狀態放在一起,就顯得毫無意義了。所以這些狀態應該是一組相關並且可互相切換的狀態。

               下面我們來看看狀態模式的類圖。

                  類圖中包含三個角色。

                  Context:它就是那個含有狀態的物件,它可以處理一些請求,這些請求最終產生的響應會與狀態相關。

                  State:狀態介面,它定義了每一個狀態的行為集合,這些行為會在Context中得以使用。

                  ConcreteState:具體狀態,實現相關行為的具體狀態類。

                  如果針對剛才對於人的狀態的例子來分析,那麼人(Person)就是Context,狀態介面依然是狀態介面,而具體的狀態類,則可以是睡覺,上班,休息,這一系列狀態。

                  不過LZ也看過不少狀態模式的文章和帖子,包括《大話設計模式》當中,都舉的是有關人的狀態的例子,所以這裡給大家換個口味,我們換一個例子。

                  

                  我們來試著寫一個DOTA的例子,最近貌似跟DOTA幹上了,不為其他,就因為DOTA伴隨了LZ四年的大學時光。

                  玩過的朋友都知道,DOTA裡的英雄有很多狀態,比如正常,眩暈,加速,減速等等。相信就算沒有玩過DOTA的朋友們,在其它遊戲裡也能見到類似的情況。那麼假設我們的DOTA沒有使用狀態模式,則我們的英雄類會非常複雜和難以維護,我們來看下,原始版的英雄類是怎樣的。

複製程式碼

package com.state;


//英雄類
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(4000);//假設加速持續4秒
            state = COMMON;
            System.out.println("------加速狀態結束,變為正常狀態------");
        }else if (state == SPEED_DOWN) {
            System.out.println("--------------減速跑動---------------");
            Thread.sleep(4000);//假設減速持續4秒
            state = COMMON;
            System.out.println("------減速狀態結束,變為正常狀態------");
        }else if (state == SWIM) {
            System.out.println("--------------不能跑動---------------");
            Thread.sleep(2000);//假設眩暈持續2秒
            state = COMMON;
            System.out.println("------眩暈狀態結束,變為正常狀態------");
        }else {
            //正常跑動則不列印內容,否則會刷屏
        }
    }

}

複製程式碼

                下面我們寫一個客戶端類,去試圖讓英雄在各種狀態下奔跑一下。

複製程式碼

package com.state;

public class Main {

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

複製程式碼


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

                在上面原始的例子當中,我們的英雄類當中有明顯的if else結構,我們再來看看百度百科中狀態模式所解決的問題的描述。

                狀態模式解決的問題:狀態模式主要解決的是當控制一個物件狀態的條件表示式過於複雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類中,可以把複雜的判斷邏輯簡化。

                不用說,狀態模式是可以解決我們上面的if else結構的,我們採用狀態模式,利用多型的特性可以消除掉if else結構。這樣所帶來的好處就是可以大大的增加程式的可維護性與擴充套件性。

                下面我們就使用狀態模式對上面的例子進行改善,首先第一步,就是我們需要定義一個狀態介面,這個介面就只有一個方法,就是run。

複製程式碼

package com.state;

public interface RunState {

    void run(Hero hero);
    
}

複製程式碼

                與狀態模式類圖不同的是,我們加入了一個引數Hero(Context),這樣做的目的是為了具體的狀態類當達到某一個條件的時候可以切換上下文的狀態。下面列出四個具體的狀態類,其實就是把if else拆掉放到這幾個類的run方法中。

複製程式碼

package com.state;

public class CommonState implements RunState{

    public void run(Hero hero) {
        //正常跑動則不列印內容,否則會刷屏
    }

}

複製程式碼

複製程式碼

package com.state;

public class SpeedUpState implements RunState{

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

複製程式碼

複製程式碼

package com.state;

public class SpeedDownState implements RunState{

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

}

複製程式碼

複製程式碼

package com.state;

public class SwimState implements RunState{

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

}

複製程式碼

                這下我們的英雄類也要相應的改動一下,最主要的改動就是那些if else可以刪掉了,如下。

複製程式碼

package com.state;


//英雄類
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();
    }

}

複製程式碼

                可以看到,現在我們的英雄類優雅了許多,我們使用剛才同樣的客戶端執行即可得到同樣的結果。

                對比我們的原始例子,現在我們使用狀態模式之後,有幾個明顯的優點:

                一、我們去掉了if else結構,使得程式碼的可維護性更強,不易出錯,這個優點挺明顯,如果試圖讓你更改跑動的方法,是剛才的一堆if else好改,還是分成了若干個具體的狀態類好改呢?答案是顯而易見的。

                二、使用多型代替了條件判斷,這樣我們程式碼的擴充套件性更強,比如要增加一些狀態,假設有加速20%,加速10%,減速10%等等等(這並不是虛構,DOTA當中是真實存在這些狀態的),會非常的容易。                

                三、狀態是可以被共享的,這個在上面的例子當中有體現,看下Hero類當中的四個static final變數就知道了,因為狀態類一般是沒有自己的內部狀態的,所有它只是一個具有行為的物件,因此是可以被共享的。
                四、狀態的轉換更加簡單安全,簡單體現在狀態的分割,因為我們把一堆if else分割成了若干個程式碼段分別放在幾個具體的狀態類當中,所以轉換起來當然更簡單,而且每次轉換的時候我們只需要關注一個固定的狀態到其他狀態的轉換。安全體現在型別安全,我們設定上下文的狀態時,必須是狀態介面的實現類,而不是原本的一個整數,這可以杜絕魔數以及不正確的狀態碼。

                

                狀態模式適用於某一個物件的行為取決於該物件的狀態,並且該物件的狀態會在執行時轉換,又或者有很多的if else判斷,而這些判斷只是因為狀態不同而不斷的切換行為。

                上面的適用場景是很多狀態模式的介紹中都提到的,下面我們就來看下剛才DOTA中,英雄例子的類圖。

                  可以看到,這個類圖與狀態模式的標準類圖是幾乎一模一樣的,只是多了一條狀態介面到上下文的依賴線,而這個是根據實際需要新增的,而且一般情況下都是需要的。

                  狀態模式也有它的缺點,不過它的缺點和大多數模式相似,有兩點。

                  1、會增加的類的數量。

                  2、使系統的複雜性增加。
                  儘管狀態模式有著這樣的缺點,但是往往我們犧牲複雜性去換取的高可維護性和擴充套件性是相當值得的,除非增加了複雜性以後,對於後者的提升會乎其微。

                  狀態模式在專案當中也算是較經常會碰到的一個設計模式,但是通常情況下,我們還是在看到if else的情況下,對專案進行重構時使用,又或者你十分確定要做的專案會朝著狀態模式發展,一般情況下,還是不建議在專案的初期使用。

                  好了,本次狀態模式的分享就到此結束了,希望各位有所收穫。

                   


 

 

 

版權宣告

 


作者:zuoxiaolong(左瀟龍)

出處:部落格園左瀟龍的技術部落格--http://www.cnblogs.com/zuoxiaolong

您的支援是對博主最大的鼓勵,感謝您的認真閱讀。

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。