狀態模式(State Pattern)
策略模式以及狀態模式是雙胞胎
策略模式是透過建立演算法的家族,可以在執行期間動態地改變物件的行為。
那狀態模式就是藉由改變物件自身的狀態,讓物件自己控制自己的行為。
這次我們的需求如下:
我們要製作一個銀行帳戶系統,它存有銀行帳戶餘額、狀態等資訊
我們的客戶透過與ATM 與銀行帳戶交涉,進行帳戶的管理。
我們的客戶包含了銀行本身,以及存取款的帳戶持有人。
因此動作支援了存取款以及銀行定期支付利息
目前不同的狀態會有不同的利率水準。
這是一張需求說明,我們如何從其中的狀態得到真正的程式呢?
以下是如何實踐狀態機的方法(State Machine)
1.找出所有狀態
存款為0~-100以下,紅色狀態,利率為0,服務費為15
存款為0~1000,銀色狀態,利率為0.01
存款為1000~10000,金色狀態,利率為0.05
2.接下來,建立一個實體變數,持有目前的狀態,然後定義每個狀態的值。
final static int RED_STATE= 1
final static int SILVER_STATE= 2
final static int GOLD_STATE= 3
int state = Red_State (這是一個實體變數有目前的狀態)
3.將所有的動作整合起來。
客戶領錢
客戶存錢
銀行支付利息
4.我們現在建立一個類別,它的作用就像是一個狀態機,對每一個動作都建立一個對應的方法。
透過if-else判斷決定出,在每個狀態什麼行為是恰檔的。
例如取款的動作我們可以把這個方法寫成這樣:
1: public void Withdraw(double amount)
2: {
3: if (state == RED_STATE)
4: {
5: Console.WriteLine("餘額不足,請存錢");
6: }
7: else if (state == SILVER_STATE)
8: {
9: if (balance > amount)
10: {
11: balance -= amount;
12: Console.WriteLine("馬上吐出現金");
13: state = RED_STATE;
14: }
15: else if (balance == amount)
16: {
17: balance = 0;
18: state = RED_STATE;
19: }
20: else
21: {
22: Console.WriteLine("餘額不足");
23: }
24: }
25: else if (state == GOLD_STATE)
26: {
27: if (balance > amount)
28: {
29: balance -= amount;
30: if (balance < 1000)
31: {
32: state = SILVER_STATE;
33: }
34: Console.WriteLine("馬上吐出現金");
35: }
36: else if (balance == amount)
37: {
38: balance = 0;
39: state = RED_STATE;
40: Console.WriteLine("馬上吐出現金");
41: }
42: else
43: {
44: Console.WriteLine("餘額不足");
45: }
46: }
47: }
其中,我們的狀態會因為不同的存款餘額進行狀態的切換
這個作法所用的技巧只是利用物件內的一個實體變數持有狀態,並利用條件式進行轉換
我們來實踐ATM吧
1: public class stateATM
2: {
3: const int RED_STATE= 1;
4: const int SILVER_STATE= 2;
5: const int GOLD_STATE= 3;
6:
7: int state = RED_STATE; //帳戶預設都是0元
8:
9: double balance; //儲存存款餘額
10:
11: public stateATM(double pInitAmount)
12: {
13: this.balance = pInitAmount;
14: if (balance > 1000)
15: {
16: state = GOLD_STATE;
17: }
18: else if (balance < 1000 && balance > 0)
19: {
20: state = SILVER_STATE;
21: }
22: else
23: {
24: state = RED_STATE;
25: }
26: Console.WriteLine(string.Format("恭喜開戶,餘額為{0}", balance));
27: }
28:
29: public void payInterest()
30: {
31: if (state == GOLD_STATE)
32: {
33: balance =balance * 1.01;
34: }
35: else if (state == SILVER_STATE)
36: {
37: balance = balance * 1.05;
38: }
39: else
40: {
41: //Red狀態,則不增加利息
42: }
43: Console.WriteLine(string.Format("利率發放日;餘額為{0}", balance));
44: }
45:
46: public void Deposit(double amount)
47: {
48: balance += amount;
49: if (balance > 1000)
50: {
51: state = GOLD_STATE;
52: Console.WriteLine("存款完畢");
53: }
54: else if (balance < 1000 && balance > 0)
55: {
56: state = SILVER_STATE;
57: Console.WriteLine("存款完畢");
58: }
59: else
60: {
61: state = RED_STATE;
62: }
63: Console.WriteLine(string.Format("餘額為{0}",balance));
64: }
65:
66: public void Withdraw(double amount)
67: {
68: if (state == RED_STATE)
69: {
70: Console.WriteLine("餘額不足,請存錢");
71: }
72: else if (state == SILVER_STATE)
73: {
74: if (balance > amount)
75: {
76: balance -= amount;
77: Console.WriteLine("馬上吐出現金");
78: state = RED_STATE;
79: }
80: else if (balance == amount)
81: {
82: balance = 0;
83: state = RED_STATE;
84: }
85: else
86: {
87: Console.WriteLine("餘額不足");
88: }
89: }
90: else if (state == GOLD_STATE)
91: {
92: if (balance > amount)
93: {
94: balance -= amount;
95: if (balance < 1000)
96: {
97: state = SILVER_STATE;
98: }
99: Console.WriteLine("馬上吐出現金");
100: }
101: else if (balance == amount)
102: {
103: balance = 0;
104: state = RED_STATE;
105: Console.WriteLine("馬上吐出現金");
106: }
107: else
108: {
109: Console.WriteLine("餘額不足");
110: }
111: }
112: Console.WriteLine(string.Format("餘額為{0}", balance));
113: }
114: }
實際測試這個ATM的程式就如下:
1: static void Main()
2: {
3: Console.WriteLine("開戶: ");
4: stateATM tAccount = new stateATM(1500);
5: Console.WriteLine("領錢: ");
6: tAccount.Withdraw(1000);
7: Console.WriteLine("領錢: ");
8: tAccount.Withdraw(500);
9: Console.WriteLine("領錢: ");
10: tAccount.Withdraw(500);
11:
12: tAccount.Deposit(600);
13: tAccount.payInterest();//發放利息囉
14:
15: // Wait for user
16: Console.ReadKey();
17: }
感覺似乎狀態都已經受到控制了,但該來的還是躲不掉
需求改變:還記得先前信用卡嗎,當信用卡氾爛之際
白金信用卡、鑽石卡、世界卡紛紛出爐,為的就是標榜不同的信用卡等級
服務也更多更高級。當然也要收取更多的卡費。
銀行這次為了強調理財並促使客戶存款,所以特別開了一個鑽石帳戶
存款利率更高,同時也區別開原本的金色狀態。加入了新的鑽石狀態
那我們要做什麼修改呢?
1.我們必須加入一種新的狀態
const int DIAMOND_STATE = 4
存款為10000~100000,金色狀態,利率為0.10
2.我們要在每一個方法內,包含付利息的方法(payInterest())中加入一個新的條件判斷
光這些就有得我們忙了,混亂的狀態還未到,但已經嗅到一些氣息
假使…未來預借現金的功能也開通了呢?可能的改變都會讓事情變的很混亂
我們是不是可以從過去的準則中找到一些撇步呢?
「或許我們可以這麼做,由狀態物件封裝目前的狀態,因為目前狀態是最有可能變動的部分了」
沒錯這正是封裝會變動的部分守則,我們可以「多用合成,少用繼承」
將動作合成到物件的動作變成結合狀態判斷進行來行為。
說起來仍然抽象,我們來著手新的設計吧(在這之前可以先回憶一下策略模式)
1.定義一個狀態介面,在這個介面內,ATM的每個動作都有一個對應的方法
2.為帳戶的每個狀態實踐狀態類別,這每類別將負責在狀態下進行ATM的行為
3.最後,擺脫舊的條件判斷碼,取而代之的方式是將動作轉介到狀態類別。
定義我們的狀態介面與類別:
所有的狀態都必須繼承介面。這裡所宣告的方法,都直接對應到ATM對銀行帳戶的動作。
每個狀態都封裝成一個類別,現在改重新改造銀行帳戶的狀態吧。
1: /// <summary>
2: /// The 'Context' class
3: /// </summary>
4: class Account
5: {
6: private State _state;
7: private string _owner;
8:
9: // Constructor
10: public Account(string owner)
11: {
12: // New accounts are 'Red' by default
13: this._owner = owner;
14: this._state = new RedState(0.0, this);
15: Console.WriteLine(string.Format("開戶成功,餘額:{0}", this.Balance));
16: Console.WriteLine(string.Format("狀態:{0}", this.State.GetType().Name));
17:
18: }
19:
20: // Properties
21: public double Balance
22: {
23: get { return _state.Balance; }
24: }
25:
26: public State State
27: {
28: get { return _state; }
29: set { _state = value; }
30: }
31:
32: public void Deposit(double amount)
33: {
34: _state.Deposit(amount);
35: Console.WriteLine("存款 {0:C} --- ", amount);
36: Console.WriteLine(" 餘額 = {0:C}", this.Balance);
37: Console.WriteLine(" 狀態 = {0}\n",
38: this.State.GetType().Name);
39: }
40:
41: public void Withdraw(double amount)
42: {
43: _state.Withdraw(amount);
44: Console.WriteLine("取款 {0:C} --- ", amount);
45: Console.WriteLine(" 餘額 = {0:C}", this.Balance);
46: Console.WriteLine(" 狀態 = {0}\n",
47: this.State.GetType().Name);
48: }
49:
50:
51: public void PayInterest()
52: {
53: _state.PayInterest();
54: Console.WriteLine("利息支付日 --- ");
55: Console.WriteLine(" 餘額 = {0:C}", this.Balance);
56: Console.WriteLine(" 狀態 = {0}\n",
57: this.State.GetType().Name);
58: }
59: }
就像策略模式,我將狀態這個介面定義的行為,定義成帳戶的一個實體變數型別(第6行)
接著,我們對於狀態介面該做的事其實很清楚,就包含了存款、取款、支付利息以及取得餘額的資訊。
現在我們的帳戶行為只需要實踐帳戶該有的行為就好了
因為我們知道,狀態會增加,但銀行帳戶的行為不變。
所以我們現在將狀態封裝在狀態介面中(State),接著來看看狀態究竟如何設計呢?
狀態的抽象類別(作為共同的介面)
1: abstract class State
2: {
3: protected Account account;
4: protected double balance;
5:
6: protected double interest;
7: protected double lowerLimit;
8: protected double upperLimit;
9:
10: // Properties
11: public Account Account
12: {
13: get { return account; }
14: set { account = value; }
15: }
16:
17: public double Balance
18: {
19: get { return balance; }
20: set { balance = value; }
21: }
22:
23: public abstract void Deposit(double amount);
24: public abstract void Withdraw(double amount);
25: public abstract void PayInterest();
26: }
我們對狀態先做一個共同的定義,因為不同的狀態,我們認為利率不一樣,而且狀態的判斷準則是來自於餘額的數目
因此我們定義出這些欄位以及抽象方法,供繼承狀態的這些狀態來實作不同的邏輯。
隨便找一個好了:
RedState的狀態實作:
1: class RedState : State
2: {
3: // Constructor
4: public RedState(State state)
5: {
6: this.balance = state.Balance;
7: this.account = state.Account;
8: Initialize();
9: }
10:
11: public RedState(double balance, Account account)
12: {
13: this.balance = balance;
14: this.account = account;
15: Initialize();
16: }
17:
18: private void Initialize()
19: {
20: // Should come from a datasource
21: interest = 0.0;
22: lowerLimit = 0.0;
23: upperLimit = 0.0;
24: }
25:
26: public override void Deposit(double amount)
27: {
28: balance += amount;
29: StateChangeCheck();
30: }
31:
32: public override void Withdraw(double amount)
33: {
34: Console.WriteLine("存款餘額不足");
35: }
36:
37: public override void PayInterest()
38: {
39: //沒有餘額,自然沒有利息,不須實作
40: }
41:
42: private void StateChangeCheck()
43: {
44: if (balance > upperLimit)
45: {
46: account.State = new SilverState(this);
47: }
48: }
49: }
我們在這個RedState狀態的建構式中,撰寫了兩組方法,參數包含將帳戶物件傳入,而其中一組是可以直接傳入帳戶餘額
這是為了讓帳戶在被操作的時候,可以在其建構式中,建立初始化餘額的行為(當然也可以結合DataBase,來取得帳戶資訊)。
Initialize()的方法則定義了這個狀態中不同邏輯的差異,包含利率、餘額上下限(決定狀態用的)。
而這個類別中同時必須實踐繼承狀態抽象類別的方法
也請注意,對於這個類別沒有意義的方法,我們可以不需要去實踐,例如payInterest()
若餘額是0,利率則再高,也沒有利息,因此我們可以不實踐其中的方法。
當餘額是0的時候,也無法進行提款,因此也會造成存款餘額不足的訊息顯示回去。
其中比較重要的是(StateChangeCheck的行為。也就是說我們在進行存款、取款的時候
這些狀態都有可能再度改變,所以我們對於可能會引起帳戶改變的方法內,都要進行狀態的 Check
同理可推,我們馬上就知道如何來SilverState了
1: class SilverState : State
2: {
3: // Overloaded constructors
4: public SilverState(State state) :
5: this(state.Balance, state.Account)
6: {
7: }
8:
9: public SilverState(double balance, Account account)
10: {
11: this.balance = balance;
12: this.account = account;
13: Initialize();
14: }
15:
16: private void Initialize()
17: {
18: // Should come from a datasource
19: interest = 0.01;
20: lowerLimit = 0.0;
21: upperLimit = 1000.0;
22: }
23:
24: public override void Deposit(double amount)
25: {
26: balance += amount;
27: StateChangeCheck();
28: }
29:
30: public override void Withdraw(double amount)
31: {
32: if (amount == balance)
33: {
34: balance -= amount;
35: StateChangeCheck();
36: }
37: else if (amount > balance)
38: {
39: Console.WriteLine("存款不足");
40: }
41: else
42: {
43: balance -= amount;
44: StateChangeCheck();
45: }
46: }
47:
48: public override void PayInterest()
49: {
50: balance += interest * balance;
51: StateChangeCheck();
52: }
53:
54: private void StateChangeCheck()
55: {
56: if (balance <= lowerLimit)
57: {
58: account.State = new RedState(this);
59: }
60: else if (balance > upperLimit)
61: {
62: account.State = new GoldState(this);
63: }
64: }
GoldState跟DiamondState就先不多說,來實際測試看看吧
1: static void Main()
2: {
3: //// Open a new account
4: Account account = new Account("Huang IP");
5:
6: //// Apply financial transactions
7: account.Deposit(200.0);
8: account.Deposit(300.0);
9: account.Deposit(550.0);
10: account.PayInterest();
11: account.Withdraw(2000.00);
12: account.Withdraw(1100.00);
13: // Wait for user
14: Console.ReadKey();
15: }
應該也可以自己想到GoldState跟DiamondState如何實踐了
最後我們同樣來定義一下狀態模式吧…
狀態模式
允許物件隨著內在的狀態改變而改變行為,好像物件的類別改變了一樣
狀態模式的類別圖
這個類別圖與策略模式的類別圖幾乎是一樣的
只是這兩個模式的差別在他們的「Intention」(意圖)(以下摘率自
以狀態模式而言,一群行為封裝在狀態物件中,而客戶對於狀態物件所知不多。
因為狀態其實是隨著Context的行為一起改變(就像透過餘額進行更換狀態)。
但策略模式其是由客戶主動指定Context所要合成的策略物件為何。
一般來說,要將策略模式想成是除了繼承之外,有更彈性的替代方案
因為如何透過繼承,你將被這個行為困住,甚至想要修改它都很難。
有了策略模式,你可以藉由組合不同的物件改變行為。
而狀態模式想成是不用在context中放置許多條件判斷的替代方案,藉由行為包裝進入狀態物件中
只要在Context內改變狀態物件(帳戶Account物件),就可以改變Context的行為了。
未來也請注意,當有任何需求再加進來的時候,別輕易的去修改原有的狀態(可以加新的狀態),
例如將金色類別中,未來若加入了特定期間的優惠利率或是抽獎,也不輕易的去直接修改GoldState的類別
因為這可能違反了一個類別一個責任的原則,萬一未來這只是一個暫時性的活動。
又要將利率改變或者是有不同獎勵條件呢?其實這些都必須抑賴你的智慧來決定了。
參考資料
HeadFirst DesignPattern