Java設計模式——狀態模式(STATE PATTERN)

場景一

描述:現在城市發展很快,百萬級人口的城市一堆一堆的,那其中有兩個東西的發明在城市的發展中起到非常重要的作用:一個是汽車,一個呢是...,猜猜看,是什麼?是電梯!汽車讓城市可以橫向擴展,電梯讓城市可以縱向延伸,向空中伸展。汽車對城市的發展我們就不說了,電梯,你想想看,如果沒有電梯,每天你需要爬10 層樓梯,你是不是會崩潰掉?建築師設計了一個沒有電梯的建築,那投資家肯定不願意投資,那也是建築師的恥辱呀,今天我們就用程序表現一下這個電梯是怎麼運作的。

        我們每天都在乘電梯,那我們來看看電梯有哪些動作(映射到Java 中就是有多少方法):開門、關門、運行、停止,就這四個動作,好,我們就用程序來實現一下電梯的動作,先看類圖設計:

非常簡單的類圖,定義一個接口,然後是一個實現類,然後業務類Client 就可以調用,並運行起來,簡單也來看看我們的程序,先看接口:

package com.gumx.common;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 定義一個電梯的接口
*/
public interface ILift {
    //首先電梯門開啓動作
    public void open();
    //電梯門有開啓,那當然也就有關閉了
    public void close();
    //電梯要能上能下,跑起來
    public void run();
    //電梯還要能停下來,停不下來那就扯淡了
    public void stop();
}
然後看實現類:

package com.gumx.common;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 電梯的實現類
*/
public class Lift implements ILift {
    //電梯門關閉
    public void close() {
        System.out.println("電梯門關閉...");
    }
    //電梯門開啓
    public void open() {
        System.out.println("電梯門開啓...");
    }
    //電梯開始跑起來
    public void run() {
        System.out.println("電梯上下跑起來...");
    }
    //電梯停止
    public void stop() {
        System.out.println("電梯停止了...");
    }
}
電梯的開、關、跑、停都實現了,開看業務是怎麼調用的:

package com.gumx.common;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 模擬電梯的動作
*/
public class Client {
    public static void main(String[] args) {
        ILift lift = new Lift();
        //首先是電梯門開啓,人進去
        lift.open();
        //然後電梯門關閉
        lift.close();
        //再然後,電梯跑起來,向上或者向下
        lift.run();
        //最後到達目的地,電梯挺下來
        lift.stop();
    }
}
運行的結果如下:

電梯門開啓...
電梯門關閉...
電梯上下跑起來...
電梯停止了...

        太簡單的程序了,是個程序員都會寫這個程序,這麼簡單的程序還拿出來show,是不是太小看我們的智商了?!非也,非也,我們繼續往下分析,這個程序有什麼問題,你想呀電梯門可以打開,但不是隨時都可以開,是有前提條件的的,你不可能電梯在運行的時候突然開門吧?!電梯也不會出現停止了但是不開門的情況吧?!那要是有也是事故嘛,再仔細想想,電梯的這四個動作的執行都是有前置條件,具體點說說在特定狀態下才能做特定事,那我們來分析一下電梯有什麼那些特定狀態:
        門敞狀態---按了電梯上下按鈕,電梯門開,這中間有5 秒的時間(當然你也可以用身體擋住電梯門,那就不是5 秒了),那就是門敞狀態;在這個狀態下電梯只能做的動作是關門動作,做別的動作?那就危險嘍

        門閉狀態---電梯門關閉了,在這個狀態下,可以進行的動作是:開門(我不想坐電梯了)、停止(忘記按路層號了)、運行
       運行狀態---電梯正在跑,上下竄,在這個狀態下,電梯只能做的是停止;
       停止狀態---電梯停止不動,在這個狀態下,電梯有兩個可選動作:繼續運行和開門動作;
       我們用一張表來表示電梯狀態和動作之間的關係:


                                   電梯狀態和動作對應表(○表示不允許,☆表示允許動作)

