昨天用 Template Method 把飲料製作的 SOP 鎖進父類別,今天延續行為型 Pattern
原文的定義,來自 Design Patterns: Elements of Reusable Object-Oriented Software
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
用中文理解可以是
允許一個物件在其內部狀態改變時改變其行為。該物件看起來就像是改變了所屬的類別一樣
State 主要要解決的是
一個物件的行為需要隨著內部狀態不同而不同,同一個請求在不同狀態下必須有不同的合法性判斷與反應
如果用 if/switch 條件判斷檢查目前狀態來決定該怎麼做,這些規則會分散在每一個呼叫的地方各自判斷一次,新增或修改狀態時很容易顧此失彼
核心角色
我們來看看 UML
舉個生活化的例子,音樂播放器只有一顆播放鍵,但按下去的效果,看目前播放器在哪個狀態
同一顆按鍵、同一個方法呼叫,因為「目前狀態」不同而做不同的事、轉去不同的下一個狀態
public interface IPlayerState // 定義共同介面
{
void PressPlay(MusicPlayer player); // 定義方法 PressPlay 帶入實例 player
}
public class StoppedState : IPlayerState // ⏹ 停止狀態 class
{
public void PressPlay(MusicPlayer player) // 實作 PressPlay 方法,傳入實體化物件 player
{
Console.WriteLine("[播放器] 開始播放"); // 停止狀態按下 Play 就開始撥放
player.SetState(new PlayingState()); // 更改實體化物件方法.SetState( ▶ new 播放類別 )
}
}
public class PlayingState : IPlayerState // ▶ 播放狀態 class
{
public void PressPlay(MusicPlayer player)
{
Console.WriteLine("[播放器] 暫停");
player.SetState(new PausedState()); // 更改實體化物件方法.SetState( ⏸ new 播放類別 )
}
}
public class PausedState : IPlayerState // ⏸ 暫停狀態 class
{
public void PressPlay(MusicPlayer player)
{
Console.WriteLine("[播放器] 繼續播放");
player.SetState(new PlayingState()); // 更改實體化物件方法.SetState( ▶ new 播放類別 )
}
}
public class MusicPlayer
{
// Context 只認得「目前狀態」,不知道也不在乎裡面裝的是哪一種狀態
// _state 在執行期間,參考到的永遠是某一個「具體類別」的實體
private IPlayerState _state = new StoppedState();
public void SetState(IPlayerState state) => _state = state;
public void PressPlay() => _state.PressPlay(this); // this -> MusicPlayer
}
var player = new MusicPlayer();
player.PressPlay(); // [播放器] 開始播放
player.PressPlay(); // [播放器] 暫停
player.PressPlay(); // [播放器] 繼續播放
還是不太熟悉嗎,朋友沒關係我也是,那我們再重新跑一次流程,確認每個階段狀態都是熟悉的,把流程轉換成文字
初始狀態:var player = new MusicPlayer();
player 裡面的 _state 就是 實體化 StoppedState
player 的 this 就是 MusicPlayer 實體本身
第一次按開始撥放 player.PressPlay():
呼叫 PressPlay() 執行 實體化 StoppedState.PressPlay 並傳入 MusicPlayer 實體
跑 Console.WriteLine("[播放器] 開始播放")
呼叫 player.SetState(new PlayingState()) 也就是 new 一個全新的 PlayingState 實體 跑 player.SetState(new PlayingState());
這時候 MusicPlayer 內部的 _state 欄位,就被換成新的 PlayingState 實體了
第二次按下 player.PressPlay() (此時 _state 已經是 PlayingState 實體):
呼叫 PressPlay() 執行 PlayingState 實體.PressPlay(this),傳入的還是同一個 MusicPlayer 實體
跑 Console.WriteLine("[播放器] 暫停")
呼叫 player.SetState(new PausedState()), new 一個全新的 PausedState 實體
MusicPlayer 內部的 _state,從 PlayingState 實體 換成這個新的 PausedState 實體
MusicPlayer.PressPlay() 只有一行 _state.PressPlay(this)
它不會知道現在是停止、播放還是暫停,也不用寫條件式去判斷,這件事交給目前的狀態物件自己決定
柴咖啡連鎖化之後,一張訂單原本只有接單、製作中、完成三個階段,現在小黑要阿柴多加一個「取餐」:客人到店取走飲料,這張訂單才算真的結束
小黑同時也訂了取消規則:接單、製作中都能取消
But 一旦進入完成,代表飲料已經做好在等客人,不能再取消
如果訂單本身只是拿著一個 OrderStatus 這樣的列舉值,那「現在能不能取消」「按下一步該變成什麼狀態」這些規則,就得在每一個會呼叫取消、呼叫推進訂單的地方各自重新判斷一次現在是什麼狀態
規則以後只會越堆越多,像是逾時未取餐要怎麼處理,散落在下單、廚房出單、客服協助取消等各種呼叫端,新增一個階段時很容易漏改其中一處
阿柴這次決定把「狀態」本身拆成物件,讓每個狀態自己知道自己能做什麼、不能做什麼、做完之後要換成哪一個狀態
先定義這個情境下訂單可能收到的兩種請求:推進到下一階段、嘗試取消
public interface IOrderState
{
string Name { get; }
void Proceed(Order order); // 推進到下一階段
void Cancel(Order order); // 嘗試取消
}
接單、製作中、已完成、已取餐、已取消,一共五個狀態
各自決定「在我這個狀態,這兩件事該怎麼反應」
public class PendingState : IOrderState // 接單
{
public string Name => "接單";
public void Proceed(Order order)
{
Console.WriteLine($"[訂單 {order.OrderId}] 開始製作");
order.SetState(new PreparingState());
}
public void Cancel(Order order)
{
Console.WriteLine($"[訂單 {order.OrderId}] 已取消");
order.SetState(new CancelledState());
}
}
public class PreparingState : IOrderState // 製作中
{
public string Name => "製作中";
public void Proceed(Order order)
{
Console.WriteLine($"[訂單 {order.OrderId}] 製作完成,通知取餐");
order.SetState(new CompletedState());
order.NotifyCompleted(); // 銜接 Day 23 的 IOrderObserver,通知客人與外送平台
}
public void Cancel(Order order)
{
Console.WriteLine($"[訂單 {order.OrderId}] 製作中取消,通知內場停止製作");
order.SetState(new CancelledState());
}
}
public class CompletedState : IOrderState // 已完成,等待取餐
{
public string Name => "已完成";
public void Proceed(Order order)
{
Console.WriteLine($"[訂單 {order.OrderId}] 客人取餐完畢");
order.SetState(new PickedUpState());
}
public void Cancel(Order order)
=> Console.WriteLine($"[訂單 {order.OrderId}] 飲料已完成,無法取消");
}
public class PickedUpState : IOrderState // 已取餐,訂單結束
{
public string Name => "已取餐";
public void Proceed(Order order)
=> Console.WriteLine($"[訂單 {order.OrderId}] 訂單已結束,沒有下一步");
public void Cancel(Order order)
=> Console.WriteLine($"[訂單 {order.OrderId}] 訂單已結束,無法取消");
}
public class CancelledState : IOrderState // 已取消,訂單結束
{
public string Name => "已取消";
public void Proceed(Order order)
=> Console.WriteLine($"[訂單 {order.OrderId}] 訂單已取消,沒有下一步");
public void Cancel(Order order)
=> Console.WriteLine($"[訂單 {order.OrderId}] 訂單已經是取消狀態");
}
Order 改成持有目前狀態物件,Proceed()/Cancel() 兩個方法本身不做判斷,單純轉交給目前的狀態
public class Order
{
public string OrderId { get; set; }
public int TotalPrice { get; set; }
public List<OrderItem> Items { get; set; }
private IOrderState _state = new PendingState(); // 訂單一建立就是接單狀態
private readonly List<IOrderObserver> _observers = new();
public string StatusName => _state.Name;
public void SetState(IOrderState state) => _state = state;
public void Proceed() => _state.Proceed(this);
public void Cancel() => _state.Cancel(this);
public void Subscribe(IOrderObserver observer) => _observers.Add(observer);
public void NotifyCompleted() => _observers.ForEach(o => o.OnOrderCompleted(this));
}
var order = new Order { OrderId = "ORD1001", TotalPrice = 150 };
order.Subscribe(new CustomerNotifier());
order.Subscribe(new DeliveryPlatform());
order.Proceed(); // 接單 → 製作中
order.Proceed(); // 製作中 → 已完成,順便通知客人與外送平台
order.Cancel(); // 已完成,無法取消
order.Proceed(); // 已完成 → 已取餐
同一個 order.Proceed()、同一個 order.Cancel(),呼叫端不用去問「現在狀態是什麼、這個動作准不准」,這些判斷都留在各個狀態類別裡自己決定
之後如果小黑又想加規則,例如逾時未取餐要自動退款,也只需要新增一個 TimeoutState,其他既有的狀態類別不用更改
如果一個物件的狀態只有兩三種、規則長期不太會變(像是一顆開關的開/關),拆成一個 IState 介面加上好幾個具體類別,會比一個列舉配一兩行 if 判斷更笨重,也多了不少要維護的檔案
State 划算的情境,是狀態數量多、狀態之間的轉移規則複雜,而且系統還會持續長出新的狀態或新的規則
柴咖啡的訂單接下來大概還會冒出更多這類需求,每次新增都只需要多寫一個狀態類別,不用回頭改動其他狀態的程式碼
State 要解決的問題:
消除散落在系統各處的 if-else / switch 狀態檢查,讓物件根據「當前狀態」自動做出正確的反應與合法的轉移
優點:
行為獨立高內聚:每個狀態只專注管好自己的規則,Context 不用寫任何條件判斷
擴充符合 OCP:加新狀態只需新增 class,不需回頭修改既有狀態的判斷邏輯
除錯好定位:哪一個階段的行為出錯,直接找對應的狀態類別動刀,不再牽一髮動全身
缺點:
類別數量膨脹:多一個狀態就多一個 class,若狀態單純反而會造成過度設計
看不清全局圖:轉移邏輯散在各個狀態內部,很難一眼看穿完整的狀態轉移流程圖
狀態間具體耦合:狀態類別通常需要直接 new 下一個狀態,彼此之間存在依賴
明天,柴咖啡的客訴處理要從店員一個人扛,變成店員擋不住就往上交給店長,店長擋不住再往上交給客服中心
這種「請求沿著一條處理鏈往下傳,直到有人願意處理為止」的情境,換 Chain of Responsibility 責任鏈模式上場