重溫《深入淺出設計模式》狀態模式 (Book Review of Head First Design Pattern, State Pattern)
2020-07-08
狀態模式和策略模式師出同門,兩者在 UML 的表示上有相同的結構,同樣是利用執行期實作相同介面的不同類別的方法,以多型的方式精簡原本需要用 if else 邏輯來控制的程式流程。兩者的關鍵差別在於使用上的意圖,狀態模式是將物件的狀態封裝為類別,並藉由類別的轉換從而多型地調用方法;策略模式則是在執行期使用依據實作相同介面的不同類別,使用其專有的演算邏輯並避免掉繼承關係所衍生的維護困難。
狀態模式
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Before Pattern
邏輯條件全部塞在單一類別的方法中,不僅混亂也不利於日後變動需求😖。
public enum State
{
Sold, HasQuarters, NoQuarters, NoGumballs
}
public class GumballMachine
{
private int _count;
private State _state = State.NoQuarters;
public GumballMachine(int count)
{
_count = count;
}
public void InsertQuarter()
{
switch (_state)
{
case State.NoQuarters:
_state = State.HasQuarters;
Console.WriteLine("Inserted a quarter");
break;
case State.Sold:
Console.WriteLine("Please wait for current gumball to come out");
break;
case State.HasQuarters:
Console.WriteLine("Can't add more quarters");
break;
case State.NoGumballs:
Console.WriteLine("Out of Stock");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public void EjectQuarter()
{
...
}
public void TurnCrank()
{
...
}
private void Dispense()
{
...
}
}
After Pattern
IState.cs
public interface IState
{
void InsertQuarter();
void EjectQuarter();
void TurnCrank();
void Dispense();
}
HasQuarterState.cs
狀態的轉換在實作介面的狀態類別進行。
public class HasQuarterState : IState
{
private GumballMachine _machine;
private readonly Random _random = new Random(DateTime.Now.Millisecond);
public HasQuarterState(GumballMachine gumballMachine)
{
this._machine = gumballMachine;
}
public void Dispense()
{
Console.WriteLine("You Can't Do That.");
}
public void EjectQuarter()
{
Console.WriteLine("Quarter Returned");
_machine.State = _machine.NoQuarterState;
}
public void InsertQuarter()
{
Console.WriteLine("Can't Insert More Than One Coin");
}
public void TurnCrank()
{
Console.WriteLine("Turned The Crank");
var winner = _random.Next(10);
if ((winner == 7) && (_machine.Count > 1))
{
_machine.State = _machine.WinnerState;
}
else
{
_machine.State = _machine.SoldState;
}
}
}
IState.cs <- SoldOutState.cs
IState.cs <- SoldState.cs
IState.cs <- WinnerState.cs
IState.cs <- NoQuarterState.cs
GumballMachine.cs
Context 負責初始化各封裝的狀態,並多型地調用各狀態類別的方法。
public class GumballMachine
{
public int Count { get; private set; }
public IState SoldOutState;
public IState NoQuarterState;
public IState HasQuarterState;
public IState SoldState;
public IState WinnerState;
public IState State { get; set; }
public GumballMachine(int count)
{
Count = count;
SoldOutState = new SoldOutState(this);
NoQuarterState = new NoQuarterState(this);
HasQuarterState = new HasQuarterState(this);
SoldState = new SoldState(this);
WinnerState = new WinnerState(this);
if (Count > 0)
{
State = NoQuarterState;
}
}
public void InsertQuarter()
{
State.InsertQuarter();
}
public void TurnCrank()
{
State.TurnCrank();
State.Dispense();
}
public void EjectQuarter()
{
State.EjectQuarter();
}
public void ReleaseBall()
{
Console.WriteLine("A ball comes rolling down.");
Count--;
}
public void Refill(int count)
{
Count = count;
State = NoQuarterState;
}
}
Program.cs
var gumballmachien = new GumballMachine(5);
while (gumballmachien.Count > 0)
{
gumballmachien.InsertQuarter();
gumballmachien.TurnCrank();
Console.WriteLine("-------------------------------------");
}
小結
✔️將變動的部分封裝起來
✔️類別應該對需求保持開放;應該對既有的程式碼修改保持關閉
狀態可能會變動;狀態的方法可能會變動,封裝為類別
✔️多用合成,少用繼承
✔️讓需要互動的物件之間的關係鬆綁
狀態類別是以合成的方式在 Context 中調用
✔️針對介面撰寫,而非針對實踐方式撰寫。
狀態作為 ISatae Interface ,狀態類別去實作此 Interface
✔️讓物件保有最少的知識
Context 只需要認識 IState,關於演算法的知識交給狀態類別去實踐
✔️單一類別單一責任
單一「狀態」單一責任