如果你的簡歷里出現(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ī)圖
有箭頭的,就是允許岸啡;沒有箭頭的原叮,就不允許
1 錯誤示范
if
- else
真是個害人精,它讓我們在實現(xiàn)功能的時候巡蘸,不必過多地思考奋隶,很多沒有研習(xí)過狀態(tài)模式的同學(xué)也是可以輕松實現(xiàn)的——只不過沒那么優(yōu)美罷了。
- 新建一個枚舉悦荒,列出四種狀態(tài)
- 電梯有4個方法
- 電梯有1種狀態(tài)
-
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.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é)合看一下類圖和序列圖
- 類圖
- 序列圖
3 狀態(tài)模式總結(jié)
-
狀態(tài)模式
允許一個對象在其內(nèi)部狀態(tài)改變時改變它的行為违帆。對象看起來似乎修改了它的類。
通過內(nèi)聚金蜀,提升了靈活性刷后、可維護(hù)性和可擴(kuò)展性。
第二部分 狀態(tài)模式與策略模式
在上一篇文章《設(shè)計模式在RESTful當(dāng)中的應(yīng)用》當(dāng)中渊抄,已經(jīng)聊過策略模式尝胆,乍一看它們的類圖,是很相似的:
- 狀態(tài)模式
- 策略模式
策略模式與狀態(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)模式是緊耦合
。
打完收工