設(shè)計模式之——狀態(tài)模式與策略模式

如果你的簡歷里出現(xiàn)了"設(shè)計模式"的字樣,那么作為面試官的我?guī)缀醵紩柕揭粋€問題: "狀態(tài)模式與策略模式有哪些區(qū)別"鬓照。很多人一臉懵,我就知道這次愉快的技術(shù)交流無疾而終了孤紧〔蝰桑可能對于很多人來說,策略模式比較熟悉号显,可什么是狀態(tài)模式留储,好多人還是比較迷糊的翼抠。此篇專題咙轩,我們就來聊聊狀態(tài)模式與策略模式获讳。

第一部分 狀態(tài)模式

考慮這樣的一個場景:一個電梯,有四種操作:運(yùn)行活喊、停止丐膝、開門關(guān)門钾菊。每一種操作成功后帅矗,都對應(yīng)著狀態(tài)的切換。每一種狀態(tài)煞烫,又可以隨著操作浑此,向另一種狀態(tài)切換。但是狀態(tài)與狀態(tài)之間又不是隨意切換的滞详。如下表所示:

  • 運(yùn)行狀態(tài)

    • 可以向停止?fàn)顟B(tài)切換
    • 不能再次切換到運(yùn)行狀態(tài)
    • 不能在電梯的運(yùn)行過程中開門
    • 不能在電梯的運(yùn)行過程中關(guān)門 - 因為運(yùn)行的過程中凛俱,電梯的門必然是關(guān)的
  • 停止?fàn)顟B(tài)

    • 可以向運(yùn)行狀態(tài)切換
    • 可以向開門狀態(tài)切換
    • 可以向關(guān)門狀態(tài)切換
    • 不能再次切換到停止?fàn)顟B(tài)
  • 開門狀態(tài)

    • 可以向關(guān)門狀態(tài)切換
    • 不能再次切換到開門狀態(tài)
    • 不能在開門的狀態(tài)下運(yùn)行
    • 不能在開門的狀態(tài)下停止 - 因為開門狀態(tài)下,電梯的狀態(tài)必然是停止的
  • 關(guān)門狀態(tài)

    • 可以向運(yùn)行狀態(tài)切換
    • 可以向開門狀態(tài)切換
    • 不能向停止?fàn)顟B(tài)切換 - 因為在關(guān)門狀態(tài)下料饥,電梯必然是停止的
    • 不能再次切換到關(guān)門狀態(tài)

說得比較復(fù)雜蒲犬,看一個狀態(tài)機(jī)圖

有箭頭的,就是允許岸啡;沒有箭頭的原叮,就不允許

Lift-State.png

1 錯誤示范

if - else真是個害人精,它讓我們在實現(xiàn)功能的時候巡蘸,不必過多地思考奋隶,很多沒有研習(xí)過狀態(tài)模式的同學(xué)也是可以輕松實現(xiàn)的——只不過沒那么優(yōu)美罷了。

  • 新建一個枚舉悦荒,列出四種狀態(tài)
  • 電梯有4個方法
  • 電梯有1種狀態(tài)
Lift-Design.png
  • LiftState.java

    package com.futureweaver.enums;
    
    // 電梯狀態(tài)
    public enum LiftState {
        // 開門狀態(tài)
        Opening,
    
        // 關(guān)門狀態(tài)
        Closed,
    
        // 運(yùn)行狀態(tài)
        Running,
    
        // 停止?fàn)顟B(tài)
        Stoped
    }
    
  • Lift.java

    package com.futureweaver.domain;
    
    import com.futureweaver.enums.LiftState;
    
    // 電梯
    public class Lift {
        private LiftState state = LiftState.Closed;
    
        // 開門
        public void open() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 無法重復(fù)開門");
            } else if (state == LiftState.Closed) {
                state = LiftState.Opening;
                System.out.println("success: 開門");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 運(yùn)行狀態(tài)下不能開門");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Opening;
                System.out.println("success: 開門");
            }
        }
    
        // 關(guān)門
        public void close() {
            if (state == LiftState.Opening) {
                state = LiftState.Closed;
                System.out.println("success: 關(guān)門");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 無法重復(fù)關(guān)門");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 運(yùn)行狀態(tài)下唯欣,就一定是關(guān)門狀態(tài)了");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 停止后就是關(guān)門狀態(tài)了");
            }
        }
    
        // 運(yùn)行
        public void run() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 電梯門沒關(guān),不能運(yùn)行");
            } else if (state == LiftState.Closed) {
                state = LiftState.Running;
                System.out.println("success: 運(yùn)行");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 無法重復(fù)運(yùn)行");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Running;
                System.out.println("success: 運(yùn)行");
            }
        }
    
        // 停止
        public void stop() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 開門狀態(tài)下不會運(yùn)行逾冬,自然也不需要停止");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 關(guān)門狀態(tài)下不會運(yùn)行黍聂,自然也不需要停止");
            } else if (state == LiftState.Running) {
                state = LiftState.Stoped;
                System.out.println("success: 停止");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 無法重復(fù)停止");
            }
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 輸出

    failure: 無法重復(fù)關(guān)門
    failure: 無法重復(fù)關(guān)門
    success: 開門
    failure: 電梯門沒關(guān),不能運(yùn)行
    failure: 無法重復(fù)開門
    failure: 開門狀態(tài)下不會運(yùn)行身腻,自然也不需要停止
    

