狀態(tài)模式

電梯具有的動作:

  • 開門:乘客進入鲁捏、出去芯砸。
  • 關(guān)門:電梯準備開始運行。
  • 運行:上下運行给梅。
  • 停止:停止運行假丧。

1、先讓電梯運行起來

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

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

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

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

思考:這個程序有什么問題动羽?

電梯門可以打開包帚,但不是隨時都可以開,是有前提條件的运吓。你不可能電梯在運行的時候突然開門吧渴邦?疯趟!電梯也不會出現(xiàn)停止了但是不開門的情況吧?谋梭!那要是有也是事故信峻。再仔細想想,電梯的這4個動作的執(zhí)行都有前置條件瓮床,具體點說就是在特定狀態(tài)下才能做特定事盹舞,那我們來分析一下電梯有哪些特定狀態(tài)。

  • 敞門狀態(tài):按了電梯上下按鈕隘庄,電梯門開踢步,這中間大概有10秒的時間,那就是敞門狀態(tài)丑掺。在這個狀態(tài)下電梯只能做的動作是關(guān)門動作获印。
  • 閉門狀態(tài):電梯門關(guān)閉了,在這個狀態(tài)下吼鱼,可以進行的動作是:開門(我不想坐電梯了)蓬豁、停止(忘記按路層號了)、運行菇肃。
  • 運行狀態(tài):電梯正在跑地粪,上下竄,在這個狀態(tài)下琐谤,電梯只能做的是停止蟆技。
  • 停止狀態(tài):電梯停止不動,在這個狀態(tài)下斗忌,電梯有兩個可選動作:繼續(xù)運行和開門動作质礼。

我們用一張表來表示電梯狀態(tài)和動作之間的關(guān)系:

image.png

2、修復(fù)一下電梯的異常

