引子:無盡加班何時休#

小菜已連續四天加班,週末估計也跑不掉。經理把工作排得滿滿,做完才能下班——但沒人下班前能做完。

上午工作精神百倍 → 中午想睡 → 下午略恢復 → 傍晚加班疲累 → 深夜不行了睡著了。

這是狀態的變化:不同的時間,會有不同的狀態。

第一版:函數式判斷#

static int Hour = 0;
static bool WorkFinished = false;

public static void WriteProgram()
{
    if (Hour < 12)        Console.WriteLine($"當前時間:{Hour} 點 上午工作,精神百倍");
    else if (Hour < 13)   Console.WriteLine($"當前時間:{Hour} 點 餓了,犯睏,午休");
    else if (Hour < 17)   Console.WriteLine($"當前時間:{Hour} 點 下午狀態還不錯,繼續努力");
    else
    {
        if (WorkFinished) Console.WriteLine($"當前時間:{Hour} 點 下班回家了");
        else if (Hour < 21) Console.WriteLine($"當前時間:{Hour} 點 加班哦,疲累之極");
        else                Console.WriteLine($"當前時間:{Hour} 點 不行了,睡著了。");
    }
}

這是面向過程的代碼。即使改成 Work 類別,WriteProgram 方法也太長了。

第二版:分類版(仍是 Long Method)#

把欄位封裝進 Work 類,但 WriteProgram 方法依然又臭又長。

Martin Fowler 在《重構》中指出,方法過長就極可能有「Long Method 壞味道」。

此方法有大量判斷分支 → 責任過大

任何時段的需求變動(例如「員工必須在 20 點前離開」)都要動到整個方法,違反開放-封閉原則

狀態模式#

狀態模式(State Pattern):當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類別。[DP]

主要解決的是「控制物件狀態轉換的條件表達式過於複雜」的情況。

把狀態的判斷邏輯轉移到表示不同狀態的一系列類別中,可把複雜判斷簡化。

如果狀態判斷很簡單,就沒必要用狀態模式——別過度設計。

結構#

  • State(抽象狀態):定義介面以封裝與 Context 某一特定狀態相關的行為
  • ConcreteState(具體狀態):每個子類實作與 Context 一個狀態相關的行為,並負責設定下一狀態
  • Context(上下文):維護一個 ConcreteState 子類的實例,這個實例定義當前的狀態
classDiagram
    class Context {
        -State state
        +Request()
    }
    class State {
        <<abstract>>
        +Handle(Context)*
    }
    class ConcreteStateA
    class ConcreteStateB
    Context o--> State
    State <|-- ConcreteStateA
    State <|-- ConcreteStateB

工作時段的狀態轉移:

stateDiagram-v2
    [*] --> ForenoonState
    ForenoonState --> NoonState : Hour ≥ 12
    NoonState --> AfternoonState : Hour ≥ 13
    AfternoonState --> EveningState : Hour ≥ 17
    EveningState --> RestState : TaskFinished
    EveningState --> SleepingState : Hour ≥ 21
    RestState --> [*]
    SleepingState --> [*]
abstract class State
{
    public abstract void Handle(Context context);
}

class ConcreteStateA : State
{
    public override void Handle(Context context)
    {
        context.State = new ConcreteStateB();
    }
}

class Context
{
    private State state;
    public Context(State state) { this.state = state; }
    public State State
    {
        get => state;
        set { state = value; Console.WriteLine($"當前狀態:{state.GetType().Name}"); }
    }
    public void Request() => state.Handle(this);
}

第三版:用狀態模式改寫工作流程#

把每個時段的狀態獨立成類,每個類負責判斷是否切換到下一狀態

public abstract class State
{
    public abstract void WriteProgram(Work w);
}

public class ForenoonState : State
{
    public override void WriteProgram(Work w)
    {
        if (w.Hour < 12)
            Console.WriteLine($"當前時間:{w.Hour} 點 上午工作,精神百倍");
        else
        {
            w.SetState(new NoonState());
            w.WriteProgram();
        }
    }
}

public class NoonState : State { /* < 13 點:午休;否則 → AfternoonState */ }
public class AfternoonState : State { /* < 17 點:下午狀態;否則 → EveningState */ }

public class EveningState : State
{
    public override void WriteProgram(Work w)
    {
        if (w.TaskFinished)
        {
            w.SetState(new RestState());
            w.WriteProgram();
            return;
        }
        if (w.Hour < 21)
            Console.WriteLine($"當前時間:{w.Hour} 點 加班哦,疲累之極");
        else
        {
            w.SetState(new SleepingState());
            w.WriteProgram();
        }
    }
}

public class SleepingState : State { /* 不行了,睡著了 */ }
public class RestState     : State { /* 下班回家了 */ }

工作類:

public class Work
{
    private State current;
    public Work() { current = new ForenoonState(); }

    public double Hour { get; set; }
    public bool TaskFinished { get; set; }

    public void SetState(State s) { current = s; }
    public void WriteProgram() => current.WriteProgram(this);
}

客戶端程式碼完全不變,但程式變得更加靈活易變。

若要符合「員工必須 20 點前離開」的新規則,只需新增一個「強制下班狀態」並修改 EveningState 的判斷——其他狀態類完全不受影響。

狀態模式的好處#

將與特定狀態相關的行為局部化,並且將不同狀態的行為分割開來。[DP]

所有與某狀態相關的程式碼都存在於某個 ConcreteState 中,透過定義新的子類可以很容易地增加新的狀態與轉換。[DP]

說白了,狀態模式的目的是消除龐大的條件分支語句。把整片版面拆成一個又一個的活字,容易維護和擴展

何時使用?#

  • 一個物件的行為取決於它的狀態,並且必須在運行時根據狀態改變它的行為
  • 業務需求中某項業務有多個狀態,狀態變化依靠大量的多分支判斷語句來實現

把每一種業務狀態定義為一個 State 子類,這樣物件就可以不依賴於其他物件而獨立變化——客戶將來增減業務狀態或改變狀態流程,都不再困難。