結(jié)論

從測試的結(jié)果可以看出产还,需求實現(xiàn)了,也沒什么問題嘀趟。但這是我們編寫的簡單代碼脐区,回過頭再來審視一下Lift.java,我們做了大量的條件判斷她按。同一個類當(dāng)中的代碼量又太多——如果狀態(tài)不止4種牛隅,怎么辦炕柔?如果狀態(tài)與狀態(tài)之間的切換,業(yè)務(wù)比較復(fù)雜媒佣,不能一兩條代碼就搞得定匕累,又怎么辦?

2 正確示范

在現(xiàn)實領(lǐng)域中默伍,電梯狀態(tài)欢嘿,自然而然就是電梯的一個屬性。然而在面向?qū)ο笳Z言中也糊,所謂萬物皆對象炼蹦,狀態(tài),自然也可以作為一個對象狸剃。既然狀態(tài)可以作為對象掐隐,那么就可以利用多態(tài)來解決了。

  • 新建一個電梯狀態(tài)的抽象類钞馁,定義4個操作: 打開虑省、關(guān)閉停止指攒、運(yùn)行
  • 新建四個電梯狀態(tài)的子類
  • 每個實際狀態(tài)慷妙,自己判斷能否向目標(biāo)狀態(tài)切換。如果能切換的話允悦,創(chuàng)建目標(biāo)狀態(tài)對象膝擂,并向電梯發(fā)送修改狀態(tài)的請求
Lift-Design-State-Pattern.png
  • Lift.java

    package com.futureweaver.domain;
    
    // 電梯
    public class Lift {
        private LiftState state = new ClosedState();
    
        public LiftState getState() {
            return state;
        }
    
        public void setState(LiftState state) {
            this.state = state;
        }
    
        // 開門
        public void open() {
            // 由狀態(tài)對象自己處理切換行為
            state.open(this);
        }
    
        // 關(guān)門
        public void close() {
            // 由狀態(tài)對象自己處理切換行為
            state.close(this);
        }
    
        // 運(yùn)行
        public void run() {
            // 由狀態(tài)對象自己處理切換行為
            state.run(this);
        }
    
        // 停止
        public void stop() {
            // 由狀態(tài)對象自己處理切換行為
            state.stop(this);
        }
    }
    
  • LiftState.java

    package com.futureweaver.domain;
    
    // 電梯狀態(tài)
    public abstract class LiftState {
    
        // 電梯狀態(tài)的共同父類,無論向哪一個狀態(tài)切換都可以隙弛,子類自己覆蓋要阻止的操作
    
        public void open(Lift lift) {
            // 如果成功的話架馋,直接修改電梯的"狀態(tài)"屬性
            lift.setState(new OpeningState());
            System.out.println("success: 開門");
        }
    
        public void close(Lift lift) {
            // 如果成功的話,直接修改電梯的"狀態(tài)"屬性
            lift.setState(new ClosedState());
            System.out.println("success: 關(guān)門");
        }
    
        public void stop(Lift lift) {
            // 如果成功的話全闷,直接修改電梯的"狀態(tài)"屬性
            lift.setState(new StopedState());
            System.out.println("success: 停止");
        }
    
        public void run(Lift lift) {
            // 如果成功的話叉寂,直接修改電梯的"狀態(tài)"屬性
            lift.setState(new RunningState());
            System.out.println("success: 運(yùn)行");
        }
    }
    
  • OpeningState.java

    package com.futureweaver.domain;
    
    public class OpeningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 無法重復(fù)開門");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 開門狀態(tài)下不會運(yùn)行,自然也不需要停止");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 電梯門沒關(guān)总珠,不能運(yùn)行");
        }
    
    }
    
  • ClosedState.java

    package com.futureweaver.domain;
    
    public class ClosedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 無法重復(fù)關(guān)門");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 關(guān)門狀態(tài)下不會運(yùn)行屏鳍,自然也不需要停止");
        }
    }
    
  • StopedState.java

    package com.futureweaver.domain;
    
    public class StopedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 停止后就是關(guān)門狀態(tài)了");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 無法重復(fù)停止");
        }
    }
    
  • RunningState.java

    package com.futureweaver.domain;
    
    public class RunningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 運(yùn)行狀態(tài)下不能開門");
        }
    
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 運(yùn)行狀態(tài)下,就一定是關(guān)門狀態(tài)了");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 無法重復(fù)運(yùn)行");
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 輸出

    failure: 無法重復(fù)關(guān)閉
    failure: 無法重復(fù)關(guān)閉
    success: 開門
    failure: 電梯門沒關(guān)局服,不能運(yùn)行
    failure: 無法重復(fù)開門
    failure: 開門狀態(tài)下不會運(yùn)行钓瞭,自然也不需要停止
    