看到這張表後,我們才發覺,哦~~,我們的程序做的很不嚴謹,好,我們來修改一下,先看類圖:


                                    增加了狀態的類圖

        在接口中定義了四個常量,分別表示電梯的四個狀態:門敞狀態、關閉狀態、運行狀態、停止狀態,然後在實現類中電梯的每一次動作發生都要對狀態進行判斷,判斷是否運行執行,也就是動作的執行是否符合業務邏輯,實現類中的四個私有方法是僅僅實現電梯的動作,沒有任何的前置條件,因此這四個方法是不能爲外部類調用的,設置爲私有方法。我們先看接口的改變:

package com.gumx.common2;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 定義一個電梯的接口
*/
public interface ILift {
    //電梯的四個狀態
    public final static int OPENING_STATE = 1; //門敞狀態
    public final static int CLOSING_STATE = 2; //門閉狀態
    public final static int RUNNING_STATE = 3; //運行狀態
    public final static int STOPPING_STATE = 4; //停止狀態;
    //設置電梯的狀態
    public void setState(int state);
    //首先電梯門開啓動作
    public void open();
    //電梯門有開啓,那當然也就有關閉了
    public void close();
    //電梯要能上能下,跑起來
    public void run();
    //電梯還要能停下來,停不下來那就扯淡了
    public void stop();
}
增加了四個靜態常量,增加了一個方法setState,設置電梯的狀態。我們再來看實現類是如何實現的:
package com.gumx.common2;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 電梯的實現類
*/
public class Lift implements ILift {
    private int state;
    public void setState(int state) {
        this.state = state;
    }
    //電梯門關閉
    public void close() {
        //電梯在什麼狀態下才能關閉
        switch(this.state){
            case OPENING_STATE: //如果是則可以關門,同時修改電梯狀態
            this.closeWithoutLogic();
            this.setState(CLOSING_STATE);
            break;
            case CLOSING_STATE: //如果電梯就是關門狀態,則什麼都不做
            //do nothing;
            break;
            case RUNNING_STATE: //如果是正在運行,門本來就是關閉的,也說明都不做
            //do nothing;
            break;
            case STOPPING_STATE: //如果是停止狀態,本也是關閉的,什麼也不做
            //do nothing;
            break;
        }
    }
    //電梯門開啓
    public void open() {
        //電梯在什麼狀態才能開啓
        switch(this.state){
            case OPENING_STATE: //如果已經在門敞狀態,則什麼都不做
           //do nothing;
           break;
           case CLOSING_STATE: //如是電梯時關閉狀態,則可以開啓
           this.openWithoutLogic();
           this.setState(OPENING_STATE);
           break;
           case RUNNING_STATE: //正在運行狀態,則不能開門,什麼都不做
           //do nothing;
           break;
           case STOPPING_STATE: //停止狀態,淡然要開門了
           this.openWithoutLogic();
           this.setState(OPENING_STATE);
           break;
        }
    }
    //電梯開始跑起來
    public void run() {
        switch(this.state){
            case OPENING_STATE: //如果已經在門敞狀態,則不你能運行,什麼都不做
            //do nothing;
            break;
            case CLOSING_STATE: //如是電梯時關閉狀態,則可以運行
            this.runWithoutLogic();
            this.setState(RUNNING_STATE);
            break;
            case RUNNING_STATE: //正在運行狀態,則什麼都不做
            //do nothing;
            break;
            case STOPPING_STATE: //停止狀態,可以運行
            this.runWithoutLogic();
            this.setState(RUNNING_STATE);
        }
    }
    //電梯停止
    public void stop() {
        switch(this.state){
            case OPENING_STATE: //如果已經在門敞狀態,那肯定要先停下來的,什麼都不做
            //do nothing;
            break;
            case CLOSING_STATE: //如是電梯時關閉狀態,則當然可以停止了
            this.stopWithoutLogic();
            this.setState(CLOSING_STATE);
            break;
            case RUNNING_STATE: //正在運行狀態,有運行當然那也就有停止了
            this.stopWithoutLogic();
            this.setState(CLOSING_STATE);
            break;
            case STOPPING_STATE: //停止狀態,什麼都不做
            //do nothing;
            break;
        }
    }
    //純粹的電梯關門,不考慮實際的邏輯
    private void closeWithoutLogic(){
        System.out.println("電梯門關閉...");
    }
    //純粹的店門開,不考慮任何條件
    private void openWithoutLogic(){
        System.out.println("電梯門開啓...");
    }
    //純粹的運行,不考慮其他條件
    private void runWithoutLogic(){
        System.out.println("電梯上下跑起來...");
    }
    //單純的停止,不考慮其他條件
    private void stopWithoutLogic(){
        System.out.println("電梯停止了...");
    }
}
程序有點長,但是還是很簡單的,就是在每一個接口定義的方法中使用witch…case 來進行判斷,是否運行運行指定的動作。我們來Client 程序的變更:

package com.gumx.common2;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 模擬電梯的動作
*/
public class Client {
    public static void main(String[] args) {
        ILift lift = new Lift();
        //電梯的初始條件應該是停止狀態
        lift.setState(ILift.STOPPING_STATE);
        //首先是電梯門開啓,人進去
        lift.open();
        //然後電梯門關閉
        lift.close();
        //再然後,電梯跑起來,向上或者向下
        lift.run();
        //最後到達目的地,電梯挺下來
        lift.stop();
    }
}
業務調用的方法中增加了電梯狀態判斷,電梯要開門不是隨時都可以開的,必須滿足了一定條件你才能開門,人才能走進去,我們設置電梯的起始是停止狀態,看運行結果:

電梯門開啓...
電梯門關閉...
電梯上下跑起來...
電梯停止了...

         我們來想一下,這段程序有什麼問題,首先Lift.java 這個文件有點長,長的原因是我們在程序中使用了大量的switch…case 這樣的判斷(if…else 也是一樣),程序中只要你有這樣的判斷就避免不了加長程序,同步的在業務比較複雜的情況下,程序體會更長,這個就不是一個很好的習慣了,較長的方法或者類的維護性比較差,畢竟程序是給人來閱讀的;其次,擴展性非常的不好,大家來想想,電梯還有兩個狀態沒有加,是什麼?通電狀態和斷電狀態,你要是在程序再增加這兩個方法,你看看Open()、Close()、Run()、Stop()這四個方法都要增加判斷條件,也就是說switch 判斷體中還要增加case 項,也就說與開閉原則相違背了;再其次,我們來思考我們的業務,電梯在門敞開狀態下就不能上下跑了嗎?電梯有沒有發生過只有運行沒有停止狀態呢(從40 層直接墜到1 層嘛)?電梯故障嘛,還有電梯在檢修的時候,可以在stop狀態下不開門,這也是正常的業務需求呀,你想想看,如果加上這些判斷條件,上面的程序有多少需要修改?雖然這些都是電梯的業務邏輯,但是一個類有且僅有一個原因引起類的變化,單一職責原則,看看我們的類,業務上的任務一個小小增加或改動都對我們的這個電梯類產生了修改,這是在項目開發上是有很大風險的。既然我們已經發現程序上有以上問題,我們怎麼來修改呢?
        剛剛我們是從電梯的有哪些方法以及這些方法執行的條件去分析,現在我們換個角度來看問題,我們來想電梯在具有這些狀態的時候,能夠做什麼事情,也就是說在電梯處於一個具體狀態時,我們來思考這個狀態是由什麼動作觸發而產生以及在這個狀態下電梯還能做什麼事情,舉個例子來說,電梯在停止狀態時,我們來思考兩個問題:

        第一、這個停止狀態時怎麼來的,那當然是由於電梯執行了stop 方法而來的;
        第二、在停止狀態下,電梯還能做什麼動作?繼續運行?開門?那當然都可以了。
       我們再來分析其他三個狀態,也都是一樣的結果,我們只要實現電梯在一個狀態下的兩個任務模型就可以了:這個狀態是如何產生的以及在這個狀態下還能做什麼其他動作(也就是這個狀態怎麼過渡到其他狀態),既然我們以狀態爲參考模型,那我們就先定義電梯的狀態接口,思考過後我們來看類圖:


                                              以狀態作爲導向的類圖

        在類圖中,定義了一個LiftState 抽象類,聲明瞭一個受保護的類型Context 變量,這個是串聯我們各個狀態的封裝類,封裝的目的很明顯,就是電梯對象內部狀態的變化不被調用類知曉,也就是迪米特法則了,我的類內部情節你知道越少越好,並且還定義了四個具體的實現類,承擔的是狀態的產生以及狀態間的轉換過渡,我們先來看LiftState 程序:

package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 定義一個電梯的接口
*/
public abstract class LiftState{
    //定義一個環境角色,也就是封裝狀態的變換引起的功能變化
    protected Context context;
    public void setContext(Context _context){
        this.context = _context;
    }
    //首先電梯門開啓動作
    public abstract void open();
    //電梯門有開啓,那當然也就有關閉了
    public abstract void close();
    //電梯要能上能下,跑起來
    public abstract void run();
    //電梯還要能停下來,停不下來那就扯淡了
    public abstract void stop();
}
抽象類比較簡單,我們來先看一個具體的實現,門敞狀態的實現類:

package com.cbf4life.advance;
/**
* @author cbf4Life [email protected]
* I'm glad to share my knowledge with you all.
* 在電梯門開啓的狀態下能做什麼事情
*/
public class OpenningState extends LiftState {
//開啓當然可以關閉了,我就想測試一下電梯門開關功能
@Override
public void close() {
//狀態修改
super.context.setLiftState(Context.closeingState);
//動作委託爲CloseState來執行
super.context.getLiftState().close();
}
//打開電梯門
@Override
public void open() {
System.out.println("電梯門開啓...");
}
//門開着電梯就想跑,這電梯,嚇死你!
@Override
public void run() {
//do nothing;
}
//開門還不停止?
public void stop() {
//do nothing;
}
}
        我來解釋一下這個類的幾個方法,Openning 狀態是由open()方法產生的,因此這個方法中有一個具體的業務邏輯,我們是用print 來代替了;在Openning 狀態下,電梯能過渡到其他什麼狀態呢?按照現在的定義的是隻能過渡到Closing 狀態,因此我們在Close()中定義了狀態變更,同時把Close 這個動作也委託了給CloseState 類下的Close 方法執行,這個可能不好理解,我們再看看Context 類就可能好理解一點:

package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
*/
public class Context {
    //定義出所有的電梯狀態
    public final static OpenningState openningState = new OpenningState();
    public final static ClosingState closeingState = new ClosingState();
    public final static RunningState runningState = new RunningState();
    public final static StoppingState stoppingState = new StoppingState();
    //定一個當前電梯狀態
    private LiftState liftState;
    public LiftState getLiftState() {
        return liftState;
    }
    public void setLiftState(LiftState liftState) {
        this.liftState = liftState;
        //把當前的環境通知到各個實現類中
        this.liftState.setContext(this);
    }
    public void open(){
        this.liftState.open();
    }
    public void close(){
        this.liftState.close();
    }
    public void run(){
        this.liftState.run();
    }
    public void stop(){
        this.liftState.stop();
    }
}
結合以上三個類,我們可以這樣理解,Context 是一個環境角色,它的作用是串聯各個狀態的過渡,在
LiftSate 抽象類中我們定義了並把這個環境角色聚合進來,並傳遞到了子類,也就是四個具體的實現類中
自己根據環境來決定如何進行狀態的過渡。我們把其他的三個具體實現類閱讀完畢,下面是關閉狀態:
package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 電梯門關閉以後,電梯可以做哪些事情
*/
public class ClosingState extends LiftState {
    //電梯門關閉,這是關閉狀態要實現的動作
    @Override
    public void close() {
        System.out.println("電梯門關閉...");
    }
    //電梯門關了再打開,逗你玩呢,那這個允許呀
    @Override
    public void open() {
        super.context.setLiftState(Context.openningState); //置爲門敞狀態
        super.context.getLiftState().open();
    }
    //電梯門關了就跑,這是再正常不過了
    @Override
    public void run() {
        super.context.setLiftState(Context.runningState); //設置爲運行狀態;
        super.context.getLiftState().run();
    }
    //電梯門關着,我就不按樓層
    @Override
    public void stop() {
        super.context.setLiftState(Context.stoppingState); //設置爲停止狀態;
        super.context.getLiftState().stop();
    }
}
下面是電梯的運行狀態:

package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 電梯在運行狀態下能做哪些動作
*/
public class RunningState extends LiftState {
    //電梯門關閉?這是肯定了
    @Override
    public void close() {
        //do nothing
    }
    //運行的時候開電梯門?你瘋了!電梯不會給你開的
    @Override
    public void open() {
        //do nothing
    }
    //這是在運行狀態下要實現的方法
    @Override
    public void run() {
        System.out.println("電梯上下跑...");
    }
    //這個事絕對是合理的,光運行不停止還有誰敢做這個電梯?!估計只有上帝了
    @Override
    public void stop() {
        super.context.setLiftState(Context.stoppingState); //環境設置爲停止狀態;
        super.context.getLiftState().stop();
    }
}
下面是停止狀態:

package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 在停止狀態下能做什麼事情
*/
public class StoppingState extends LiftState {
    //停止狀態關門?電梯門本來就是關着的!
    @Override
    public void close() {
        //do nothing;
    }
    //停止狀態,開門,那是要的!
    @Override
    public void open() {
        super.context.setLiftState(Context.openningState);
        super.context.getLiftState().open();
    }
    //停止狀態再跑起來,正常的很
    @Override
    public void run() {
        super.context.setLiftState(Context.runningState);
        super.context.getLiftState().run();
    }
    //停止狀態是怎麼發生的呢?當然是停止方法執行了
    @Override
    public void stop() {
        System.out.println("電梯停止了...");
    }
}
業務邏輯都已經實現了,我們來看看Client 怎麼實現:

package com.gumx.advance;
/**
* @author gumx
* I'm glad to share my knowledge with you all.
* 模擬電梯的動作
*/
public class Client {
    public static void main(String[] args) {
        Context context = new Context();
        context.setLiftState(new ClosingState());
        context.open();
        context.close();
        context.run();
        context.stop();
    }
}
Client 調用類太簡單了,只要定義個電梯的初始狀態,然後調用相關的方法,就完成了,完全不用考
慮狀態的變更,看運行結果:

