在實際項目中,應用程序往往需要根據不同的情況做出不同的處理。在開發工程中,需要考慮到各種場景、分支,常常會使用到if..else或者switch case等分支,通過判斷條件處理不同的情況。當這種判斷變得複雜的時候,分支增多,代碼量增多,對代碼的維護和可讀性、擴展性帶來了不好的影響。這種時候就可以考慮使用狀態模式。
狀態模式:
允許一個對象在其內部狀態改變時改變它的行爲。對象看起來似乎修改了它的類。
在很多情況下,一個對象的行爲取決於一個或多個動態變化的屬性,這樣的屬性叫做狀態,這樣的對象叫做有狀態的(stateful)對象,這樣的對象狀態是從事先定義好的一系列值中取出的。當一個這樣的對象與外部事件產生互動時,其內部狀態就會改變,從而使得系統的行爲也隨之發生變化。
例子1:按鈕來控制一個電梯的狀態,一個電梯開們,關門,停,運行。每一種狀態改變,都有可能要根據其他狀態來更新處理。例如,開門狀體,你不能在運行的時候開門,而是在電梯定下後才能開門。
例子2:我們給一部手機打電話,就可能出現這幾種情況:用戶開機,用戶關機,用戶欠費停機,用戶消戶等。 所以當我們撥打這個號碼的時候:系統就要判斷,該用戶是否在開機且不忙狀態,又或者是關機,欠費等狀態。但不管是那種狀態我們都應給出對應的處理操作。
適用場景
1.一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變它的行爲。
2.一個操作中含有龐大的多分支結構,並且這些分支決定於對象的狀態。
角色說明
1.環境類(Context): 定義客戶感興趣的接口。維護一個ConcreteState子類的實例,這個實例定義當前狀態。
2.抽象狀態類(State): 定義一個接口以封裝與Context的一個特定狀態相關的行爲。
3.具體狀態類(ConcreteState): 每一子類實現一個與Context的一個狀態相關的行爲。
類圖
代碼示例
狀態模式最出名的莫過於狀態機了,這裏用代碼模擬一個娃娃機(Context)。簡單模擬一下娃娃機的狀態分別爲:空倉,等待投幣,操作中三個具體狀態(ConcreteStats);用戶對娃娃機的行爲包括:補貨,投幣,搖桿操作,抓娃娃操作。
先寫一段僞代碼,看看不用狀態模式時是怎麼寫的
switch (事件){ case 投幣: if(等待狀態){ //可以投幣 }else if(操作中狀態){ //不能投幣 }else { //空倉狀態,不能投幣 } return; case 搖桿: //略………… case 抓娃娃: //略………… case 補貨: //略………… }爲了縮短代碼長度,寫得比較粗糙,不過大意就是:不同的事件需要判斷娃娃機當前的狀態,然後根據狀態做出相應的響應。如果這段代碼實實在在的都寫完寫好了,相比是一大段判斷語句,一大段代碼,可讀性、擴展性都不太好。
下面我們試着使用狀態模式完成娃娃機
Stats:
/** * 定義了4個娃娃機的行爲 * Created by yangjiachang on 2016/9/12. */ public abstract class Stats { //娃娃機實例 protected StatsMachine machine; /** * 投幣 */ public abstract void insertCoins(); /** * 搖桿 */ public abstract void rock(); /** * 抓娃娃 */ public abstract void get(); /** * 補貨 */ public abstract void supplement(int count); }
ConcreteStats:
/** * 空倉狀態,沒有娃娃了 * Created by yangjiachang on 2016/9/12. */ public class EmptyStats extends Stats { public EmptyStats(StatsMachine machine){ this.machine = machine; } public EmptyStats(){ } public void insertCoins() { System.out.println("沒有娃娃了,不能投幣,錢退還給你"); } public void rock() { System.out.println("搖桿無效"); } public void get() { System.out.println("不能抓娃娃"); } public void supplement(int count) { machine.setCount(machine.getCount() + count); machine.setStats(machine.getWaitStats()); System.out.println("補貨完成,又有" + machine.getCount()+"個娃娃了"); } }
/** * 操作搖桿狀態,抓娃娃 * Created by yangjiachang on 2016/9/12. */ public class OperateStats extends Stats { public OperateStats(StatsMachine machine){ this.machine = machine; } public OperateStats(){ } public void insertCoins() { System.out.println("已經投過錢幣了,不要再投了,多的錢退給你"); } /** * 搖桿 */ public void rock() { System.out.println("尋找一個最好的娃娃"); } /** * 抓娃娃 */ public void get() { System.out.println("正在抓取娃娃……"); int count = machine.getCount()-1; if(count > 0){ machine.setCount(count); machine.setStats(machine.getWaitStats()); }else { machine.setCount(count); machine.setStats(machine.getEmptyStats()); } System.out.println("抓到娃娃,剩餘:"+machine.getCount()); } public void supplement(int count) { System.out.println("用戶正在操作,請稍後補貨"); } }
/** * 等待投幣狀態 * Created by yangjiachang on 2016/9/12. */ public class WaitStats extends Stats { public WaitStats(StatsMachine machine){ this.machine = machine; } public WaitStats(){} public void insertCoins() { System.out.println("投幣成功,你可以開始抓娃娃了"); machine.setStats(machine.getOperateStats()); } public void rock() { System.out.println("搖桿無效,請先投幣"); } public void get() { System.out.println("不能抓娃娃,請先投幣"); } public void supplement(int count) { machine.setCount(machine.getCount() + count); System.out.println("補貨完成,又有" + machine.getCount()+"個娃娃了"); } }Context:
/** * 定義一個狀態機,這裏就是娃娃機 * Created by yangjiachang on 2016/9/12. */ public class StatsMachine { //娃娃機存在的三個狀態 private Stats emptyStats = new EmptyStats(this); private Stats waitStats = new WaitStats(this); private Stats operateStats = new OperateStats(this); //記錄當前狀態 private Stats stats; //娃娃數量 private int count; //初始化 public StatsMachine(int count){ if (count <= 0){ this.count = 0; this.stats = emptyStats; }else { this.count = count; this.stats = waitStats; } } /* 行爲start */ public void insertCoins(){ stats.insertCoins(); } public void rock(){ stats.rock(); } public void get(){ stats.get(); } public void supplement(int count){ stats.supplement(count); } /* 行爲end */ //setting getting public Stats getStats() { return stats; } public void setStats(Stats stats) { this.stats = stats; } public Stats getEmptyStats() { return emptyStats; } public Stats getWaitStats() { return waitStats; } public Stats getOperateStats() { return operateStats; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } }測試方法:
public static void main(String[] args) { int count = 2; StatsMachine machine = new StatsMachine(count); for(int i=0 ; i<count+1 ; i++){ System.out.println("當前狀態:"+machine.getStats().getClass().getSimpleName()); machine.insertCoins(); System.out.println("當前狀態:" + machine.getStats().getClass().getSimpleName()); machine.rock(); System.out.println("當前狀態:" + machine.getStats().getClass().getSimpleName()); machine.get(); System.out.println(); } machine.supplement(3); System.out.println("剩餘數量:" + machine.getCount()); }
執行結果:
當前狀態:WaitStats
投幣成功,你可以開始抓娃娃了
當前狀態:OperateStats
尋找一個最好的娃娃
當前狀態:OperateStats
正在抓取娃娃……
抓到娃娃,剩餘:1
當前狀態:WaitStats
投幣成功,你可以開始抓娃娃了
當前狀態:OperateStats
尋找一個最好的娃娃
當前狀態:OperateStats
正在抓取娃娃……
抓到娃娃,剩餘:0
當前狀態:EmptyStats
沒有娃娃了,不能投幣,錢退還給你
當前狀態:EmptyStats
搖桿無效
當前狀態:EmptyStats
不能抓娃娃
補貨完成,又有3個娃娃了
剩餘數量:3
對狀態模式的理解:
1.它將與特定狀態相關的行爲局部化,並且將不同狀態的行爲分割開來
將狀態封裝成對象,由抽象狀態類定義所有的行爲;將Context的每種狀態定義成一個具體狀態類,並實現該狀態下各種行爲的實現。所以通過定義新的子類很容易擴展新的狀態和轉移狀態(例如投幣行爲將娃娃機由等待投幣狀態變爲可操作狀態),並實現該新狀態下的行爲。這種方式避免了過多的if..else或者switch case語句帶來的分支代碼,並將各自狀態的邏輯封裝到一個具體狀態類中,對於理解該狀態下什麼能做、做了什麼提供了更好的可讀性。
2.顯示的轉換狀態,增強可讀性
實際應用中,狀態的判斷往往的多個變量的值的判斷,在換貨狀態時,需要對多個變量賦值,這樣的方式加深了理解的難道。通過狀態模式,將狀態封裝在一個類中,在判斷與轉換的過程中,更加直截了當
3.狀態可以共享
如果有必要,可以多個Context共享同一個Stats狀態,也可以減少
4.將狀態封裝成對象,勢必會增加系統類和對象個數,運用不當反而會顯得結構和代碼混亂