結(jié)論

這種實現(xiàn)方式,電梯與電梯狀態(tài)產(chǎn)生了雙向依賴淫奔,屬于一種緊耦合山涡;電梯狀態(tài)抽象父類,與電梯狀態(tài)具體類又產(chǎn)生了雙向依賴,屬于一種緊耦合鸭丛。理解起來有點繞竞穷,一條一條地說。

  • 電梯與電梯狀態(tài)的通信

電梯具備一個電梯狀態(tài)對象鳞溉,當(dāng)接收到操作請求時瘾带,電梯對象本身不做任何的判斷和處理,而是交由狀態(tài)對象處理穿挨。

當(dāng)電梯狀態(tài)接收到轉(zhuǎn)換請求時月弛,如果可以轉(zhuǎn)換,那么我們想要得到的結(jié)果是:電梯的狀態(tài)發(fā)生了變化科盛。所以電梯狀態(tài)需要向電梯對象發(fā)送"改變狀態(tài)"的消息,那么電梯狀態(tài)就需要知道菜皂,到底是哪一個電梯對象贞绵。所以電梯狀態(tài)的轉(zhuǎn)換操作,需要接收一個電梯對象的參數(shù)恍飘。

  • 抽象狀態(tài)與具體狀態(tài)的通信

具體狀態(tài)需要繼承自抽象狀態(tài)榨崩。

抽象狀態(tài)定義了默認(rèn)方法,其中的默認(rèn)方法就是: 所有的操作章母,都是合法的母蛛。既然是合法的,就要向目標(biāo)狀態(tài)切換乳怎。因為使用了狀態(tài)模式彩郊,狀態(tài)是一個對象了,所以需要創(chuàng)建具體狀態(tài)的對象蚪缀,再把創(chuàng)建好的狀態(tài)對象秫逝,發(fā)送給電梯。

  • 說得比較復(fù)雜询枚,結(jié)合看一下類圖和序列圖

    • 類圖
Lift-Design-State-Pattern.png
  • 序列圖
Lift-Sequence.png

3 狀態(tài)模式總結(jié)

State-Pattern.png
  • 狀態(tài)模式

    允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為违帆。對象看起來似乎修改了它的類。

    通過內(nèi)聚金蜀,提升了靈活性刷后、可維護(hù)性和可擴(kuò)展性。

第二部分 狀態(tài)模式與策略模式

在上一篇文章《設(shè)計模式在RESTful當(dāng)中的應(yīng)用》當(dāng)中渊抄,已經(jīng)聊過策略模式尝胆,乍一看它們的類圖,是很相似的:

  • 狀態(tài)模式
State-Pattern.png
  • 策略模式
Strategy-Pattern.png

策略模式與狀態(tài)模式都把實際的行為抒线,延遲到了子類班巩,以此完成多態(tài)。同時,上下文(Context)面向的都是一個抽象類抱慌。(一些編程語言明確地區(qū)分了接口與抽象類逊桦,比如Java;而一些編程語言并沒有明確地區(qū)分抑进,比如C++强经。OOD本身與語言的關(guān)聯(lián)是比較弱的,所以在OOP的時候寺渗,到底是面向接口還是抽象類匿情,是需要酌情考慮的。)