image.png
//接口中增加了狀態(tài)
public interface ILift2 {
    public final static int OPENING_STATE = 1;  //敞門狀態(tài)
    public final static int CLOSING_STATE = 2;  //閉門狀態(tài)
    public final static int RUNNING_STATE = 3;  //運行狀態(tài)
    public final static int STOPPING_STATE = 4; //停止狀態(tài)
    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() {
        //電梯在什么狀態(tài)才能開啟
        switch (this.state) {
            case OPENING_STATE: //閉門狀態(tài)织阳,什么都不做
                //do nothing;
                break;
            case CLOSING_STATE: //閉門狀態(tài)眶蕉,則可以開啟
                System.out.println("電梯開門了……");
                this.setState(OPENING_STATE);
                break;
            case RUNNING_STATE: //運行狀態(tài),則不能開門唧躲,什么都不做
                //do nothing;
                break;
            case STOPPING_STATE: //停止狀態(tài)造挽,當然要開門了
                System.out.println("電梯開門了……");
                this.setState(OPENING_STATE);
                break;
        }
    }
    @Override
    public void close() {
        //電梯在什么狀態(tài)下才能關(guān)閉
        switch (this.state) {
            case OPENING_STATE:  //可以關(guān)門,同時修改電梯狀態(tài)
                System.out.println("電梯關(guān)門了……");
                this.setState(CLOSING_STATE);
                break;
            case CLOSING_STATE:  //電梯是關(guān)門狀態(tài)弄痹,則什么都不做
                //do nothing;
                break;
            case RUNNING_STATE: //正在運行饭入,門本來就是關(guān)閉的,也什么都不做
                //do nothing;
                break;
            case STOPPING_STATE:  //停止狀態(tài)肛真,門也是關(guān)閉的谐丢,什么也不做
                //do nothing;
                break;
        }
    }
    @Override
    public void run() {
        switch (this.state) {
            case OPENING_STATE: //敞門狀態(tài),什么都不做
                //do nothing;
                break;
            case CLOSING_STATE: //閉門狀態(tài),則可以運行
                System.out.println("電梯運行起來了……");
                this.setState(RUNNING_STATE);
                break;
            case RUNNING_STATE: //運行狀態(tài)乾忱,則什么都不做
                //do nothing;
                break;
            case STOPPING_STATE: //停止狀態(tài)讥珍,可以運行
                System.out.println("電梯運行起來了……");
                this.setState(RUNNING_STATE);
        }

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

思考:這個程序有什么問題纲酗?

  • 電梯實現(xiàn)類Lift有點長

    長的原因是我們在程序中使用了大量的switch...case這樣的判斷(if...else也是一樣),程序中只要有這樣的判斷就避免不了加長程序新蟆,而且在業(yè)務(wù)復(fù)雜的情況下觅赊,程序會更長,這就不是一個很好的習慣了琼稻,較長的方法和類無法帶來良好的維護性吮螺,畢竟,程序首先是給人閱讀的帕翻,然后才是機器執(zhí)行鸠补。

  • 擴展性非常差勁

    大家來想想,電梯還有兩個狀態(tài)沒有加嘀掸,是什么紫岩?通電狀態(tài)和斷電狀態(tài),你要是在程序增加這兩個方法睬塌,你看看Open()泉蝌、Close()、Run()揩晴、Stop()這4個方法都要增加判斷條件勋陪,也就是說switch判斷體中還要增加case項,這與開閉原則相違背硫兰。

  • 非常規(guī)狀態(tài)無法實現(xiàn)

    我們來思考我們的業(yè)務(wù)诅愚,電梯在門敞開狀態(tài)下就不能上下運行了嗎?電梯有沒有發(fā)生過只有運行沒有停止狀態(tài)呢(從40層直接墜到1層嘛)劫映?電梯故障嘛呻粹,還有電梯在檢修的時候,可以在stop狀態(tài)下不開門苏研,這也是正常的業(yè)務(wù)需求呀,你想想看腮郊,如果加上這些判斷條件摹蘑,上面的程序有多少需要修改?雖然這些都是電梯的業(yè)務(wù)邏輯轧飞,但是一個類有且僅有一個原因引起類的變化衅鹿,單一職責原則撒踪,看看我們的類,業(yè)務(wù)任務(wù)上一個小小的增加或改動都使得我們這個電梯類產(chǎn)生了修改大渤,這在項目開發(fā)上是有很大風險的制妄。

如何解決這些問題?剛剛我們都是從電梯的執(zhí)行方法來分析的泵三,換一個角度:狀態(tài)耕捞!可以將電梯的運行抽解成兩個任務(wù)模型:

  • 當前狀態(tài)如何來的?比如停止狀態(tài)烫幕,肯定是執(zhí)行了stop方法來的俺抽。
  • 在當前狀態(tài)下能執(zhí)行哪些動作?比如停止狀態(tài)下较曼,能執(zhí)行開門磷斧、運行。

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

image.png

在類圖中弛饭,定義了一個LiftState抽象類,聲明了一個受保護的類型Context變量萍歉,這個是串聯(lián)各個狀態(tài)的封裝類侣颂。封裝的目的很明顯,就是電梯對象內(nèi)部狀態(tài)的變化不被調(diào)用類知曉翠桦,也就是迪米特法則了(我的類內(nèi)部情節(jié)你知道得越少越好)横蜒,并且還定義了4個具體的實現(xiàn)類,承擔的是狀態(tài)的產(chǎn)生以及狀態(tài)間的轉(zhuǎn)換過渡销凑,每個實現(xiàn)類都有open丛晌、close、run斗幼、stop四個方法澎蛛。

對于context我們可以這樣理解:Context是一個環(huán)境角色,它的作用是串聯(lián)各個狀態(tài)的過渡蜕窿,在LiftSate抽象類中我們定義并把這個環(huán)境角色聚合進來谋逻,并傳遞到子類,也就是4個具體的實現(xiàn)類中自己根據(jù)環(huán)境來決定如何進行狀態(tài)的過渡桐经。

對于LiftState實現(xiàn)類中的方法毁兆,以O(shè)penningState為例進行解釋:Openning狀態(tài)是由open()方法產(chǎn)生的,因此阴挣,在這個方法中有一個具體的業(yè)務(wù)邏輯气堕,我們是用print來代替了。在Openning狀態(tài)下,電梯能過渡到其他什么狀態(tài)呢茎芭?按照現(xiàn)在的定義的是只能過渡到Closing狀態(tài)揖膜,因此我們在Close()中定義了狀態(tài)變更,同時把Close這個動作也委托了給CloseState類下的Close方法執(zhí)行梅桩,

public abstract class LiftState {
    //定義一個環(huán)境角色壹粟,也就是封裝狀態(tài)的變化引起的功能變化
    protected Context context;
    public void setContext(Context _context){
        this.context = _context;
    }
    //首先電梯門開啟動作
    public abstract void open();
    //電梯門有開啟,那當然也就有關(guān)閉了
    public abstract void close();
    //電梯要能上能下宿百,運行起來
    public abstract void run();
    //電梯還要能停下來
    public abstract void stop();
}
public class Context {
    //定義出所有的電梯狀態(tài)
    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();
    //定義一個當前電梯狀態(tài)
    private LiftState liftState;
    public LiftState getLiftState() {
        return liftState;
    }
    public void setLiftState(LiftState liftState) {
        this.liftState = liftState;
        //把當前的環(huán)境通知到各個實現(xiàn)類中
        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 {
     //開啟當然可以關(guān)閉了趁仙,我就想測試一下電梯門開關(guān)功能
     @Override
     public void close() {
             //狀態(tài)修改
             super.context.setLiftState(Context.closeingState);
             //動作委托為CloseState來執(zhí)行
             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{
    //電梯門關(guān)閉外臂,這是關(guān)閉狀態(tài)要實現(xiàn)的動作
    @Override
    public void close() {
        System.out.println("電梯門關(guān)閉...");
    }
    //電梯門關(guān)了再打開
    @Override
    public void open() {
        super.context.setLiftState(Context.openningState);  //置為敞門狀態(tài)
        super.context.getLiftState().open();
    }
    //電梯門關(guān)了就運行坐儿,這是再正常不過了
    @Override
    public void run() {
        super.context.setLiftState(Context.runningState); //設(shè)置為運行狀態(tài)
        super.context.getLiftState().run();
    }
    //電梯門關(guān)著,我就不按樓層
    @Override
    public void stop() {
        super.context.setLiftState(Context.stoppingState);  //設(shè)置為停止狀態(tài)
        super.context.getLiftState().stop();
    }
}
public class RunningState extends LiftState {
    //電梯門關(guān)閉宋光?這是肯定的
    @Override
    public void close() {
        //do nothing
    }
    //運行的時候開電梯門貌矿?你瘋了!電梯不會給你開的
    @Override
    public void open() {
        //do nothing
    }
    //這是在運行狀態(tài)下要實現(xiàn)的方法
    @Override
    public void run() {
        System.out.println("電梯上下運行...");
    }
    //這絕對是合理的罪佳,只運行不停止還有誰敢坐這個電梯逛漫?!估計只有上帝了
    @Override
    public void stop() {
        super.context.setLiftState(Context.stoppingState);//環(huán)境設(shè)置為停止狀態(tài)
        super.context.getLiftState().stop();
    }
}
public class StoppingState extends LiftState {
    //停止狀態(tài)關(guān)門赘艳?電梯門本來就是關(guān)著的酌毡!
    @Override
    public void close() {
        //do nothing;
    }
    //停止狀態(tài),開門蕾管,那是要的枷踏!
    @Override
    public void open() {
        super.context.setLiftState(Context.openningState);
        super.context.getLiftState().open();
    }
    //停止狀態(tài)再運行起來,正常得很
    @Override
    public void run() {
        super.context.setLiftState(Context.runningState);
        super.context.getLiftState().run();
    }
    //停止狀態(tài)是怎么發(fā)生的呢掰曾?當然是停止方法執(zhí)行了
    @Override
    public void stop() {
        System.out.println("電梯停止了...");
    }
}

總結(jié)一下:

代碼太長的問題:通過各個子類來實現(xiàn)旭蠕,每個子類的代碼都很短,而且也取消了switch...case條件的判斷旷坦。

不符合開閉原則:如果在我們這個例子中要增加兩個狀態(tài)應(yīng)該怎么做呢掏熬?增加兩個子類,一個是通電狀態(tài)秒梅,另一個是斷電狀態(tài)旗芬,同時修改其他實現(xiàn)類的相應(yīng)方法,因為狀態(tài)要過渡捆蜀,那當然要修改原有的類岗屏,只是在原有類中的方法上增加辆琅,而不去做修改。

不符合迪米特法則:我們現(xiàn)在的各個狀態(tài)是單獨的類这刷,只有與這個狀態(tài)有關(guān)的因素修改了,這個類才修改娩井,符合迪米特法則暇屋。

非常完美!這就是狀態(tài)模式。

4洞辣、總結(jié)

1咐刨、狀態(tài)模式的定義:

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

狀態(tài)模式的核心是封裝定鸟,狀態(tài)的變更引起了行為的變更,從外部看起來就好像這個對象對應(yīng)的類發(fā)生了改變一樣著瓶。

2联予、狀態(tài)模式中的3個角色:

  • State——抽象狀態(tài)角色:接口或抽象類,負責對象狀態(tài)定義材原,并且封裝環(huán)境角色以實現(xiàn)狀態(tài)切換沸久。
  • ConcreteState——具體狀態(tài)角色:每一個具體狀態(tài)必須完成兩個職責:本狀態(tài)的行為管理以及趨向狀態(tài)處理,通俗地說余蟹,就是本狀態(tài)下要做的事情卷胯,以及本狀態(tài)如何過渡到其他狀態(tài)。
  • Context——環(huán)境角色:定義客戶端需要的接口威酒,并且負責具體狀態(tài)的切換窑睁。

3、狀態(tài)模式的優(yōu)點

  • 結(jié)構(gòu)清晰:避免了過多的switch...case或者if...else語句的使用葵孤,避免了程序的復(fù)雜性,提高系統(tǒng)的可維護性担钮。
  • 遵循設(shè)計原則:很好地體現(xiàn)了開閉原則和單一職責原則,每個狀態(tài)都是一個子類佛呻,你要增加狀態(tài)就要增加子類裳朋,你要修改狀態(tài),你只修改一個子類就可以了吓著。
  • 封裝性非常好:這也是狀態(tài)模式的基本要求鲤嫡,狀態(tài)變換放置到類的內(nèi)部來實現(xiàn),外部的調(diào)用不用知道類內(nèi)部如何實現(xiàn)狀態(tài)和行為的變換绑莺。

4暖眼、狀態(tài)模式的缺點

子類會太多,也就是類膨脹纺裁。如果一個事物有很多個狀態(tài)也不稀奇诫肠,如果完全使用狀態(tài)模式就會有太多的子類司澎,不好管理,這個需要大家在項目中自己衡量栋豫。

5挤安、狀態(tài)模式的使用場景

  • 行為隨狀態(tài)改變而改變的場景
    這也是狀態(tài)模式的根本出發(fā)點,例如權(quán)限設(shè)計丧鸯,人員的狀態(tài)不同即使執(zhí)行相同的行為結(jié)果也會不同蛤铜,在這種情況下需要考慮使用狀態(tài)模式。

  • 條件丛肢、分支判斷語句的替代者
    在程序中大量使用switch語句或者if判斷語句會導致程序結(jié)構(gòu)不清晰围肥,邏輯混亂,使用狀態(tài)模式可以很好地避免這一問題蜂怎,它通過擴展子類實現(xiàn)了條件的判斷處理穆刻。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市杠步,隨后出現(xiàn)的幾起案子氢伟,更是在濱河造成了極大的恐慌,老刑警劉巖篮愉,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腐芍,死亡現(xiàn)場離奇詭異,居然都是意外死亡试躏,警方通過查閱死者的電腦和手機猪勇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來颠蕴,“玉大人泣刹,你說我怎么就攤上這事∠唬” “怎么了椅您?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長寡键。 經(jīng)常有香客問我掀泳,道長,這世上最難降的妖魔是什么西轩? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任员舵,我火速辦了婚禮,結(jié)果婚禮上藕畔,老公的妹妹穿的比我還像新娘马僻。我一直安慰自己,他們只是感情好注服,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布韭邓。 她就那樣靜靜地躺著措近,像睡著了一般。 火紅的嫁衣襯著肌膚如雪女淑。 梳的紋絲不亂的頭發(fā)上瞭郑,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天,我揣著相機與錄音鸭你,去河邊找鬼凰浮。 笑死,一個胖子當著我的面吹牛苇本,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菜拓,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瓣窄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纳鼎?” 一聲冷哼從身側(cè)響起俺夕,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贱鄙,沒想到半個月后劝贸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡逗宁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年映九,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞎颗。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡件甥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哼拔,到底是詐尸還是另有隱情引有,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布倦逐,位于F島的核電站譬正,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檬姥。R本人自食惡果不足惜曾我,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望穿铆。 院中可真熱鬧您单,春花似錦、人聲如沸荞雏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至悦陋,卻和暖如春蜈彼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俺驶。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工幸逆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人暮现。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓还绘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親栖袋。 傳聞我的和親對象是個殘疾皇子拍顷,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359