Simple Demo 假設我有一部iPhoneX,又非常喜歡玩游戲,那麼我這部破手機主要存在兩種狀態:待機和游戲中。 此時手機的狀態圖非常簡單: 將這個狀態圖轉換為代碼: 每一個狀態用不同的整數代表,將每一個動作整合成方法,每一個動作都可能造成狀態的轉換。 測試代碼: 更改需求 但存在一種特殊情況 ...
Simple Demo
假設我有一部iPhoneX,又非常喜歡玩游戲,那麼我這部破手機主要存在兩種狀態:待機和游戲中。
此時手機的狀態圖非常簡單:
將這個狀態圖轉換為代碼:
每一個狀態用不同的整數代表,將每一個動作整合成方法,每一個動作都可能造成狀態的轉換。
public class MyiPhoneX { public final static int STANDBY = 0; // 待機狀態 public final static int PLAYING = 1; // 游戲進行中狀態 private int state = STANDBY; // 持有當前狀態的實例變數 public MyiPhoneX() { } public void startGame() { // 打開游戲 if (state == STANDBY) { System.out.println("Game is loading..."); state = PLAYING; } else if (state == PLAYING) { System.out.println("Game is already in progress!");// 已存在該游戲進程 } } public void exitGame() { // 結束游戲 if (state == STANDBY) { System.out.println("There is no game process!");// 不存在游戲進程,這是此狀態的一個不恰當動作 } else if (state == PLAYING) { System.out.println("Game is exiting..."); state = STANDBY; } } }
測試代碼:
public class MyiPhoneXTest { public static void main(String[] args) { MyiPhoneX iPhoneX = new MyiPhoneX(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); } }
Game is loading... Game is exiting... There is no game process! Game is loading... Game is exiting...
更改需求
但存在一種特殊情況:遇上了某款“渣渣游戲”,其在響應網路方面的功能沒有很完善,我的iPhoneX在載入這款游戲的過程中某個瞬間網路狀況不太好,進入了“游戲無限載入”狀態,最後只能選擇退出游戲後再重新嘗試打開游戲。
下麵是加入“無限載入狀態”的手機狀態圖:
如果在第原版本的代碼進行維護, 需要作如下修改:
可這樣的代碼不易於擴展,我的iPhoneX每新增一個狀態,需要MyiPhoneX類做出的改變地方可能會更多,這樣的設計不符合“對修改關閉,對擴展開放”的原則、也不夠面向對象。
而且這還只是一個只有兩三個狀態的簡單例子,如果是狀態及操作方法較多的程式,按照以上的設計,代碼臃腫混亂,每新增一個狀態,需要在每個操作方法里都新增一個 else if 判斷,所有的代碼維護都堆積在一個類中。
新的設計
為了更容易的維護和修改程式,我們需要對原代碼重新設計:
我們將每個狀態的行為都放在各自的類中(將會改變的那部分封裝起來),那麼每個狀態類只要實現它自己的動作就可以了,對iPhoneX執行的動作只需要委托給當前狀態的狀態對象來具體執行就好了。我們可以這麼做:
① 先定義一個State介面,iPhoneX可以執行的每一個動作都有對應的方法在這個介面內。(面向介面編程)
② iPhoneX的每個狀態都有一個對應的實現了State介面的具體狀態類。
③ 對iPhoneX執行動作委托給具體狀態類來執行。
狀態介面和其具體實現類的類圖如下:
實際代碼:
public interface State { void startGame(); void exitGame(); }
具體狀態實現類主要做的是實現適合所在的這個狀態的具體動作。
public class Standby implements State { //“待機”狀態 private MyiPhoneX iPhoneX; public Standby(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is loading..."); // 游戲正在載入 iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改為“游戲中狀態” } @Override public void exitGame() { System.out.println("There is no game process!");// 不存在該游戲進程,這是此狀態的一個不恰當動作 } }
public class Playing implements State { //“游戲中”狀態 private MyiPhoneX iPhoneX; public Playing(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is already in progress!");// 已存在該游戲進程 } @Override public void exitGame() { System.out.println("Game is exiting..."); // 退出游戲中 iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改為“待機狀態” } }
我的iPhoneX實體類:
public class MyiPhoneX { private State standby; private State playing; private State state ; // 持有當前狀態的實例變數 public MyiPhoneX() { standby = new Standby(this); playing = new Playing(this); state = standby; } public void startGame() { // 打開游戲 state.startGame(); } public void exitGame() { // 結束游戲 state.exitGame(); } public void setState(State state) { // 用於修改手機當前狀態 this.state = state; } public State getStandby() { return standby; } public State getPlaying() { return playing; } }
實現需求
將新的“游戲無限載入”狀態加上,代碼修改如下:
增加遇到渣渣游戲的“游戲無限載入”狀態類:
public class RubbishGame implements State { //“游戲無限載入”狀態 private MyiPhoneX iPhoneX; public RubbishGame(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { System.out.println("Game is already in progress!");// 已存在該游戲進程 } @Override public void exitGame() { System.out.println("Game is exiting..."); // 退出游戲中 iPhoneX.setState(iPhoneX.getStandby()); // iPhone修改為“待機狀態” } }
在MyiPhoneX類中添加引用“游戲無限載入”狀態的實例變數:
public class MyiPhoneX { private State standby; private State playing; private State rubbishGame; //添加遇到渣渣游戲的“游戲無限載入”狀態 private State state; // 持有當前狀態的實例變數 public MyiPhoneX() { standby = new Standby(this); playing = new Playing(this); rubbishGame = new RubbishGame(this); //初始化“游戲無限載入”狀態 state = standby; } public void startGame() { // 打開游戲 state.startGame(); } public void exitGame() { // 結束游戲 state.exitGame(); } public void setState(State state) { // 用於修改手機當前狀態 this.state = state; } public State getStandby() { return standby; } public State getPlaying() { return playing; } public State getRubbishGame() { //提供獲取“游戲無限載入”狀態的方法 return rubbishGame; } }
修改“待機”狀態類,我們用隨機數代表遇到“渣渣游戲”的機會,增加從“待機”到“游戲無限載入”狀態的轉換的代碼:
public class Standby implements State { private MyiPhoneX iPhoneX; private Random randomRubbish = new Random(); //用於產生隨機數 public Standby(MyiPhoneX iPhoneX) { this.iPhoneX = iPhoneX; } @Override public void startGame() { int rubbish = randomRubbish.nextInt(10); if (rubbish <= 1) { System.out.println("Rubbish Game!"); // 游戲正在載入 iPhoneX.setState(iPhoneX.getRubbishGame()); // iPhone修改為“游戲中狀態” } else { System.out.println("Game is loading..."); // 游戲正在載入 iPhoneX.setState(iPhoneX.getPlaying()); // iPhone修改為“游戲中狀態” } } @Override public void exitGame() { System.out.println("There is no game process!");// 不存在該游戲進程,這是此狀態的一個不恰當動作 } }
測試代碼:
public class MyiPhoneXTest { public static void main(String[] args) { MyiPhoneX iPhoneX = new MyiPhoneX(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); iPhoneX.startGame(); iPhoneX.exitGame(); } }
Rubbish Game! Game is exiting... Game is loading... Game is exiting... Rubbish Game! Game is exiting... Game is loading... Game is exiting...
這樣就為我的iPhoneX增加了一個新的狀態,並實現了這個新的狀態。要做的只是新增這個狀態類及增加能夠轉換到這個狀態的代碼。
相對最早的代碼版本,刪除了容易產生問題的if語句,日後更好維護,讓大多數狀態類“對修改關閉”,讓MyiPhoneX“對擴展開放”,而且這樣的代碼結構,對應起相關的狀態圖也更容易閱讀和理解。
這樣設計的好處在這個比較簡單的例子里看起來可能不是很明顯;假如是在多個狀態的程式,我們新增一個狀態,只需新增這個狀態的實體類、在主體類中加入這個新狀態類的實例引用、在能夠轉換到這個狀態的其他狀態類加入轉換的邏輯代碼就可以了。
狀態模式:
以上的新設計採用了狀態模式,狀態模式允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了它的類。
這個模式將狀態封裝成獨立的類,並將動作委托到代表當前狀態的對象,我的iPhoneX在“待機”和“游戲中”兩種不同狀態時,如果執行“開始游戲”的動作,就會得到不同的具體行為(提示已開啟游戲和啟動游戲)。
我們使用組合通過簡單引用不同的狀態對象來造成類改變的假象(執行一個動作時,當前的狀態對象的具體動作里可能會轉換內部的狀態,而客戶的視角是主體對象自己發生了改變,沒有意識到是狀態對象起的作用)。
狀態模式的類圖:
我們可以發現,它和策略模式的類圖是一樣的。
兩個模式的差別在於它們的“意圖”不同。
①對狀態模式而言
將具體行為封裝在狀態類中,Context的行為委托給當前具體狀態對象來執行,執行具體行為後可能會改變當前狀態,這樣Context執行的具體行為也在改變(每個狀態類對每種行為都有各自的具體實現)。而使用Context的客戶對於狀態對象卻是陌生的、甚至是未知的。這個方案可以去掉原本存在Context內的許多的條件判斷,通過Context對象內簡單的改變狀態對象來改變行為。
②對策略模式而言
客戶對於策略對象是很熟悉的,通常由客戶主動選擇Context需要組合的策略對象,而且雖然策略模式可以在運行時改變策略,但對於某個Context對象來說,通常只有一個最適當的策略對象。一般用來作為除了繼承之外的一種彈性替代方案,通過組合不同對象來改變行為。
參考書籍:《Head First 設計模式》