狀態(tài)模式與策略模式的區(qū)別

類結(jié)構(gòu)相似信殊,想要找出狀態(tài)模式與策略模式的區(qū)別炬称,就需要從它們的行為入手了

  • 策略模式

    在程序運(yùn)行的過程中,策略與策略之間涡拘,是相互獨(dú)立的玲躯,從而耦合度是比較松的。因為本身策略中封裝的是一系列可以相互替換的算法鳄乏,每一個策略是可以獨(dú)立完成自己所要完成的工作的跷车。

    上下文(Context)依賴于策略,而策略不依賴于上下文橱野。因為策略在工作時朽缴,并不關(guān)心這個信息是誰發(fā)送過來的。

  • 狀態(tài)模式: 允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為水援。對象看起來似乎修改了它的類密强。

    在程序運(yùn)行的過程中,狀態(tài)與狀態(tài)之間裹唆。比如從A狀態(tài)誓斥,過渡到B狀態(tài),A狀態(tài)是需要獲取一個B狀態(tài)(至于到底怎么獲取许帐,創(chuàng)建也好劳坑,使用注冊表也好,使用享元也好成畦,這個就要看具體業(yè)務(wù)了)的狀態(tài)對象的距芬。所以狀態(tài)與狀態(tài)之間是互相依賴的,耦合度是比較緊的循帐。

    上下文(Context)依賴于狀態(tài)框仔,而狀態(tài)又依賴于上下文。比如從A狀態(tài)拄养,過渡到B狀態(tài)离斩,A狀態(tài)先獲取一個B狀態(tài)银舱,之后要找到上下文,把上下文的狀態(tài)給修改掉跛梗。所以上下文下狀態(tài)之間是互相依賴的寻馏,耦合度也是比較緊的。

第三部分 總結(jié)

綜上所述核偿,策略模式實現(xiàn)起來比較簡單诚欠,是真正利用了面向?qū)ο蟮亩鄳B(tài)技術(shù),完成了算法的互換使用漾岳,并且既遵循了高內(nèi)聚轰绵,又遵循了松耦合的設(shè)計原則。

而狀態(tài)模式實現(xiàn)起來比較復(fù)雜尼荆,其亦是利用了面向?qū)ο蟮亩鄳B(tài)技術(shù)左腔,完成狀態(tài)與狀態(tài)之間的過渡。雖然狀態(tài)模式遵循了高內(nèi)聚的設(shè)計原則耀找,但卻破壞了松耦合原則翔悠。

兩者都是通過內(nèi)聚,提升了靈活性野芒,可維護(hù)性可擴(kuò)展性。但歸根結(jié)底双炕,兩者的區(qū)別就在于:策略模式是松耦合狞悲、狀態(tài)模式是緊耦合

打完收工

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妇斤,一起剝皮案震驚了整個濱河市摇锋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌站超,老刑警劉巖荸恕,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異死相,居然都是意外死亡融求,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進(jìn)店門算撮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來生宛,“玉大人,你說我怎么就攤上這事肮柜∠菥耍” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵审洞,是天一觀的道長莱睁。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么仰剿? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任创淡,我火速辦了婚禮,結(jié)果婚禮上酥馍,老公的妹妹穿的比我還像新娘辩昆。我一直安慰自己,他們只是感情好旨袒,可當(dāng)我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布汁针。 她就那樣靜靜地躺著,像睡著了一般砚尽。 火紅的嫁衣襯著肌膚如雪施无。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天必孤,我揣著相機(jī)與錄音猾骡,去河邊找鬼。 笑死敷搪,一個胖子當(dāng)著我的面吹牛兴想,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赡勘,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼嫂便,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闸与?” 一聲冷哼從身側(cè)響起毙替,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎践樱,沒想到半個月后厂画,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拷邢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年袱院,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片解孙。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡坑填,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弛姜,到底是詐尸還是另有隱情脐瑰,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布廷臼,位于F島的核電站苍在,受9級特大地震影響绝页,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寂恬,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一续誉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧初肉,春花似錦酷鸦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妄壶,卻和暖如春摔握,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背丁寄。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工氨淌, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伊磺。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓盛正,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屑埋。 傳聞我的和親對象是個殘疾皇子蛮艰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容