電梯門開啓...
電梯門關閉...
電梯上下跑起來...
電梯停止了...

        我們再來回顧一下我們剛剛批判上一段的代碼,首先我們說人家代碼太長,這個問題我們解決了,通過各個子類來實現,每個子類的代碼都很短,而且也取消了的switch…case 條件的判斷;其次,說人家不符合開閉原則,那如果在我們這個例子中要增加兩個狀態怎麼加?增加兩個子類,一個是通電狀態,一個是斷電狀態,同時修改其他實現類的相應方法,因爲狀態要過渡呀,那當然要修改原有的類,只是在原有類中的方法上增加,而不去做修改;再其次,我們說人家不符合迪米特法則,我們現在呢是各個狀態是單獨的一個類,只有與這個狀態的有關的因素修改了這個類才修改,符合迪米特法則,非常完美!

        上面例子中多次提到狀態,那我們這節講的就是狀態模式,什麼是狀態模式呢?當一個對象內在狀態改變時允許其改變行爲,這個對象看起來像是改變了其類。說實話,這個定義的後半句我也沒看懂,看過GOF 才明白是怎麼回事: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class. [GoF, p305],也就是說狀態模式封裝的非常好,狀態的變更引起了行爲的變更,從外部看起來就好像這個對象對應的類發生了改變一樣。狀態模式的通用實現類如下:


                                                                        狀態模式通用類圖

        狀態模式中有什麼優點呢?首先是避免了過多的swith…case 或者if..else 語句的使用,避免了程序的複雜性;其次是很好的使用體現了開閉原則和單一職責原則,每個狀態都是一個子類,你要增加狀態就增加子類,你要修改狀態,你只修改一個子類就可以了;最後一個好處就是封裝性非常好,這也是狀態模式的基本要求,狀態變換放置到了類的內部來實現,外部的調用不用知道類內部如何實現狀態和行爲的變換。
        狀態模式既然有優點,那當然有缺點了,只有一個缺點,子類會太多,也就是類膨脹,你想一個事物有七八、十來個狀態也不稀奇,如果完全使用狀態模式就會有太多的子類,不好管理,這個需要大家在項目自己衡量。其實有很大方式解決這個狀態問題,比如在數據庫中建立一個狀態表,然後根據狀態執行相應的操作,這個也不復雜,看大家的習慣和嗜好了。狀態模式使用於當某個對象在它的狀態發生改變時,它的行爲也隨着發生比較大的變化,也就是說行爲是受狀態約束的情況下可以使用狀態模式,而且狀態模式使用時對象的狀態最好不要超過五個,防止你寫子類寫瘋掉。

          上面的例子可能比較複雜,請各位看官耐心的看,看完我想肯定有所收穫。我翻遍了所有能找的到的資料(至少也有十幾本,其中有幾本原文的書還是很的很不錯的,我舉這個電梯的例子也是從《Design  Pattern for Dummies》這本書來激發出來的),基本(基本哦,還是有幾本講的不錯)上沒有一本把這個狀態模式講透徹的,我不敢說我就講的透徹,大家都只講了一個狀態到另一個狀態過渡,狀態間的過渡是固定的,舉個簡單的例子:


        這個狀態圖是很多書上都有的,狀態A 只能變更到狀態B,狀態B 再變更到狀態C,例子舉的最多的就是TCP 監聽的例子,TCP 有三個狀態:等待,連接,斷開,然後這三個狀態中按照順序循環變更,按照這個狀態變更來講解狀態模式,我認爲是不太合適的,爲什麼呢?你在項目中太少看到一個狀態只能過渡到另一個狀態情形,項目中遇到的大多數情況都是一個狀態可以轉換爲幾種狀態,如下圖:


        狀態B 可以轉換爲狀態C 也可以轉換爲狀態D,而狀態D 呢也可以轉換爲狀態A 或狀態B,這在項目分析過程中有一個叫狀態圖可以完整的展示這種蜘蛛網結構,舉個實際例子來說,一些收費網站的用戶就有很多狀態,比如普通用戶,普通會員,VIP 會員,白金級用戶等等,這個狀態的變更你不允許跳躍?!這不可能,所以我在例子中就舉了一個比較複雜的應用,基本上可以實現狀態間自由切換,這纔是最經常用到的狀態模式。然後我再提問個問題,狀態間的自由切換,那會有很多種呀,你要一個一個的牢記一遍嗎?比如上面那個電梯的例子,我要一個正常的電梯運行邏輯,規則是開門->關門->運行->停止;還要一個緊急狀態(比如火災)下的運行邏輯,關門->停止,緊急狀態電梯當然不能用了;再要一個維修狀態下的運行邏輯,這個狀態任何情況都可以,開着門電梯運行?可以!門來回開關?可以!永久停止不動?可以! 那這怎麼實現呢?需要我們把已經有的幾種狀態按照一定的順序再重新組裝一下,那這個是什麼模式?什麼模式?大聲點!建造者模式!對,建造模式+狀態模式會起到非常好的封裝作用。

         再往深裏面扯幾句,應該有部分讀者做過工作流開發,如果不是土製框架的話,就應該有個狀態機管理(即使是土製框架也應該有),比如一個Activity(節點)有初始化狀態(Initialized State)、掛起狀態(Suspended State)、完成狀態(Completed State)等等,流程實例也是有這麼多狀態,那這些狀態怎麼管理呢?通過狀態機(State Machine)來管理,那狀態機是個什麼東西呢?就是我們上面提到的Context類的升級變態BOSS!



















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