21. State


Posted by WayneCheng on 2021-01-28

關於 State 本篇將討論以下幾個問題

1. 關於 State

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 State

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

by Gang of Four

  • 當物件的內部狀態改變時,允許改變其行為
  • 物件看起來像改變了類別

State(狀態)屬於行為型(Behavioral Patterns),當遇到大量 if/else 或 switch 時,可藉由 State 來將判斷邏輯抽離原本物件,並將每個狀態邏輯拆分開來,各自處理單一狀態與邏輯,降低物件本身複雜度。

優點:

  • 符合 單一職責原則(Single Responsibility Principle)
  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 隨著狀態數量提高,class 數量增加會造成整體程式複雜度提升

2. UML

Class 間關聯:

  • Context 可包含 State
  • ConcreteState A/B 繼承 State

Class:

  • Context:需要判斷狀態來運作的物件
  • State:狀態的抽象類別或介面
  • ConcreteState:狀態實做

3. 將 UML 轉為程式碼

需要判斷狀態來運作的物件

/// <summary>
/// 需要判斷狀態來運作的物件
/// </summary>
public class Context
{
    private IState _state;

    public Context(IState state)
    {
        State = state;
    }

    public IState State
    {
        get => _state;
        set
        {
            _state = value;
            Console.WriteLine($"State: {_state.GetType().Name}");
        }
    }

    public void Request()
    {
        _state.Handle(this);
    }
}

狀態的介面

/// <summary>
/// 狀態的介面
/// </summary>
public interface IState
{
    void Handle(Context context);
}

狀態實做

/// <summary>
/// 狀態實做 A
/// </summary>
public class ConcreteStateA : IState
{
    public void Handle(Context context)
    {
        context.State = new ConcreteStateB();
    }
}

/// <summary>
/// 狀態實做 B
/// </summary>
public class ConcreteStateB : IState
{
    public void Handle(Context context)
    {
        context.State = new ConcreteStateA();
    }
}
  1. 建立context並給定初始狀態
  2. 依據當前狀態呼叫狀態實做
static void Main(string[] args)
{
    Default.Context context = new Default.Context(new Default.ConcreteStateA());

    context.Request();
    context.Request();
    context.Request();
    context.Request();

    Console.ReadLine();
}

執行結果

State: ConcreteStateA
State: ConcreteStateB
State: ConcreteStateA
State: ConcreteStateB
State: ConcreteStateA

4. 情境

我們接到了一個餐飲部麵包店看板要隨著時間改變內容的需求

  • 早上十點到下午兩點「麵包出爐」
  • 下午兩點到八點「麵包搭配咖啡打八折」
  • 晚上八點到九點「麵包全面七折」
  • 其餘時間不營業

狀態的介面

/// <summary>
/// 狀態的介面
/// </summary>
public interface IState
{
    void Handle(BreadStore breadStore, int time);
}

狀態實做,處理每個時間區間狀態

/// <summary>
/// 還沒營業喔
/// </summary>
public class Close : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time < 10 || time > 21)
        {
            Console.WriteLine($"現在時間:{time} 點, 還沒營業喔~");
        }
        else
        {
            breadStore.State = new FreshBread();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包出爐囉
/// </summary>
public class FreshBread : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 10 && time < 14)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包出爐囉~");
        }
        else
        {
            breadStore.State = new BreadAndCoffee();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包搭配咖啡打八折
/// </summary>
public class BreadAndCoffee : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 14 && time < 20)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包搭配咖啡打八折喔~");
        }
        else
        {
            breadStore.State = new ClearingSale();
            breadStore.Request(time);
        }
    }
}

/// <summary>
/// 麵包全面七折喔
/// </summary>
public class ClearingSale : IState
{
    public void Handle(BreadStore breadStore, int time)
    {
        if (time >= 20)
        {
            Console.WriteLine($"現在時間:{time} 點, 麵包全面七折喔~");
        }
        else
        {
            breadStore.State = new Close();
            breadStore.Request(time);
        }
    }
}

麵包店

/// <summary>
/// 麵包店
/// </summary>
public class BreadStore
{
    public IState State { get; set; }

    public BreadStore(IState state)
    {
        State = state;
    }

    public void Request(int time)
    {
        State.Handle(this, time);
    }
}
  1. 建立breadStore並給定初始狀態
  2. 依據當前狀態呼叫狀態實做
static void Main(string[] args)
{
    Situation.BreadStore breadStore = new Situation.BreadStore(new Situation.Close());

    breadStore.Request(9);
    breadStore.Request(10);
    breadStore.Request(13);
    breadStore.Request(14);
    breadStore.Request(20);

    Console.ReadLine();
}

執行結果

現在時間:9 點, 還沒營業喔~
現在時間:10 點, 麵包出爐囉~
現在時間:13 點, 麵包出爐囉~
現在時間:14 點, 麵包搭配咖啡打八折喔~
現在時間:20 點, 麵包全面七折喔~

完整程式碼

GitHub:Behavioral_08_State


總結

一開始處理到 if/else 或 switch 判斷數量不多時,不用急著套用 State 來抽離判斷部分邏輯,少量的 if/else 或 switch 在維護上還是相對容易的,且未來若是沒有新增判斷的需求,那這部分反而是過度設計了。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

【Day06】Vue-cli & Vuex mapState

【Day06】Vue-cli & Vuex mapState

Ruby on Rails 第一步 學習筆記

Ruby on Rails 第一步 學習筆記

政策不確定下的投資風險

政策不確定下的投資風險


Comments