設計模式之狀態模式詳解

電梯具有的動作:

  • 開門:乘客進入、出去。
  • 關門:電梯準備開始運行。
  • 運行:上下運行。
  • 停止:停止運行。

1、先讓電梯運行起來

image.png

//定義電梯接口
public interface ILift {
    void open();
    void close();
    void run();
    void stop();
}
//電梯實現類
public class LiftImp implements ILift{
    @Override
    public void open() {
        System.out.println("電梯開門了……");
    }

    @Override
    public void close() {
        System.out.println("電梯關門了……");
    }

    @Override
    public void run() {
        System.out.println("電梯運行起來了……");
    }

    @Override
    public void stop() {
        System.out.println("電梯停止了……");
    }
}

思考:這個程序有什麼問題?

電梯門可以打開,但不是隨時都可以開,是有前提條件的。你不可能電梯在運行的時候突然開門吧?!電梯也不會出現停止了但是不開門的情況吧?!那要是有也是事故。再仔細想想,電梯的這4個動作的執行都有前置條件,具體點說就是在特定狀態下才能做特定事,那我們來分析一下電梯有哪些特定狀態。

  • 敞門狀態:按了電梯上下按鈕,電梯門開,這中間大概有10秒的時間,那就是敞門狀態。在這個狀態下電梯只能做的動作是關門動作。
  • 閉門狀態:電梯門關閉了,在這個狀態下,可以進行的動作是:開門(我不想坐電梯了)、停止(忘記按路層號了)、運行。
  • 運行狀態:電梯正在跑,上下竄,在這個狀態下,電梯只能做的是停止。
  • 停止狀態:電梯停止不動,在這個狀態下,電梯有兩個可選動作:繼續運行和開門動作。

我們用一張表來表示電梯狀態和動作之間的關係:

image.png

2、修復一下電梯的異常

image.png

//接口中增加了狀態
public interface ILift2 {
    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; //停止狀態
    void open();
    void close();
    void run();
    void stop();
}
public class LiftImp2 implements ILift2 {

    private int state;

    public void setState(int state) {
        this.state = state;
    }
    @Override
    public void open() {
        //電梯在什麼狀態才能開啓
        switch (this.state) {
            case OPENING_STATE: //閉門狀態,什麼都不做
                //do nothing;
                break;
            case CLOSING_STATE: //閉門狀態,則可以開啓
                System.out.println("電梯開門了……");
                this.setState(OPENING_STATE);
                break;
            case RUNNING_STATE: //運行狀態,則不能開門,什麼都不做
                //do nothing;
                break;
            case STOPPING_STATE: //停止狀態,當然要開門了
                System.out.println("電梯開門了……");
                this.setState(OPENING_STATE);
                break;
        }
    }
    @Override
    public void close() {
        //電梯在什麼狀態下才能關閉
        switch (this.state) {
            case OPENING_STATE:  //可以關門,同時修改電梯狀態
                System.out.println("電梯關門了……");
                this.setState(CLOSING_STATE);
                break;
            case CLOSING_STATE:  //電梯是關門狀態,則什麼都不做
                //do nothing;
                break;
            case RUNNING_STATE: //正在運行,門本來就是關閉的,也什麼都不做
                //do nothing;
                break;
            case STOPPING_STATE:  //停止狀態,門也是關閉的,什麼也不做
                //do nothing;
                break;
        }
    }
    @Override
    public void run() {
        switch (this.state) {
            case OPENING_STATE: //敞門狀態,什麼都不做
                //do nothing;
                break;
            case CLOSING_STATE: //閉門狀態,則可以運行
                System.out.println("電梯運行起來了……");
                this.setState(RUNNING_STATE);
                break;
            case RUNNING_STATE: //運行狀態,則什麼都不做
                //do nothing;
                break;
            case STOPPING_STATE: //停止狀態,可以運行
                System.out.println("電梯運行起來了……");
                this.setState(RUNNING_STATE);
        }

    }
    @Override
    public void stop() {
        switch (this.state) {
            case OPENING_STATE: //敞門狀態,要先停下來的,什麼都不做
                //do nothing;
                break;
            case CLOSING_STATE: //閉門狀態,則當然可以停止了
                System.out.println("電梯停止了……");
                this.setState(CLOSING_STATE);
                break;
            case RUNNING_STATE: //運行狀態,有運行當然那也就有停止了
                System.out.println("電梯停止了……");
                this.setState(CLOSING_STATE);
                break;
            case STOPPING_STATE: //停止狀態,什麼都不做
                //do nothing;
                break;
        }
    }
}

