引子:無盡加班何時休#
小菜已連續四天加班,週末估計也跑不掉。經理把工作排得滿滿,做完才能下班——但沒人下班前能做完。
上午工作精神百倍 → 中午想睡 → 下午略恢復 → 傍晚加班疲累 → 深夜不行了睡著了。
這是狀態的變化:不同的時間,會有不同的狀態。
第一版:函數式判斷#
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 子類,這樣物件就可以不依賴於其他物件而獨立變化——客戶將來增減業務狀態或改變狀態流程,都不再困難。