設計模式--行為型模式--狀態模式
在軟體開發過程中,應用程式中的部分物件可能會根據不同的情況做出不同的行為,我們把這種物件稱為有狀態的物件,而把影響物件行為的一個或多個動態變化的屬性稱為狀態。當有狀態的物件與外部事件產生互動時,其內部狀態就會發生改變,從而使其行為也發生改變。如人都有高興和傷心的時候,不同的情緒有不同的行為,當然外界也會影響其情緒變化。
對這種有狀態的物件程式設計,傳統的解決方案是:將這些所有可能發生的情況全都考慮到,然後使用 if-else 或 switch-case 語句來做狀態判斷,再進行不同情況的處理。但是顯然這種做法對複雜的狀態判斷存在天然弊端,條件判斷語句會過於臃腫,可讀性差,且不具備擴充套件性,維護難度也大。且增加新的狀態時要新增新的 if-else 語句,這違背了“開閉原則”,不利於程式的擴充套件。
以上問題如果採用“狀態模式”就能很好地得到解決。狀態模式的解決思想是:當控制一個物件狀態轉換的條件表示式過於複雜時,把相關“判斷邏輯”提取出來,用各個不同的類進行表示,系統處於哪種情況,直接使用相應的狀態類物件進行處理,這樣能把原來複雜的邏輯判斷簡單化,消除了 if-else、switch-case 等冗餘語句,程式碼更有層次性,並且具備良好的擴充套件力
狀態模式的定義與特點
狀態(State)模式的定義:對有狀態的物件,把複雜的“判斷邏輯”提取到不同的狀態物件中,允許狀態物件在其內部狀態發生改變時改變其行為。
狀態模式是一種物件行為型模式,其主要優點如下。
- 結構清晰,狀態模式將與特定狀態相關的行為區域性化到一個狀態中,並且將不同狀態的行為分割開來,滿足“單一職責原則”。
- 將狀態轉換顯示化,減少物件間的相互依賴。將不同的狀態引入獨立的物件中會使得狀態轉換變得更加明確,且減少物件間的相互依賴。
- 狀態類職責明確,有利於程式的擴充套件。通過定義新的子類很容易地增加新的狀態和轉換。
狀態模式的主要缺點如下。
- 狀態模式的使用必然會增加系統的類與物件的個數。
- 狀態模式的結構與實現都較為複雜,如果使用不當會導致程式結構和程式碼的混亂。
- 狀態模式對開閉原則的支援並不太好,對於可以切換狀態的狀態模式,增加新的狀態類需要修改那些負責狀態轉換的原始碼,否則無法切換到新增狀態,而且修改某個狀態類的行為也需要修改對應類的原始碼。
狀態模式的結構與實現
狀態模式把受環境改變的物件行為包裝在不同的狀態物件裡,其意圖是讓一個物件在其內部狀態改變的時候,其行為也隨之改變。現在我們來分析其基本結構和實現方法。
1. 模式的結構
狀態模式包含以下主要角色。
- 環境類(Context)角色:也稱為上下文,它定義了客戶端需要的介面,內部維護一個當前狀態,並負責具體狀態的切換。
- 抽象狀態(State)角色:定義一個介面,用以封裝環境物件中的特定狀態所對應的行為,可以有一個或多個行為。
- 具體狀態(Concrete State)角色:實現抽象狀態所對應的行為,並且在需要的情況下進行狀態切換。
其結構圖如圖 1 所示。
2. 模式的實現
狀態模式的實現程式碼如下:
public class StatePatternClient { public static void main(String[] args) { Context context = new Context(); //建立環境 context.Handle(); //處理請求 context.Handle(); context.Handle(); context.Handle(); } } //環境類 class Context { private State state; //定義環境類的初始狀態 public Context() { this.state = new ConcreteStateA(); } //設定新狀態 public void setState(State state) { this.state = state; } //讀取狀態 public State getState() { return (state); } //對請求做處理 public void Handle() { state.Handle(this); } } //抽象狀態類 abstract class State { public abstract void Handle(Context context); } //具體狀態A類 class ConcreteStateA extends State { public void Handle(Context context) { System.out.println("當前狀態是 A."); context.setState(new ConcreteStateB()); } } //具體狀態B類 class ConcreteStateB extends State { public void Handle(Context context) { System.out.println("當前狀態是 B."); context.setState(new ConcreteStateA()); } }
程式執行結果如下:
當前狀態是 A.
當前狀態是 B.
當前狀態是 A.
當前狀態是 B.
狀態模式的應用例項
【例1】用“狀態模式”設計一個學生成績的狀態轉換程式。
分析:本例項包含了“不及格”“中等”和“優秀” 3 種狀態,當學生的分數小於 60 分時為“不及格”狀態,當分數大於等於 60 分且小於 90 分時為“中等”狀態,當分數大於等於 90 分時為“優秀”狀態,我們用狀態模式來實現這個程式。
首先,定義一個抽象狀態類(AbstractState),其中包含了環境屬性、狀態名屬性和當前分數屬性,以及加減分方法 addScore(intx) 和檢查當前狀態的抽象方法 checkState()。
然後,定義“不及格”狀態類 LowState、“中等”狀態類 MiddleState 和“優秀”狀態類 HighState,它們是具體狀態類,實現 checkState() 方法,負責檢査自己的狀態,並根據情況轉換。
最後,定義環境類(ScoreContext),其中包含了當前狀態物件和加減分的方法 add(int score),客戶類通過該方法來改變成績狀態。圖 2 所示是其結構圖。
程式程式碼如下:
public class ScoreStateTest { public static void main(String[] args) { ScoreContext account = new ScoreContext(); System.out.println("學生成績狀態測試:"); account.add(30); account.add(40); account.add(25); account.add(-15); account.add(-25); } } //環境類 class ScoreContext { private AbstractState state; ScoreContext() { state = new LowState(this); } public void setState(AbstractState state) { this.state = state; } public AbstractState getState() { return state; } public void add(int score) { state.addScore(score); } } //抽象狀態類 abstract class AbstractState { protected ScoreContext hj; //環境 protected String stateName; //狀態名 protected int score; //分數 public abstract void checkState(); //檢查當前狀態 public void addScore(int x) { score += x; System.out.print("加上:" + x + "分,\t當前分數:" + score); checkState(); System.out.println("分,\t當前狀態:" + hj.getState().stateName); } } //具體狀態類:不及格 class LowState extends AbstractState { public LowState(ScoreContext h) { hj = h; stateName = "不及格"; score = 0; } public LowState(AbstractState state) { hj = state.hj; stateName = "不及格"; score = state.score; } public void checkState() { if (score >= 90) { hj.setState(new HighState(this)); } else if (score >= 60) { hj.setState(new MiddleState(this)); } } } //具體狀態類:中等 class MiddleState extends AbstractState { public MiddleState(AbstractState state) { hj = state.hj; stateName = "中等"; score = state.score; } public void checkState() { if (score < 60) { hj.setState(new LowState(this)); } else if (score >= 90) { hj.setState(new HighState(this)); } } } //具體狀態類:優秀 class HighState extends AbstractState { public HighState(AbstractState state) { hj = state.hj; stateName = "優秀"; score = state.score; } public void checkState() { if (score < 60) { hj.setState(new LowState(this)); } else if (score < 90) { hj.setState(new MiddleState(this)); } } }
程式執行結果如下:
學生成績狀態測試: 加上:30分, 當前分數:30分, 當前狀態:不及格 加上:40分, 當前分數:70分, 當前狀態:中等 加上:25分, 當前分數:95分, 當前狀態:優秀 加上:-15分, 當前分數:80分, 當前狀態:中等 加上:-25分, 當前分數:55分, 當前狀態:不及格
【例2】用“狀態模式”設計一個多執行緒的狀態轉換程式。
分析:多執行緒存在 5 種狀態,分別為新建狀態、就緒狀態、執行狀態、阻塞狀態和死亡狀態,各個狀態當遇到相關方法呼叫或事件觸發時會轉換到其他狀態,其狀態轉換規律如圖 3 所示。
現在先定義一個抽象狀態類(TheadState),然後為圖 3 所示的每個狀態設計一個具體狀態類,它們是新建狀態(New)、就緒狀態(Runnable )、執行狀態(Running)、阻塞狀態(Blocked)和死亡狀態(Dead),每個狀態中有觸發它們轉變狀態的方法,環境類(ThreadContext)中先生成一個初始狀態(New),並提供相關觸發方法,圖 4 所示是執行緒狀態轉換程式的結構圖。
程式程式碼如下:
public class ScoreStateTest { public static void main(String[] args) { ThreadContext context = new ThreadContext(); context.start(); context.getCPU(); context.suspend(); context.resume(); context.getCPU(); context.stop(); } } //環境類 class ThreadContext { private ThreadState state; ThreadContext() { state = new New(); } public void setState(ThreadState state) { this.state = state; } public ThreadState getState() { return state; } public void start() { ((New) state).start(this); } public void getCPU() { ((Runnable) state).getCPU(this); } public void suspend() { ((Running) state).suspend(this); } public void stop() { ((Running) state).stop(this); } public void resume() { ((Blocked) state).resume(this); } } //抽象狀態類:執行緒狀態 abstract class ThreadState { protected String stateName; //狀態名 } //具體狀態類:新建狀態 class New extends ThreadState { public New() { stateName = "新建狀態"; System.out.println("當前執行緒處於:新建狀態."); } public void start(ThreadContext hj) { System.out.print("呼叫start()方法-->"); if (stateName.equals("新建狀態")) { hj.setState(new Runnable()); } else { System.out.println("當前執行緒不是新建狀態,不能呼叫start()方法."); } } } //具體狀態類:就緒狀態 class Runnable extends ThreadState { public Runnable() { stateName = "就緒狀態"; System.out.println("當前執行緒處於:就緒狀態."); } public void getCPU(ThreadContext hj) { System.out.print("獲得CPU時間-->"); if (stateName.equals("就緒狀態")) { hj.setState(new Running()); } else { System.out.println("當前執行緒不是就緒狀態,不能獲取CPU."); } } } //具體狀態類:執行狀態 class Running extends ThreadState { public Running() { stateName = "執行狀態"; System.out.println("當前執行緒處於:執行狀態."); } public void suspend(ThreadContext hj) { System.out.print("呼叫suspend()方法-->"); if (stateName.equals("執行狀態")) { hj.setState(new Blocked()); } else { System.out.println("當前執行緒不是執行狀態,不能呼叫suspend()方法."); } } public void stop(ThreadContext hj) { System.out.print("呼叫stop()方法-->"); if (stateName.equals("執行狀態")) { hj.setState(new Dead()); } else { System.out.println("當前執行緒不是執行狀態,不能呼叫stop()方法."); } } } //具體狀態類:阻塞狀態 class Blocked extends ThreadState { public Blocked() { stateName = "阻塞狀態"; System.out.println("當前執行緒處於:阻塞狀態."); } public void resume(ThreadContext hj) { System.out.print("呼叫resume()方法-->"); if (stateName.equals("阻塞狀態")) { hj.setState(new Runnable()); } else { System.out.println("當前執行緒不是阻塞狀態,不能呼叫resume()方法."); } } } //具體狀態類:死亡狀態 class Dead extends ThreadState { public Dead() { stateName = "死亡狀態"; System.out.println("當前執行緒處於:死亡狀態."); } }
程式執行結果如下:
當前執行緒處於:新建狀態. 呼叫start()方法-->當前執行緒處於:就緒狀態. 獲得CPU時間-->當前執行緒處於:執行狀態. 呼叫suspend()方法-->當前執行緒處於:阻塞狀態. 呼叫resume()方法-->當前執行緒處於:就緒狀態. 獲得CPU時間-->當前執行緒處於:執行狀態. 呼叫stop()方法-->當前執行緒處於:死亡狀態.
狀態模式的應用場景
通常在以下情況下可以考慮使用狀態模式。
- 當一個物件的行為取決於它的狀態,並且它必須在執行時根據狀態改變它的行為時,就可以考慮使用狀態模式。
- 一個操作中含有龐大的分支結構,並且這些分支決定於物件的狀態時。
狀態模式的擴充套件
在有些情況下,可能有多個環境物件需要共享一組狀態,這時需要引入享元模式,將這些具體狀態物件放在集合中供程式共享,其結構圖如圖 5 所示。
分析:共享狀態模式的不同之處是在環境類中增加了一個 HashMap 來儲存相關狀態,當需要某種狀態時可以從中獲取,其程式程式碼如下:
package state; import java.util.HashMap; public class FlyweightStatePattern { public static void main(String[] args) { ShareContext context = new ShareContext(); //建立環境 context.Handle(); //處理請求 context.Handle(); context.Handle(); context.Handle(); } } //環境類 class ShareContext { private ShareState state; private HashMap<String, ShareState> stateSet = new HashMap<String, ShareState>(); public ShareContext() { state = new ConcreteState1(); stateSet.put("1", state); state = new ConcreteState2(); stateSet.put("2", state); state = getState("1"); } //設定新狀態 public void setState(ShareState state) { this.state = state; } //讀取狀態 public ShareState getState(String key) { ShareState s = (ShareState) stateSet.get(key); return s; } //對請求做處理 public void Handle() { state.Handle(this); } } //抽象狀態類 abstract class ShareState { public abstract void Handle(ShareContext context); } //具體狀態1類 class ConcreteState1 extends ShareState { public void Handle(ShareContext context) { System.out.println("當前狀態是: 狀態1"); context.setState(context.getState("2")); } } //具體狀態2類 class ConcreteState2 extends ShareState { public void Handle(ShareContext context) { System.out.println("當前狀態是: 狀態2"); context.setState(context.getState("1")); } }
程式執行結果如下:
當前狀態是: 狀態1
當前狀態是: 狀態2
當前狀態是: 狀態1
當前狀態是: 狀態2
拓展
狀態模式與責任鏈模式的區別
狀態模式和責任鏈模式都能消除 if-else 分支過多的問題。但在某些情況下,狀態模式中的狀態可以理解為責任,那麼在這種情況下,兩種模式都可以使用。
從定義來看,狀態模式強調的是一個物件內在狀態的改變,而責任鏈模式強調的是外部節點物件間的改變。
從程式碼實現上來看,兩者最大的區別就是狀態模式的各個狀態物件知道自己要進入的下一個狀態物件,而責任鏈模式並不清楚其下一個節點處理物件,因為鏈式組裝由客戶端負責。
狀態模式與策略模式的區別
狀態模式和策略模式的 UML 類圖架構幾乎完全一樣,但兩者的應用場景是不一樣的。策略模式的多種演算法行為擇其一都能滿足,彼此之間是獨立的,使用者可自行更換策略演算法,而狀態模式的各個狀態間存在相互關係,彼此之間在一定條件下存在自動切換狀態的效果,並且使用者無法指定狀態,只能設定初始狀態。