昨天用 State 把訂單的狀態機拆成物件,讓「現在能不能做這件事」交給狀態自己決定
今天要處理的是另一種常見的情境:一個請求進來,不知道最後會是誰處理,只知道「先讓某個人看看,他不能處理就換下一個人看」
原文的定義,來自 Design Patterns: Elements of Reusable Object-Oriented Software
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
用中文理解可以是
避免把請求的發送者和接收者綁在一起,讓多個物件都有機會處理同一個請求。把這些物件串成一條鏈,讓請求沿著鏈往下傳,直到有物件願意處理它為止
我們來看看 UML
核心角色
舉個生活化的例子,公司的請假簽核,依請假天數不同,會由不同層級的主管核准
員工遞出去的請假單,並不知道最後會是誰核准,只知道先送給組長
public interface IApprover // Handler 處理者介面
{
IApprover SetNext(IApprover next); // 設定下一位,並回傳下一位方便串接
void Approve(LeaveRequest request);
}
public class LeaveRequest
{
public string EmployeeName { get; set; }
public int Days { get; set; }
}
public class TeamLeadApprover : IApprover // 組長 ConcreteHandler(具體處理者)
{
private IApprover _next;
public IApprover SetNext(IApprover next)
{
_next = next;
return next;
}
public void Approve(LeaveRequest request)
{
if (request.Days <= 3)
Console.WriteLine($"[組長] 核准 {request.EmployeeName} 請假 {request.Days} 天");
else
_next?.Approve(request); // 天數超過權限,交給下一位
}
}
public class ManagerApprover : IApprover // 經理
{
private IApprover _next;
public IApprover SetNext(IApprover next)
{
_next = next;
return next;
}
public void Approve(LeaveRequest request)
{
if (request.Days <= 7)
Console.WriteLine($"[經理] 核准 {request.EmployeeName} 請假 {request.Days} 天");
else
_next?.Approve(request);
}
}
public class HRApprover : IApprover // 人資,鏈尾,全部核准
{
public IApprover SetNext(IApprover next) => next;
public void Approve(LeaveRequest request)
=> Console.WriteLine($"[人資] 核准 {request.EmployeeName} 請假 {request.Days} 天");
}
var teamLead = new TeamLeadApprover();
var manager = new ManagerApprover();
var hr = new HRApprover();
teamLead.SetNext(manager).SetNext(hr); // 組長 → 經理 → 人資
teamLead.Approve(new LeaveRequest { EmployeeName = "小明", Days = 2 }); // [組長] 核准 小明 請假 2 天
teamLead.Approve(new LeaveRequest { EmployeeName = "小華", Days = 5 }); // [經理] 核准 小華 請假 5 天
teamLead.Approve(new LeaveRequest { EmployeeName = "小美", Days = 10 }); // [人資] 核准 小美 請假 10 天
來釐清一下整個過程,呼叫的入口是 teamLead.Approve(request),所以每一筆請假單都是先經過組長:
不管請假天數是多少,員工永遠只找組長遞假單,teamLead.Approve(...) 這一行程式碼不用變,天數該給誰核准,是每一位 Approver 自己判斷、自己決定要不要往下傳的事
柴咖啡連鎖化之後,客訴量早就不是店員一個人扛得住的規模,而且不同客訴的嚴重程度差很多
有些是「飲料太甜、退一杯的錢就沒事」,有些是「飲料裡喝到異物」,屬於食安層級,不是退錢能打發的事
阿柴這次直接照店內三個角色的權責範圍設計責任鏈
先定義客訴這個請求本身,跟處理者共同的介面
public class Complaint
{
public string OrderId { get; set; }
public int RefundAmount { get; set; }
public bool IsFoodSafetyIssue { get; set; }
public string Description { get; set; }
}
public interface IComplaintHandler
{
IComplaintHandler SetNext(IComplaintHandler next);
void Handle(Complaint complaint);
}
店員、店長各自守住自己的權限範圍,接不住的往下一棒傳
public class StaffHandler : IComplaintHandler // 店員
{
private IComplaintHandler _next;
public IComplaintHandler SetNext(IComplaintHandler next)
{
_next = next;
return next;
}
public void Handle(Complaint complaint)
{
if (!complaint.IsFoodSafetyIssue && complaint.RefundAmount <= 100)
Console.WriteLine($"[店員] 受理訂單 {complaint.OrderId},退款 {complaint.RefundAmount} 元");
else
_next?.Handle(complaint);
}
}
public class StoreManagerHandler : IComplaintHandler // 店長
{
private IComplaintHandler _next;
public IComplaintHandler SetNext(IComplaintHandler next)
{
_next = next;
return next;
}
public void Handle(Complaint complaint)
{
if (!complaint.IsFoodSafetyIssue && complaint.RefundAmount <= 500)
Console.WriteLine($"[店長] 受理訂單 {complaint.OrderId},退款 {complaint.RefundAmount} 元");
else
_next?.Handle(complaint);
}
}
客服中心是鏈尾,兩種情況都會落到這裡;核准退款時,不重新發明一套退款邏輯,而是沿用 Day 24 已經做好的 RefundCommand、丟進 SupportConsole 去執行,客訴退款一樣享有撤銷/重做的能力
public class HeadquartersComplaintHandler : IComplaintHandler // 客服中心,鏈尾
{
private readonly SupportConsole _supportConsole;
private readonly PaymentGateway _paymentGateway;
public HeadquartersComplaintHandler(SupportConsole supportConsole, PaymentGateway paymentGateway)
{
_supportConsole = supportConsole;
_paymentGateway = paymentGateway;
}
public IComplaintHandler SetNext(IComplaintHandler next) => next; // 鏈尾,沒有下一棒了
public void Handle(Complaint complaint)
{
var reason = complaint.IsFoodSafetyIssue ? "食安問題,另外走通報流程" : "退款金額超過店長權限";
Console.WriteLine($"[客服中心] 受理訂單 {complaint.OrderId}({reason})");
_supportConsole.ExecuteCommand(new RefundCommand(_paymentGateway, complaint.OrderId, complaint.RefundAmount));
}
}
var supportConsole = new SupportConsole();
var paymentGateway = new PaymentGateway();
IComplaintHandler staff = new StaffHandler();
IComplaintHandler manager = new StoreManagerHandler();
IComplaintHandler headquarters = new HeadquartersComplaintHandler(supportConsole, paymentGateway);
staff.SetNext(manager).SetNext(headquarters); // 店員 → 店長 → 客服中心
// 客訴一 :
staff.Handle(new Complaint { OrderId = "ORD2001", RefundAmount = 50, Description = "飲料太甜" });
// [店員] 受理訂單 ORD2001,退款 50 元
// 客訴二 :
staff.Handle(new Complaint { OrderId = "ORD2002", RefundAmount = 300, Description = "少了燕麥奶加購" });
// 店員接不住 → [店長] 受理訂單 ORD2002,退款 300 元
// 客訴三 :
staff.Handle(new Complaint { OrderId = "ORD2003", RefundAmount = 20, IsFoodSafetyIssue = true, Description = "飲料裡有異物" });
// 金額很小,但食安問題直接跳過店員、店長 → [客服中心] 受理訂單 ORD2003(食安問題,另外走通報流程),並執行 RefundCommand
呼叫端只會找 staff.Handle(complaint),這個客訴最後會落在誰手上、要不要真的退款,都是處理者自己的事,跟 Day 24 建立的退款/補償機制接起來後,也不用重新煩惱「這筆退款怎麼執行、能不能撤銷」這件事
我們來看看 Mermaid 的類別架構圖(Class Diagram)會怎麼呈現
朋友如果你第一次看 Mermaid 架構圖,這是小指南
方框內部三層結構:
上層:類別或介面名稱(標註 <<interface>> 代表介面合約)
中層:屬性與私有欄位(- / + 代表 private / public)
下層:對外提供的方法 (- / + 代表 private / public)
箭頭符號的意義:
◄--(虛線箭頭):實作介面,代表類別遵守合約規定
—►(實線箭頭):依賴/呼叫,箭頭指向被使用的一方
⟣—►(實線菱形):菱形持有/組合箭頭方的實例,代表內部包含該物件的參考
如果處理層級只有一兩層、規則長期不太會變(例如只有「店員/總部」兩層,而且金額門檻幾乎不會調整),拆成好幾個 Handler 類別,可能比一段簡單的 if-else 更麻煩
責任鏈划算的情境,是處理層級數量多、判斷規則之後還會持續增加或調整,而且希望能在執行期動態組裝、調整鏈的順序,而不是寫死在一段條件式裡
它也有自己的代價:
SetNext 呼叫的先後)本身變成一種隱性規則,順序調換就會改變行為,卻不會有編譯期的錯誤提醒你Chain of Responsibility 要解決的問題:
一個請求可能需要被多個候選者依序檢查是否要處理,若把所有判斷邏輯寫成一長串 if-else 集中在一處,新增或調整處理規則時,順序與條件會彼此牽動、容易顧此失彼
優點:
缺點:
明天,柴咖啡的訂單,牽涉到廚房、收銀、外送平台三個角色互相通知,如果讓它們互相直接呼叫對方,任何一個角色要換掉都會牽動所有跟它有往來的角色,換 Mediator 中介者模式上場,讓一個訂單協調員居中協調
Refactoring Guru - Chain of Responsibility
C# Chain of Responsibility Design Pattern