重溫《深入淺出設計模式》狀態模式 (Book Review of Head First Design Pattern, State Pattern)

2020-07-08

狀態模式和策略模式師出同門,兩者在 UML 的表示上有相同的結構,同樣是利用執行期實作相同介面的不同類別的方法,以多型的方式精簡原本需要用 if else 邏輯來控制的程式流程。兩者的關鍵差別在於使用上的意圖,狀態模式是將物件的狀態封裝為類別,並藉由類別的轉換從而多型地調用方法;策略模式則是在執行期使用依據實作相同介面的不同類別,使用其專有的演算邏輯並避免掉繼承關係所衍生的維護困難。

logo

狀態模式

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

狀態模式的 UML

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,關於演算法的知識交給狀態類別去實踐

✔️單一類別單一責任
單一「狀態」單一責任

參考資料