狀態模式(State Pattern)

狀態模式(State Pattern)

策略模式以及狀態模式是雙胞胎

策略模式是透過建立演算法的家族,可以在執行期間動態地改變物件的行為。

 

那狀態模式就是藉由改變物件自身的狀態,讓物件自己控制自己的行為。

 

這次我們的需求如下:

我們要製作一個銀行帳戶系統,它存有銀行帳戶餘額、狀態等資訊

我們的客戶透過與ATM 與銀行帳戶交涉,進行帳戶的管理。

我們的客戶包含了銀行本身,以及存取款的帳戶持有人。

因此動作支援了存取款以及銀行定期支付利息

目前不同的狀態會有不同的利率水準。

 

image

 

這是一張需求說明,我們如何從其中的狀態得到真正的程式呢?

以下是如何實踐狀態機的方法(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: }

image

感覺似乎狀態都已經受到控制了,但該來的還是躲不掉

需求改變:還記得先前信用卡嗎,當信用卡氾爛之際

白金信用卡、鑽石卡、世界卡紛紛出爐,為的就是標榜不同的信用卡等級

服務也更多更高級。當然也要收取更多的卡費。

 

銀行這次為了強調理財並促使客戶存款,所以特別開了一個鑽石帳戶

存款利率更高,同時也區別開原本的金色狀態。加入了新的鑽石狀態

那我們要做什麼修改呢?

1.我們必須加入一種新的狀態

const int DIAMOND_STATE = 4

存款為10000~100000,金色狀態,利率為0.10

2.我們要在每一個方法內,包含付利息的方法(payInterest())中加入一個新的條件判斷

光這些就有得我們忙了,混亂的狀態還未到,但已經嗅到一些氣息

 

假使…未來預借現金的功能也開通了呢?可能的改變都會讓事情變的很混亂

我們是不是可以從過去的準則中找到一些撇步呢?

 

「或許我們可以這麼做,由狀態物件封裝目前的狀態,因為目前狀態是最有可能變動的部分了」

沒錯這正是封裝會變動的部分守則,我們可以「多用合成,少用繼承」

將動作合成到物件的動作變成結合狀態判斷進行來行為。

 

說起來仍然抽象,我們來著手新的設計吧(在這之前可以先回憶一下策略模式)

1.定義一個狀態介面,在這個介面內,ATM的每個動作都有一個對應的方法

2.為帳戶的每個狀態實踐狀態類別,這每類別將負責在狀態下進行ATM的行為

3.最後,擺脫舊的條件判斷碼,取而代之的方式是將動作轉介到狀態類別。

 

定義我們的狀態介面與類別:

image

所有的狀態都必須繼承介面。這裡所宣告的方法,都直接對應到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: }
image

應該也可以自己想到GoldState跟DiamondState如何實踐了

 

最後我們同樣來定義一下狀態模式吧…

狀態模式

允許物件隨著內在的狀態改變而改變行為,好像物件的類別改變了一樣

 

狀態模式的類別圖

image

 

這個類別圖與策略模式的類別圖幾乎是一樣的

只是這兩個模式的差別在他們的「Intention」(意圖)(以下摘率自

以狀態模式而言,一群行為封裝在狀態物件中,而客戶對於狀態物件所知不多。

因為狀態其實是隨著Context的行為一起改變(就像透過餘額進行更換狀態)。

但策略模式其是由客戶主動指定Context所要合成的策略物件為何。

一般來說,要將策略模式想成是除了繼承之外,有更彈性的替代方案

因為如何透過繼承,你將被這個行為困住,甚至想要修改它都很難。

有了策略模式,你可以藉由組合不同的物件改變行為。

而狀態模式想成是不用在context中放置許多條件判斷的替代方案,藉由行為包裝進入狀態物件中

只要在Context內改變狀態物件(帳戶Account物件),就可以改變Context的行為了。

未來也請注意,當有任何需求再加進來的時候,別輕易的去修改原有的狀態(可以加新的狀態),

例如將金色類別中,未來若加入了特定期間的優惠利率或是抽獎,也不輕易的去直接修改GoldState的類別

因為這可能違反了一個類別一個責任的原則,萬一未來這只是一個暫時性的活動。

又要將利率改變或者是有不同獎勵條件呢?其實這些都必須抑賴你的智慧來決定了。

 

參考資料

HeadFirst DesignPattern