no_mkd
一。什麼是狀態模式?
把所有動作都封裝在狀態物件中,狀態持有者將行為委託給當前狀態物件
也就是說,狀態持有者(比如汽車、電視、ATM 機都有多個狀態)並不知道動作細節,狀態持有者只關心自己當前所處的狀態(持有的狀態物件是哪個),再把一切事情都交給當前狀態物件去打理就好了,甚至都不用去控制狀態切換(當然,狀態持有者有權利控制狀態切換,也可以選擇做甩手掌櫃。。)
二。舉個例子
假設我們要模擬一個 ATM 機,有以下需求:
- 取款,驗證卡密,吐出現鈔,結束服務
- 若卡密驗證失敗或者餘額不足,則直接彈出卡片,結束本次服務
- 機器內無存鈔,顯示 No Cash,並向銀行發送無鈔資訊
- 機器故障,顯示 No Service,並向維修人員發送維修請求
那麼用戶取款的過程應該是這樣的:

這是用戶操作的流程,對於 ATM 機而言還需要注意以下幾點:
- 獲得用戶輸入金額後,不僅要驗證用戶卡內餘額,還要驗證 ATM 機內餘額
- 每一次成功的取款服務結束後,都要檢查 ATM 機內餘額(若無鈔則進行相應處理)
- 任何環節出現無法處理的錯誤,都按照故障來處理
想想上面的取款過程,我們還必須考慮各種非法操作,比如:
- 不插卡直接輸入密碼
- 機器故障時仍然進行操作
- 機內無鈔時要求取款
細分之後,我們會發現細節問題變得很麻煩,機器就擺在那裡,所有的用戶接口都開放著,用戶完全有可能在任何時候使用任何接口。為了防止這些非法操作,我們不得不添加一系列的判斷,比如:
- 驗證密碼之前應該檢查是否已經插卡,以及機器是否發生了故障
- 插卡之前應該檢查機器是否發生了故障
- 取款操作之前應該檢查機器是否故障,是否無鈔,是否。。。
我們的代碼中需要設置大量成員變數,用來標識機器的狀態,更可怕的是我們的邏輯塊裡面存在大量的 if-else,而且每一個操作裡面的 if-else 塊都只有細微的差別,看起來像冗餘,但又不好處理(即使我們把每一個判斷都提出來作為獨立的方法,可以縮減代碼規模,但在處理過程上仍然是冗餘。。)
更好的處理方法就是使用狀態模式,能夠完全消除狀態判斷部分的冗餘,並提供清晰整潔的代碼結構。下面用狀態模式來實現例子中的需求
(1)首先找出 ATM 提供的所有接口
- 插卡
- 提交密碼
- 取款(假設取款按鈕是物理鍵)
- 查詢(假設同上)
- 取卡
(2)再找出 ATM 的所有狀態以及各個狀態對應的動作
- 準備就緒(Ready),可用接口:全部
- 無鈔(NoCash),可用接口:1,2,4,5
- 故障(NoService),可用接口:無
(3)編碼實現
先定義 State 基類,類中封裝了(1)列出的所有接口:
package StatePattern;/**
-
定義 ATM 機狀態
-
@author ayqy / public interface ATMState { /*
- 插卡 */ public abstract void insertCard();
/**
- 提交密碼 */ public abstract void submitPwd();
/**
- 取款 */ public abstract void getCash();
/**
- 查詢餘額 */ public abstract void queryBalance();
/**
- 取卡 */ public abstract void ejectCard(); }
再逐一實現三個狀態
ReadyState:
package StatePattern;/**
-
實現 ATM 就緒狀態
-
@author ayqy */ public class ReadyState implements ATMState{ private ATM atm;//保留狀態持有者的引用,以便對其進行操作
public ReadyState(ATM atm){ this.atm = atm; }
@Override public void insertCard() { System.out.println("插卡完成"); }
@Override public void submitPwd() { System.out.println("密碼提交完成"); //驗證密碼並做相應處理 if("123".equals(atm.getPwd())){ System.out.println("密碼驗證通過"); } else{ System.out.println("密碼驗證失敗"); //彈出卡片 ejectCard(); } }
@Override public void getCash() { if(atm.getTotalAmount() >= atm.getAmount() && atm.getBalance() >= atm.getAmount()){ //更新賬戶餘額 atm.setBalance(atm.getBalance() - atm.getAmount()); //更新機內現鈔總數 atm.setTotalAmount(atm.getTotalAmount() - atm.getAmount()); System.out.println("吐出¥" + atm.getAmount()); System.out.println("取款完成"); //彈出卡片 ejectCard(); //檢查機內餘鈔 if(atm.getTotalAmount() == 0){//若無鈔,切換到 NoCash 狀態 atm.setCurrState(atm.getNoCashState()); System.out.println("無鈔資訊已經發送至銀行"); } } else{ System.out.println("取款失敗,餘額不足"); //彈出卡片 ejectCard(); } }
@Override public void queryBalance() { System.out.println("餘額¥" + atm.getBalance()); System.out.println("餘額查詢完成"); }
@Override public void ejectCard() { System.out.println("取卡完成"); } }
注意我們在狀態類中進行狀態切換的部分:
if(atm.getTotalAmount() == 0){//若無鈔,切換到 NoCash 狀態
atm.setCurrState(atm.getNoCashState());
}
我們並不是直接 new 具體狀態物件,而是使用了 ATM 提供的 set 接口,這樣做是為了盡量的解耦(兄弟物件彼此之間並不認識),獲取更多的彈性
實現 NoCashState:
package StatePattern;/**
-
實現 ATM 無鈔狀態
-
@author ayqy */ public class NoCashState implements ATMState{ private ATM atm;//保留狀態持有者的引用,以便對其進行操作
public NoCashState(ATM atm){ this.atm = atm; }
@Override public void insertCard() { System.out.println("插卡完成"); }
@Override public void submitPwd() { System.out.println("密碼提交完成"); //驗證密碼並做相應處理 if("123".equals(atm.getPwd())){ System.out.println("密碼驗證通過"); } else{ System.out.println("密碼驗證失敗"); //彈出卡片 ejectCard(); } }
@Override public void getCash() { System.out.println("取款失敗,機內無鈔"); }
@Override public void queryBalance() { System.out.println("餘額¥" + atm.getBalance()); System.out.println("餘額查詢完成"); }
@Override public void ejectCard() { System.out.println("取卡完成"); } }
實現 NoServiceState:
package StatePattern;/**
-
實現 ATM 故障狀態
-
@author ayqy */ public class NoServiceState implements ATMState{ private ATM atm;//保留狀態持有者的引用,以便對其進行操作
public NoServiceState(ATM atm){ this.atm = atm; }
@Override public void insertCard() { System.out.println("插卡失敗,機器發生了故障"); }
@Override public void submitPwd() { System.out.println("密碼提交失敗,機器發生了故障"); }
@Override public void getCash() { System.out.println("取款失敗,機器發生了故障"); }
@Override public void queryBalance() { System.out.println("餘額查詢失敗,機器發生了故障"); }
@Override public void ejectCard() { System.out.println("取卡失敗,機器發生了故障"); } }
實現了具體的狀態,就可以構造 ATM 類了,就像這樣:
package StatePattern;/**
-
實現 ATM 機
-
@author ayqy */ public class ATM { /所有狀態/ private ATMState readyState; private ATMState noCashState; private ATMState noServiceState;
private ATMState currState;//當前狀態 private int totalAmount;//機內現鈔總數
/測試用的臨時變數/ private String pwd;//密碼 private int balance;//餘額 private int amount;//取款金額
public ATM(int totalAmount, int balance, int amount, String pwd) throws Exception{ //初始化所有狀態 readyState = new ReadyState(this); noCashState = new NoCashState(this); noServiceState = new NoServiceState(this);
if(totalAmount > 0){ currState = readyState; } else if(totalAmount == 0){ currState = noCashState; } else{ throw new Exception(); } //初始化測試數據 this.totalAmount = totalAmount; this.balance = balance; this.amount = amount; this.pwd = pwd;}
/把具體行為委託給狀態物件/ /**
- 插卡 */ public void insertCard(){ currState.insertCard(); }
/**
- 提交密碼 */ public void submitPwd(){ currState.submitPwd(); }
/**
- 取款 */ public void getCash(){ currState.getCash(); }
/**
- 查詢餘額 */ public void queryBalance(){ currState.queryBalance(); }
/**
- 取卡 */ public void ejectCard(){ currState.ejectCard(); }
public String toString(){ return "現鈔總數¥" + totalAmount; }
/此處略去大量 getter and setter/ }
一切都做好了,迫不及待的測試一下吧
三。運行示例
首先實現一個 Test 類:
package StatePattern;import java.util.Scanner;
/**
-
實現測試類
-
@author ayqy / public class Test { public static void main(String[] args) { /測試數據/ / 機內總數 賬戶餘額 取款金額 密碼 * 1000 500 200 123 * 1000 300 500 123 * 0 500 200 123 * */ try { test(1000, 500, 200, "123"); System.out.println("-------"); test(1000, 300, 500, "123"); System.out.println("-------"); test(0, 500, 200, "123"); } catch (Exception e) { System.out.println("機器故障,維修請求已經發送至維修方"); } }
private static void test(int totalAmount, int balance, int amount, String pwd)throws Exception{ //創建 ATM ATM atm; atm = new ATM(totalAmount, balance, amount, pwd); //輸出初始狀態 System.out.println(atm.toString()); atm.insertCard(); atm.submitPwd(); atm.getCash(); //輸出結束狀態 System.out.println(atm.toString()); } }
我們設計的三個用例(正常取款,取大於餘額的款,機內無現鈔)運行結果如下:

四。狀態模式與策略模式
還記得策略模式嗎?難道不覺得這兩者很相像嗎?
沒錯,這兩種模式的類圖是完全相同的,解釋一下:
- 狀態主體(擁有者)持有狀態物件,運行時可以通過動態指定狀態物件來改變類的行為
- 策略主體持有算法族物件,運行時可以通過動態選擇算法族中的算法(策略)來改變類的行為
也就是說,狀態模式與策略模式都支持運行時的多態,並且其實現方式都是組合 + 委託。但是這並不代表這兩種模式是相同的,因為它們的目標不同:
- 狀態模式實現了算法流程可變(即狀態切換,不同的狀態有不同的流程)
- 策略模式實現了算法細節可選(即選擇算法族內的算法,一個算法族包含多個可選算法)
暫無評論,快來發表你的看法吧