糖果機
如下糖果機工作狀態圖,我們對這個狀態圖進行編碼實現糖果機的工作過程
這個狀態圖的每個圓圈代表一個狀態,可以看到有4個狀態同時又4個動作,分別是:“投入1元錢”、“退回1元錢”、“轉動曲柄”、“發放糖果”。當要發放糖果的時候需要判斷糖果數量是否為0來進入“糖果售磐”或者“沒有1元錢”狀態。所以有5個狀態轉換。
接下來我們對狀態圖進行分析實現編碼
①找出狀態:沒有1元錢、有1元錢、糖果售出、糖果售磐。
②創建實例變量持有當前狀態,定義每個狀態的值。
static int SOLD_OUT=0; static int NO_ONERMB=1; static int HAS_ONERMB=2; static int SOLD=3; int State=SOLD_OUT;
③將系統中的動作整合起來:投入1元、退回1元、轉動曲柄、發放糖果。
以投入1元為例
public void InsertOneRMB() { if (State == HAS_ONERMB) { Console.WriteLine("已經投入了,不能再投入"); } else if (State == SOLD_OUT) { Console.WriteLine("糖果已經售磐,不能再投入"); } else if (State == SOLD) { Console.WriteLine("請稍后投入,正在發放糖果"); } else if (State == NO_ONERMB) { State = HAS_ONERMB; Console.WriteLine("你投入了1元錢"); } }
根據分析我們就可以寫出糖果機的代碼,其他幾個動作具體實現就不再寫了。
1 class GumballMachine 2 { 3 readonly static int SOLD_OUT = 0; 4 readonly static int NO_ONERMB = 1; 5 readonly static int HAS_ONERMB = 2; 6 readonly static int SOLD = 3; 7 8 int State = SOLD_OUT; 9 int Count = 0; 10 11 public GumballMachine(int count) { 12 this.Count = count; 13 if (count > 0) 14 { 15 State = NO_ONERMB; 16 } 17 } 18 /// <summary> 19 /// 投入1元 20 /// </summary> 21 public void InsertOneRMB() { 22 23 if (State == HAS_ONERMB) 24 { 25 Console.WriteLine("已經投入了,不能再投入"); 26 } 27 else if (State == SOLD_OUT) { 28 Console.WriteLine("糖果已經售磐,不能再投入"); 29 } 30 else if (State == SOLD) 31 { 32 Console.WriteLine("請稍后投入,正在發放糖果"); 33 } 34 else if (State == NO_ONERMB) 35 { 36 State = HAS_ONERMB; 37 Console.WriteLine("你投入了1元錢"); 38 } 39 } 40 /// <summary> 41 /// 退回1元 42 /// </summary> 43 public void EjectOneRMB() { } 44 45 /// <summary> 46 /// 轉動手柄 47 /// </summary> 48 public void TurnCrank() { } 49 50 /// <summary> 51 /// 發放糖果 52 /// </summary> 53 public void Dispense() { } 54 }
通過這樣的實現已經是考慮的比較周詳而且代碼清晰。但是該來的還是回來,需求變更仍然讓我們的代碼面臨問題。接下來我們看如何滿足需求以及狀態模式的使用。
需求變更
需求:當個贏家!10人有1人可以得到一顆免費糖果(當曲柄轉動時,有10%的機率掉下來兩顆糖果)。
針對於這個需求我們將狀態添加到狀態圖
針對於原來的代碼怎么修改呢?首先我們需要加上一個新的狀態“贏家”,然后必須在每個方法中加入一個新的條件判斷處理“贏家”狀態,更麻煩的是TurnCrank方法需要大改造,因為必須加上檢查是否贏家來決定切換到贏家狀態還是售出糖果狀態。如果再加入其他狀態,那么代碼要繼續修改,而現在的代碼面對變法時有幾個問題。
①沒有遵循開閉原則。
②狀態轉換被隱藏在條件語句中,不明顯。
③沒有把會改變的部分封裝起來。
④該設計不符合面向對象。
新的設計
我們不用現在的代碼,重新它以便將狀態對象封裝在各自的類中,然后再動作發生時委托給當前狀態。
①首先,我們定義一個Sate接口。在這個接口內,糖果機的每個動作都有一個對應的方法。
②為機器中的每個狀態實現狀態類。這些類負責在對應的狀態下進行機器的行為。
③將動作委托到狀態類。
用類圖來梳理設計
按照類圖進行實現,首先定義接口。然后實現NoOneRMBState
1 public class NoOneRMBState : State 2 { 3 GumballMachine gumballMachine; 4 public NoOneRMBState(GumballMachine gumballMachine) 5 { 6 this.gumballMachine = gumballMachine; 7 } 8 9 public void InsertOneRMB() 10 { 11 Console.WriteLine("你投入了1元錢"); 12 gumballMachine.SetState(gumballMachine.hasOneRMBState); //將糖果狀態改到hasOneRMBState 13 } 14 public void EjectOneRMB() 15 { 16 Console.WriteLine("沒有錢可退"); 17 } 18 19 public void TurnCrank() 20 { 21 Console.WriteLine("沒有錢,不能轉動"); 22 } 23 public void Dispense() 24 { 25 Console.WriteLine("沒有錢,不能發放糖果"); 26 } 27 }
其他狀態類是具體的業務代碼就不再一一實現了,我們最后改造糖果機
1 public class GumballMachine 2 { 3 public State soldOutState { get; } 4 public State noOneRMBState { get; } 5 public State hasOneRMBState { get; } 6 public State soldState { get; } 7 8 9 State State; 10 int Count = 0; 11 12 public GumballMachine(int count) 13 { 14 this.Count = count; 15 soldOutState = new SoldOutState(this); 16 noOneRMBState = new NoOneRMBState(this); 17 hasOneRMBState = new HasOneRMBState(this); 18 soldState = new SoldState(this); 19 if (count > 0) 20 { 21 State = noOneRMBState; 22 } 23 else { 24 State = soldOutState; 25 } 26 } 27 /// <summary> 28 /// 投入1元 29 /// </summary> 30 public void InsertOneRMB() 31 { 32 State.InsertOneRMB(); 33 } 34 /// <summary> 35 /// 退回1元 36 /// </summary> 37 public void EjectOneRMB() { 38 State.EjectOneRMB(); 39 } 40 41 /// <summary> 42 /// 轉動手柄 43 /// </summary> 44 public void TurnCrank() { 45 State.TurnCrank(); 46 //狀態內部動作,所以我們不在需要單獨一個發放糖果的方法。 47 State.Dispense(); 48 } 49 50 /// <summary> 51 /// 設置狀態 52 /// </summary> 53 /// <param name="state"></param> 54 public void SetState(State state) 55 { 56 this.State = state; 57 } 58 }
如上就是利用狀態模式改造后的代碼。
狀態模式定義
狀態模式:允許對象在內部狀態改變時改變它的行為,對象看起來好像是修改了它的類。
定義的第一部分描述這個模式將狀態封裝為獨立的類,並將動作委托到代表當前狀態的對象,行為會隨着內部狀態而改變。例如在noOneRMBState和hasOneRMBState兩個狀態時,投入1元,就會得到不同的行為。
第二部分“對象看起來好像是修改了它的類”,從客戶來看如果說使用的對象能夠完全改變自己的行為,那么會覺得這個對象實際上是從別的類再實例化而來的。事實上我們實在使用組合簡單引用不同狀態對象來造成類改變的假象。
策略模式與狀態模式
我們發現策略模式與狀態模式類圖一樣,但是他們所要干事情的意圖完全不一樣,所以我做個簡要的區分
狀態模式:對象創建后,可以告訴客戶從什么狀態開始,然后隨着時間推移改變自己的狀態,而任何狀態的改變都是定義好的。
策略模式:允許對象通過組合和委托來擁有不同的算法或行為。能實例化一個類,給它一個實現某些行為的策略對象,也可以在運行時改變行為。