作者:zuoxiaolong8810(左瀟龍),轉載請注明出處,特別說明:本博文來自博主原博客,為保證新博客中博文的完整性,特復制到此留存,如需轉載請注明新博客地址即可。
本次LZ給各位介紹狀態模式,之前在寫設計模式的時候,引入了一些小故事,二十章職責連模式是故事版的最后一篇,之后還剩余四個設計模式,LZ會依照原生的方式去解釋這幾個設計模式,特別是原型模式和解釋器模式,會包含一些其它的內容。
好了,接下來,我們先來看看狀態模式的定義吧。
定義:(源於Design Pattern):當一個對象的內在狀態改變時允許改變其行為,這個對象看起來像是改變了其類。
上述是百度百科中對狀態模式的定義,定義很簡單,只有一句話,請各位形象的去理解這句話,它說當狀態改變時,這個對象的行為也會變,而看起來就像是這個類改變了一樣。
這正是應驗了我們那句話,有些人一旦發生過什么事以后,就像變了個人似的,這句話其實與狀態模式有異曲同工之妙。
我們仔細體會一下定義當中的要點。
1、有一個對象,它是有狀態的。
2、這個對象在狀態不同的時候,行為不一樣。
3、這些狀態是可以切換的,而非毫無關系。
前兩點比較好理解,第3點有時候容易給人比較迷惑的感覺,什么叫這些狀態是可以切換的,而非毫無關系?
舉個例子,比如一個人的狀態,可以有很多,像生病和健康,這是兩個狀態,這是有關系並且可以轉換的兩個狀態。再比如,睡覺、上班、休息,這也算是一組狀態,這三個狀態也是有關系的並且可以互相轉換。
那如果把生病和休息這兩個狀態放在一起,就顯得毫無意義了。所以這些狀態應該是一組相關並且可互相切換的狀態。
下面我們來看看狀態模式的類圖。
類圖中包含三個角色。
Context:它就是那個含有狀態的對象,它可以處理一些請求,這些請求最終產生的響應會與狀態相關。
State:狀態接口,它定義了每一個狀態的行為集合,這些行為會在Context中得以使用。
ConcreteState:具體狀態,實現相關行為的具體狀態類。
如果針對剛才對於人的狀態的例子來分析,那么人(Person)就是Context,狀態接口依然是狀態接口,而具體的狀態類,則可以是睡覺,上班,休息,這一系列狀態。
不過LZ也看過不少狀態模式的文章和帖子,包括《大話設計模式》當中,都舉的是有關人的狀態的例子,所以這里給大家換個口味,我們換一個例子。
我們來試着寫一個DOTA的例子,最近貌似跟DOTA干上了,不為其他,就因為DOTA伴隨了LZ四年的大學時光。
玩過的朋友都知道,DOTA里的英雄有很多狀態,比如正常,眩暈,加速,減速等等。相信就算沒有玩過DOTA的朋友們,在其它游戲里也能見到類似的情況。那么假設我們的DOTA沒有使用狀態模式,則我們的英雄類會非常復雜和難以維護,我們來看下,原始版的英雄類是怎樣的。
package com.state; //英雄類 public class Hero { public static final int COMMON = 1;//正常狀態 public static final int SPEED_UP = 2;//加速狀態 public static final int SPEED_DOWN = 3;//減速狀態 public static final int SWIM = 4;//眩暈狀態 private int state = COMMON;//默認是正常狀態 private Thread runThread;//跑動線程 //設置狀態 public void setState(int state) { this.state = state; } //停止跑動 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑動---------------"); } //開始跑動 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { try { hero.run(); } catch (InterruptedException e) { break; } } } }); System.out.println("--------------開始跑動---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } //英雄類開始奔跑 private void run() throws InterruptedException{ if (state == SPEED_UP) { System.out.println("--------------加速跑動---------------"); Thread.sleep(4000);//假設加速持續4秒 state = COMMON; System.out.println("------加速狀態結束,變為正常狀態------"); }else if (state == SPEED_DOWN) { System.out.println("--------------減速跑動---------------"); Thread.sleep(4000);//假設減速持續4秒 state = COMMON; System.out.println("------減速狀態結束,變為正常狀態------"); }else if (state == SWIM) { System.out.println("--------------不能跑動---------------"); Thread.sleep(2000);//假設眩暈持續2秒 state = COMMON; System.out.println("------眩暈狀態結束,變為正常狀態------"); }else { //正常跑動則不打印內容,否則會刷屏 } } }
下面我們寫一個客戶端類,去試圖讓英雄在各種狀態下奔跑一下。
package com.state; public class Main { public static void main(String[] args) throws InterruptedException { Hero hero = new Hero(); hero.startRun(); hero.setState(Hero.SPEED_UP); Thread.sleep(5000); hero.setState(Hero.SPEED_DOWN); Thread.sleep(5000); hero.setState(Hero.SWIM); Thread.sleep(5000); hero.stopRun(); } }
可以看到,我們的英雄在跑動過程中隨着狀態的改變,會以不同的狀態進行跑動。
在上面原始的例子當中,我們的英雄類當中有明顯的if else結構,我們再來看看百度百科中狀態模式所解決的問題的描述。
狀態模式解決的問題:狀態模式主要解決的是當控制一個對象狀態的條件表達式過於復雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類中,可以把復雜的判斷邏輯簡化。
不用說,狀態模式是可以解決我們上面的if else結構的,我們采用狀態模式,利用多態的特性可以消除掉if else結構。這樣所帶來的好處就是可以大大的增加程序的可維護性與擴展性。
下面我們就使用狀態模式對上面的例子進行改善,首先第一步,就是我們需要定義一個狀態接口,這個接口就只有一個方法,就是run。
package com.state; public interface RunState { void run(Hero hero); }
與狀態模式類圖不同的是,我們加入了一個參數Hero(Context),這樣做的目的是為了具體的狀態類當達到某一個條件的時候可以切換上下文的狀態。下面列出四個具體的狀態類,其實就是把if else拆掉放到這幾個類的run方法中。
package com.state; public class CommonState implements RunState{ public void run(Hero hero) { //正常跑動則不打印內容,否則會刷屏 } }
package com.state; public class SpeedUpState implements RunState{ public void run(Hero hero) { System.out.println("--------------加速跑動---------------"); try { Thread.sleep(4000);//假設加速持續4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------加速狀態結束,變為正常狀態------"); } }
package com.state; public class SpeedDownState implements RunState{ public void run(Hero hero) { System.out.println("--------------減速跑動---------------"); try { Thread.sleep(4000);//假設減速持續4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------減速狀態結束,變為正常狀態------"); } }
package com.state; public class SwimState implements RunState{ public void run(Hero hero) { System.out.println("--------------不能跑動---------------"); try { Thread.sleep(2000);//假設眩暈持續2秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------眩暈狀態結束,變為正常狀態------"); } }
這下我們的英雄類也要相應的改動一下,最主要的改動就是那些if else可以刪掉了,如下。
package com.state; //英雄類 public class Hero { public static final RunState COMMON = new CommonState();//正常狀態 public static final RunState SPEED_UP = new SpeedUpState();//加速狀態 public static final RunState SPEED_DOWN = new SpeedDownState();//減速狀態 public static final RunState SWIM = new SwimState();//眩暈狀態 private RunState state = COMMON;//默認是正常狀態 private Thread runThread;//跑動線程 //設置狀態 public void setState(RunState state) { this.state = state; } //停止跑動 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑動---------------"); } //開始跑動 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { state.run(hero); } } }); System.out.println("--------------開始跑動---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } }
可以看到,現在我們的英雄類優雅了許多,我們使用剛才同樣的客戶端運行即可得到同樣的結果。
對比我們的原始例子,現在我們使用狀態模式之后,有幾個明顯的優點:
一、我們去掉了if else結構,使得代碼的可維護性更強,不易出錯,這個優點挺明顯,如果試圖讓你更改跑動的方法,是剛才的一堆if else好改,還是分成了若干個具體的狀態類好改呢?答案是顯而易見的。
二、使用多態代替了條件判斷,這樣我們代碼的擴展性更強,比如要增加一些狀態,假設有加速20%,加速10%,減速10%等等等(這並不是虛構,DOTA當中是真實存在這些狀態的),會非常的容易。
三、狀態是可以被共享的,這個在上面的例子當中有體現,看下Hero類當中的四個static final變量就知道了,因為狀態類一般是沒有自己的內部狀態的,所有它只是一個具有行為的對象,因此是可以被共享的。
四、狀態的轉換更加簡單安全,簡單體現在狀態的分割,因為我們把一堆if else分割成了若干個代碼段分別放在幾個具體的狀態類當中,所以轉換起來當然更簡單,而且每次轉換的時候我們只需要關注一個固定的狀態到其他狀態的轉換。安全體現在類型安全,我們設置上下文的狀態時,必須是狀態接口的實現類,而不是原本的一個整數,這可以杜絕魔數以及不正確的狀態碼。
狀態模式適用於某一個對象的行為取決於該對象的狀態,並且該對象的狀態會在運行時轉換,又或者有很多的if else判斷,而這些判斷只是因為狀態不同而不斷的切換行為。
上面的適用場景是很多狀態模式的介紹中都提到的,下面我們就來看下剛才DOTA中,英雄例子的類圖。
可以看到,這個類圖與狀態模式的標准類圖是幾乎一模一樣的,只是多了一條狀態接口到上下文的依賴線,而這個是根據實際需要添加的,而且一般情況下都是需要的。
狀態模式也有它的缺點,不過它的缺點和大多數模式相似,有兩點。
1、會增加的類的數量。
2、使系統的復雜性增加。
盡管狀態模式有着這樣的缺點,但是往往我們犧牲復雜性去換取的高可維護性和擴展性是相當值得的,除非增加了復雜性以后,對於后者的提升會乎其微。
狀態模式在項目當中也算是較經常會碰到的一個設計模式,但是通常情況下,我們還是在看到if else的情況下,對項目進行重構時使用,又或者你十分確定要做的項目會朝着狀態模式發展,一般情況下,還是不建議在項目的初期使用。
好了,本次狀態模式的分享就到此結束了,希望各位有所收獲。