意圖
允許一個對象在其內部狀態改變時改變它的行爲
說人話:允許對象在改變自身狀態時候,更改綁定的特定方法
狀態模式的誕生
【產品】:Hello,開發小哥,我們需要開發一款 娃娃機
,你可以提前想想怎麼設計它啦。
【開發】:娃娃機?我想想奧,它需要投幣,用戶移動,確認抓取,結束這幾個動作,好像很好做欸,用一個變量維護它當前的階段,然後寫四個 if 語句就好啦。
【BOSS】:你準備用一個主方法,四個子方法配合 if 語句外加一個狀態變量去做嗎?
// 僞代碼
public void handle() {
if (flag == A) {
a();
}
if (flag == B) {
b();
}
}
【開發】:對啊,老大,你真是我肚子裏的蛔蟲!
【BOSS】:蛔你個頭,這樣做 大錯特錯! ,你難道想對 投幣口,按鈕,搖桿都綁定同一個方法嗎?
【開發】:對哦,它們應該是 不同的方法,同時暴露給用戶,我再思考思考
HeadFirst 核心代碼
定義狀態接口,同時封裝變化,利用default關鍵字封裝默認方法
public interface State {
/** 投幣 **/
default void giveMoney() {
System.out.println("無法投幣");
}
/** 移動滑桿 **/
default void move() {
System.out.println("無法移動滑桿");
}
/** 抓取 **/
default void grab() {
System.out.println("無法抓取");
}
void changeState();
}
投幣狀態 狀態的其中之一
public class MoneyState implements State{
Context context;
public MoneyState(Context context) {
this.context = context;
}
@Override
public void giveMoney() {
System.out.println("已投幣!");
changeState();
}
@Override
public void changeState() {
context.setExecute(new MoveState(context));
}
}
爲了儘量減少代碼,只展示了其中一種狀態,我們可以看到在 MoneyState 狀態類執行所屬的業務方法時,更改了上下文持有的狀態類,這就產生了 狀態的變更 ,同時上下文更加清晰,即:我只用考慮我下一個狀態是什麼
狀態模式的設計思路:
- Context 上下文環境,持有狀態
- State 狀態頂層接口
- ConcreteState 具體的狀態
簡單來說,
- 必須清晰的認識到共有多少種不同的狀態,並通過接口定義其核心方法,封裝變化
- 狀態類持有 Context 上下文,在覈心方法處理後更改其狀態
如果看着有點模棱兩可,建議看完本文後,訪問專題設計模式開源項目,裏面有具體的代碼示例,鏈接在最下面
狀態模式的關鍵
- 明確所有可能發生的狀態,及其轉換關係
- 明確狀態模式中的各個狀態是有可能同時暴露給用戶的
就好像娃娃機運作的多種狀態, 投幣,移動搖桿,按下確認按鈕等等可能不按先後順序觸發
整一個 “流程” 模式
每個狀態的方法名都一樣會如何?
上文中我們大概知道了狀態模式的特點,把狀態封裝成類,在調用狀態-核心方法時候更改其狀態本身,此時考慮的多種狀態方法名可能各不相同,假設我們都起一樣的名字會如何?
我們會首先遇到一個問題,我們無法得知它需要調用幾次方法(因爲可能有重複性 A - B 的情況),但如果無限循環,在適當的地方控制其結束點,和是否繼續執行的標識,好像就可以解決了。
來一個流程案例
簡單描述下即:開始處理訂單
- 正常則進入成功狀態,入庫,結束執行
- 失敗則進入失敗狀態,檢測是否重新執行,扭轉狀態爲處理訂單
上代碼
Context 上下文
public class Context {
/**
* 最大執行次數
*/
public static final Integer FAIL_NUM = 3;
/***
* 失敗次數
*/
private int failNum;
/**
* 是否繼續執行的標識
*/
private boolean isAbandon;
/***
* 當前狀態
*/
private StateInterface stateInterface;
public Context() {
this.stateInterface = new HandleOrder();
this.failNum = 1;
this.isAbandon = false;
}
/***
* 處理方法
*/
public void handle () {
stateInterface.doAction(this);
}
// 省略無用代碼...
}
處理訂單狀態
public class HandleOrder implements StateInterface {
@Override
public void doAction(Context context) {
printCurrentState();
// do somethings
int num = (int) (Math.random() * 11);
if (num >= 8) {
System.out.println("處理訂單完成, 進入成功狀態...");
context.setStateInterface(new SuccessOrder());
} else {
System.out.println("處理訂單失敗, 進入失敗狀態...");
context.setStateInterface(new FailOrder());
}
CodeUtils.spilt();
}
@Override
public StateEnums getCurrentState() {
return StateEnums.HANDLE_ORDER;
}
}
客戶端調用方法
public class App {
public static void main(String[] args) {
// 模擬從隊列中取任務按流程循環執行
Context context = new Context();
while (true) {
// 校驗是否爲廢棄 | 已完成任務
if (context.isAbandon()) {
System.out.println("此條任務不再執行... ");
break;
}
context.handle();
}
}
}
測試結果輸出:
當前狀態:訂單處理
處理訂單失敗, 進入失敗狀態...
------------------------
當前狀態:處理訂單失敗
訂單處理失敗... 當前執行次數: 1
------------------------
當前狀態:訂單處理
處理訂單失敗, 進入失敗狀態...
------------------------
當前狀態:處理訂單失敗
訂單處理失敗... 當前執行次數: 2
------------------------
當前狀態:訂單處理
處理訂單完成, 進入成功狀態...
------------------------
當前狀態:處理訂單成功
訂單處理完成 -> 進入入庫邏輯...
入庫處理完成
------------------------
此條任務不再執行...
如果看着有點模棱兩可,建議看完本文後,訪問專題設計模式開源項目,裏面有具體的代碼示例,鏈接在最下面
“流程” 模式適用的場景
在這樣的設計中,與其說是狀態的變更,不如說是 “流程” 的變更更爲貼切,因此它可以作爲諸多後臺任務的解決方案,尤其是面臨很多業務流程場景時,可以極大的提高代碼的可維護性: 我只用考慮和我有關的 “流程”
遵循的設計原則
- 封裝變化:在父級接口中提供 default 方法,子類實現其對應的狀態方法即可
- 多用組合,少用繼承:狀態模式經常和策略模式做對比,它們都是利用組合而非繼承增強其變化和能力
什麼場景適合使用狀態模式
- 一個對象的行爲取決於它的狀態,並且它必須在運行時刻根據狀態改變其行爲
- 一個操作中含有龐大的多分支條件語句,且這些分支依賴於該對象的狀態
最後
附上GOF一書中對於狀態模式的UML圖:
相關代碼鏈接
- 兼顧了《HeadFirst》以及《GOF》兩本經典書籍中的案例
- 提供了友好的閱讀指導