狀態模式(State Pattern)詳解
一、狀態模式簡介
狀態模式(State Pattern) 是一種 行為型設計模式(對象行為型模式),它允許一個對象在其內部狀態改變時改變其行為。換句話說,對象看起來好像修改了它的類。
你可以這樣理解:
“同一個對象,在不同狀態下表現出不同的行為。”
這就像一個自動售貨機:
- 當你沒有投幣時,它不響應“出貨”按鈕;
- 投幣后,它才允許你選擇商品;
- 選擇商品后,它才會出貨并找零。
這個機器的行為隨著內部狀態(如“未投幣”、“已投幣”、“已選擇”)的變化而變化。
-
紅綠燈
-
H2O的三種狀態(未考慮臨界點):
在軟件系統中:
有些對象具有多種狀態。
這些狀態在某些情況下能夠相互轉換。
對象在不同的狀態下將具有不同的行為。
復雜的條件判斷語句來進行狀態的判斷和轉換操作 --> 導致代碼的可維護性和靈活性下降 --> 出現新的狀態時,代碼的擴展性很差,客戶端代碼也需要進行相應的修改,違背了開閉原則。
// 非常多的if else
class TestXYZ
{int behaviour;//Getter and Setter......public void HandleAll(){if (behaviour == 0){ //do something }else if (behaviour == 1){ //do something }else if (behaviour == 2){ //do something }else if (behaviour == 3){ //do something }... some more else if ...}
}
又名狀態對象(Objects for States)
用于解決系統中復雜對象的狀態轉換以及不同狀態下行為的封裝問題。
將一個對象的狀態從該對象中分離出來,封裝到專門的狀態類中,使得對象狀態可以靈活變化。
對于客戶端而言,無須關心對象狀態的轉換以及對象所處的當前狀態,無論對于何種狀態的對象,客戶端都可以一致處理。
狀態模式包含以下3個角色:
Context(環境類)
State(抽象狀態類)
ConcreteState(具體狀態類)
二、解決的問題類型
狀態模式主要用于解決以下問題:
- 對象的行為依賴于其狀態,并且狀態較多導致代碼中出現大量
if-else
或switch-case
判斷邏輯。 - 希望將每種狀態的行為封裝到獨立的類中,提高可讀性和可維護性。
- 需要在運行時動態切換對象的行為。
三、使用場景
場景 | 示例 |
---|---|
工作流/審批系統 | 訂單狀態:待支付 → 已支付 → 發貨中 → 已完成 |
游戲角色控制 | 角色狀態:站立、奔跑、跳躍、死亡等 |
用戶登錄狀態 | 未登錄、已登錄、鎖定、超時等 |
電梯控制系統 | 運行、停止、開門、關門等狀態 |
在有些情況下,多個環境對象可能需要共享同一個狀態。
如果希望在系統中實現多個環境對象共享一個或多個狀態對象,那么需要將這些狀態對象定義為環境類的靜態成員對象。
對于客戶端而言,無須關心狀態類,可以為環境類設置默認的狀態類,將狀態的轉換工作交給環境類(或具體狀態類)來完成,具體的轉換細節對于客戶端而言是透明的。
可以通過環境類來實現狀態轉換,環境類作為一個狀態管理器,統一實現各種狀態之間的轉換操作。
對象的行為依賴于它的狀態(例如某些屬性值),狀態的改變將導致行為的變化
在代碼中包含大量與對象狀態有關的條件語句,這些條件語句的出現會導致代碼的可維護性和靈活性變差,不能方便地增加和刪除狀態,并且導致客戶類與類庫之間的耦合增強。
四、核心概念
- Context(上下文):持有當前狀態對象的引用,對外提供接口。
- State(狀態接口):定義所有具體狀態類必須實現的行為。
- ConcreteState(具體狀態類):實現狀態接口,封裝特定狀態下的行為。
五、實際代碼案例(Java)
我們以一個簡單的“訂單”系統為例,演示狀態模式的使用。
1. 定義狀態接口
// 訂單狀態接口
interface OrderState {void pay(OrderContext context);void ship(OrderContext context);void deliver(OrderContext context);void cancel(OrderContext context);
}
2. 創建具體狀態類
// 待支付狀態
class PendingState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("訂單已支付,準備發貨...");context.setState(new ShippedState()); // 轉換到已發貨狀態}@Overridepublic void ship(OrderContext context) {System.out.println("請先支付!");}@Overridepublic void deliver(OrderContext context) {System.out.println("請先支付并發貨!");}@Overridepublic void cancel(OrderContext context) {System.out.println("訂單已取消。");context.setState(new CancelledState());}
}// 已發貨狀態
class ShippedState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("訂單已支付過,無需重復支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("訂單已經在運輸途中。");}@Overridepublic void deliver(OrderContext context) {System.out.println("貨物已送達!");context.setState(new DeliveredState());}@Overridepublic void cancel(OrderContext context) {System.out.println("發貨后無法取消訂單。");}
}// 已送達狀態
class DeliveredState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("訂單已完成,無需支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("訂單已送達,無需發貨。");}@Overridepublic void deliver(OrderContext context) {System.out.println("訂單已送達,請勿重復操作。");}@Overridepublic void cancel(OrderContext context) {System.out.println("訂單已完成,無法取消。");}
}// 已取消狀態
class CancelledState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("訂單已取消,無法支付。");}@Overridepublic void ship(OrderContext context) {System.out.println("訂單已取消,無法發貨。");}@Overridepublic void deliver(OrderContext context) {System.out.println("訂單已取消,無法送達。");}@Overridepublic void cancel(OrderContext context) {System.out.println("訂單已取消。");}
}
3. 創建上下文類(訂單)
// 訂單上下文
class OrderContext {private OrderState state;public OrderContext() {this.state = new PendingState(); // 初始狀態:待支付}public void setState(OrderState state) {this.state = state;}// 委托給當前狀態對象處理public void pay() {state.pay(this);}public void ship() {state.ship(this);}public void deliver() {state.deliver(this);}public void cancel() {state.cancel(this);}
}
4. 客戶端測試類
public class Client {public static void main(String[] args) {OrderContext order = new OrderContext();System.out.println("=== 模擬訂單流程 ===");order.pay(); // 支付order.ship(); // 發貨(此時無實際發貨動作,僅提示)order.deliver(); // 送達order.cancel(); // 嘗試取消System.out.println("\n=== 嘗試取消未支付訂單 ===");OrderContext order2 = new OrderContext();order2.cancel();}
}
輸出結果:
=== 模擬訂單流程 ===
訂單已支付,準備發貨...
訂單已經在運輸途中。
貨物已送達!
訂單已完成,無法取消。=== 嘗試取消未支付訂單 ===
訂單已取消。
典型代碼案例:
典型的抽象狀態類代碼
abstract class State
{//聲明抽象業務方法,不同的具體狀態類可以有不同的實現public abstract void Handle();
}
典型的環境類代碼
class Context
{private State state; //維持一個對抽象狀態對象的引用private int value; //其他屬性值,該屬性值的變化可能會導致對象狀態發生變化//設置狀態對象public void SetState(State state)
{this.state = state;}public void Request()
{//其他代碼state.Handle(); //調用狀態對象的業務方法//其他代碼}
}
狀態轉換的實現
- 統一由環境類來負責狀態之間的轉換,環境類充當了狀態管理器(State Manager)角色。
……
public void ChangeState()
{//判斷屬性值,根據屬性值進行狀態轉換
if (value == 0)
{this.SetState(new ConcreteStateA());}else if (value == 1)
{this.SetState(new ConcreteStateB());}......}……
- 由具體狀態類來負責狀態之間的轉換,可以在具體狀態類的業務方法中判斷環境類的某些屬性值,再根據情況為環境類設置新的狀態對象,實現狀態轉換。
……
public void ChangeState(Context ctx)
{//根據環境對象中的屬性值進行狀態轉換
if (ctx.Value == 1)
{ctx.SetState(new ConcreteStateB());}else if (ctx.Value == 2)
{ctx.SetState(new ConcreteStateC());}......}……
其他案例:
- 某軟件公司要為一銀行開發一套信用卡業務系統,銀行賬戶(Account)是該系統的核心類之一,通過分析,該軟件公司開發人員發現在系統中賬戶存在3種狀態,且在不同狀態下賬戶存在不同的行為,具體說明如下:
(1) 如果賬戶中余額大于等于0,則賬戶的狀態為正常狀態(Normal State),此時用戶既可以向該賬戶存款也可以從該賬戶取款;
(2) 如果賬戶中余額小于0,并且大于-2000,則賬戶的狀態為透支狀態(Overdraft State),此時用戶既可以向該賬戶存款也可以從該賬戶取款,但需要按天計算利息;
(3) 如果賬戶中余額等于-2000,那么賬戶的狀態為受限狀態(Restricted State),此時用戶只能向該賬戶存款,不能再從中取款,同時也將按天計算利息;
(4) 根據余額的不同,以上3種狀態可發生相互轉換。
現使用狀態模式設計并實現銀行賬戶狀態的轉換。
- 論壇用戶等級
在某論壇系統中,用戶可以發表留言,發表留言將增加積分;用戶也可以回復留言,回復留言也將增加積分;用戶還可以下載文件,下載文件將扣除積分。該系統用戶分為三個等級,分別是新手、高手和專家,這三個等級對應三種不同的狀態,這三種狀態分別定義如下:
(1) 如果積分小于100分,則為新手狀態,用戶可以發表留言、回復留言,但是不能下載文件。如果積分大于等于1000分,則轉換為專家狀態;如果積分大于等于100分,則轉換為高手狀態。
(2) 如果積分大于等于100分但小于1000分,則為高手狀態,用戶可以發表留言、回復留言,還可以下載文件,而且用戶在發表留言時可以獲取雙倍積分。如果積分小于100分,則轉換為新手狀態;如果積分大于等于1000分,則轉換為專家狀態;如果下載文件后積分小于0,則不能下載該文件。
(3) 如果積分大于等于1000分,則為專家狀態,用戶可以發表留言、回復留言和下載文件,用戶除了在發表留言時可以獲取雙倍積分外,下載文件只扣除所需積分的一半。如果積分小于100分,則轉換為新手狀態;如果積分小于1000分,但大于等于100,則轉換為高手狀態;如果下載文件后積分小于0,則不能下載該文件。
- 某系統要求兩個開關對象要么都處于開的狀態,要么都處于關的狀態,在使用時它們的狀態必須保持一致,開關可以由開轉換到關,也可以由關轉換到開。
試使用狀態模式來實現開關的設計。
- 現要開發一個屏幕放大鏡工具,其具體功能描述如下:
用戶單擊“放大鏡”按鈕之后屏幕將放大一倍,再單擊一次“放大鏡”按鈕屏幕再放大一倍,第三次單擊該按鈕后屏幕將還原到默認大小。
試使用狀態模式來設計該屏幕放大鏡工具。
六、優缺點分析
優點 | 描述 |
---|---|
? 消除復雜的條件判斷語句 | 將 if-else 或 switch 分散到各個狀態類中,結構清晰。允許狀態轉換邏輯與狀態對象合成一體,而不是提供一個巨大的條件語句塊,可以避免使用龐大的條件語句來將業務方法和狀態轉換代碼交織在一起 |
? 符合開閉原則 | 新增狀態只需添加新類,無需修改現有代碼 |
? 職責分離 | 每個狀態類只關注自己狀態下的行為,職責明確 |
其他 | 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數。封裝了狀態的轉換規則,可以對狀態轉換代碼進行集中管理,而不是分散在一個個業務方法中。 |
缺點 | 描述 |
---|---|
? 增加類的數量 | 每個狀態對應一個類,可能導致類膨脹 |
? 狀態轉換邏輯分散 | 狀態之間的流轉由具體狀態類控制,可能不夠集中。對開閉原則的支持并不太好。增加新的狀態類需要修改負責狀態轉換的源代碼,否則無法轉換到新增狀態;而且修改某個狀態類的行為也需要修改對應類的源代碼 |
? 不適合狀態較少的場景 | 如果狀態只有兩三種,使用狀態模式反而顯得過度設計。結構與實現都較為復雜,如果使用不當將導致程序結構和代碼混亂,增加系統設計的難度 |
七、與策略模式對比(補充)
對比點 | 狀態模式 | 策略模式 |
---|---|---|
目的 | 改變對象的行為以反映其狀態變化 | 封裝算法,使算法可互換 |
狀態流轉 | 狀態之間可以相互轉換 | 策略之間通常是獨立的 |
上下文關系 | 上下文行為隨狀態變化而變 | 上下文使用策略完成某項任務 |
八、最終小結
狀態模式是一種非常實用的設計模式,特別適用于那些具有多個狀態、且每個狀態下行為差異較大的對象。它通過將每種狀態封裝成獨立的類,使程序結構更清晰、易于擴展和維護。
在開發訂單系統、工作流引擎、游戲狀態管理、UI 控件狀態控制等項目中,狀態模式能有效提升代碼的可讀性和可維護性。
📌 一句話總結:
狀態模式就像一個“狀態驅動的行為控制器”,讓對象根據自己的“心情”(狀態)做出不同的反應。
? 推薦使用場景:
- 對象有多個狀態,且狀態轉換頻繁;
- 使用大量
if-else
或switch-case
判斷狀態; - 需要后期擴展更多狀態。
注意,以上部分內容由AI大模型生成,注意識別!