思考:這個程序有什麼問題?

  • 電梯實現類Lift有點長

    長的原因是我們在程序中使用了大量的switch…case這樣的判斷(if…else也是一樣),程序中只要有這樣的判斷就避免不了加長程序,而且在業務複雜的情況下,程序會更長,這就不是一個很好的習慣了,較長的方法和類無法帶來良好的維護性,畢竟,程序首先是給人閱讀的,然後纔是機器執行。

  • 擴展性非常差勁

    大家來想想,電梯還有兩個狀態沒有加,是什麼?通電狀態和斷電狀態,你要是在程序增加這兩個方法,你看看Open()、Close()、Run()、Stop()這4個方法都要增加判斷條件,也就是說switch判斷體中還要增加case項,這與開閉原則相違背。

  • 非常規狀態無法實現

    我們來思考我們的業務,電梯在門敞開狀態下就不能上下運行了嗎?電梯有沒有發生過只有運行沒有停止狀態呢(從40層直接墜到1層嘛)?電梯故障嘛,還有電梯在檢修的時候,可以在stop狀態下不開門,這也是正常的業務需求呀,你想想看,如果加上這些判斷條件,上面的程序有多少需要修改?雖然這些都是電梯的業務邏輯,但是一個類有且僅有一個原因引起類的變化,單一職責原則,看看我們的類,業務任務上一個小小的增加或改動都使得我們這個電梯類產生了修改,這在項目開發上是有很大風險的。

如何解決這些問題?剛剛我們都是從電梯的執行方法來分析的,換一個角度:狀態!可以將電梯的運行抽解成兩個任務模型:

  • 當前狀態如何來的?比如停止狀態,肯定是執行了stop方法來的。
  • 在當前狀態下能執行哪些動作?比如停止狀態下,能執行開門、運行。

3、讓電梯完美的運行起來

image.png

在類圖中,定義了一個LiftState抽象類,聲明瞭一個受保護的類型Context變量,這個是串聯各個狀態的封裝類。封裝的目的很明顯,就是電梯對象內部狀態的變化不被調用類知曉,也就是迪米特法則了(我的類內部情節你知道得越少越好),並且還定義了4個具體的實現類,承擔的是狀態的產生以及狀態間的轉換過渡,每個實現類都有open、close、run、stop四個方法。

對於context我們可以這樣理解:Context是一個環境角色,它的作用是串聯各個狀態的過渡,在LiftSate抽象類中我們定義並把這個環境角色聚合進來,並傳遞到子類,也就是4個具體的實現類中自己根據環境來決定如何進行狀態的過渡。

對於LiftState實現類中的方法,以OpenningState爲例進行解釋:Openning狀態是由open()方法產生的,因此,在這個方法中有一個具體的業務邏輯,我們是用print來代替了。在Openning狀態下,電梯能過渡到其他什麼狀態呢?按照現在的定義的是隻能過渡到Closing狀態,因此我們在Close()中定義了狀態變更,同時把Close這個動作也委託了給CloseState類下的Close方法執行,

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();
}
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();
    }
}
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;
     }
}
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();
    }
}
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();
    }
}
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("電梯停止了...");
    }
}

總結一下:

代碼太長的問題:通過各個子類來實現,每個子類的代碼都很短,而且也取消了switch…case條件的判斷。

不符合開閉原則:如果在我們這個例子中要增加兩個狀態應該怎麼做呢?增加兩個子類,一個是通電狀態,另一個是斷電狀態,同時修改其他實現類的相應方法,因爲狀態要過渡,那當然要修改原有的類,只是在原有類中的方法上增加,而不去做修改。

不符合迪米特法則:我們現在的各個狀態是單獨的類,只有與這個狀態有關的因素修改了,這個類才修改,符合迪米特法則。

非常完美!這就是狀態模式。

4、總結

1、狀態模式的定義:

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

狀態模式的核心是封裝,狀態的變更引起了行爲的變更,從外部看起來就好像這個對象對應的類發生了改變一樣。

2、狀態模式中的3個角色:

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

3、狀態模式的優點

  • 結構清晰:避免了過多的switch…case或者if…else語句的使用,避免了程序的複雜性,提高系統的可維護性。
  • 遵循設計原則:很好地體現了開閉原則和單一職責原則,每個狀態都是一個子類,你要增加狀態就要增加子類,你要修改狀態,你只修改一個子類就可以了。
  • 封裝性非常好:這也是狀態模式的基本要求,狀態變換放置到類的內部來實現,外部的調用不用知道類內部如何實現狀態和行爲的變換。

4、狀態模式的缺點

子類會太多,也就是類膨脹。如果一個事物有很多個狀態也不稀奇,如果完全使用狀態模式就會有太多的子類,不好管理,這個需要大家在項目中自己衡量。

5、狀態模式的使用場景

  • 行爲隨狀態改變而改變的場景
    這也是狀態模式的根本出發點,例如權限設計,人員的狀態不同即使執行相同的行爲結果也會不同,在這種情況下需要考慮使用狀態模式。

  • 條件、分支判斷語句的替代者
    在程序中大量使用switch語句或者if判斷語句會導致程序結構不清晰,邏輯混亂,使用狀態模式可以很好地避免這一問題,它通過擴展子類實現了條件的判斷處理。

參考書籍《設計模式之禪》:https://www.kancloud.cn/sstd521/design/193608

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