🎯 什麼情境該想到我
當你「一個請求要依序交給多個處理者,直到有人處理它(或有人擋下它)」的時候——
審批流程、多層驗證、事件冒泡、middleware 的擋接、客服分級、日誌分級。
⚙️ 怎麼用
意圖
讓多個物件都有機會處理請求,從而避免請求的發送者與接收者之間的耦合。把這些物件連成一條鏈,並沿著鏈傳遞請求,直到有物件處理它為止。
Request ──▶ HandlerA ──▶ HandlerB ──▶ HandlerC ──▶ (沒人處理)
│ │
能處理就 ★ 任一環都可以「停下來不往下傳」
處理並停止
★ 和 Decorator 的關鍵差別(最常被問)
結構幾乎一樣,差在「能不能中斷」:
| 責任鏈 | 裝飾者模式 | |
|---|---|---|
| 每一環的職責 | ★ 決定「我處理,還是往下傳」 | 一定會往下傳(只是前後加料) |
| 可以中斷嗎 | ★ 可以,而且這是重點 | 不應該(會破壞語義) |
| 最終誰處理 | 鏈上的某一個(或都不處理) | ★ 一定是最內層的那個 |
| 語義 | 「找到能處理的人」 | 「層層包裝同一個處理」 |
★ 一句話:Decorator 是「所有人都參與」,責任鏈是「只有一個人(或沒人)真正處理」。
⚠️ 實務上的 middleware 兩者兼具:認證 middleware 擋下未授權請求(責任鏈的行為),
log middleware 前後加料(裝飾者的行為)。不用為了分類糾結——
重點是設計時要明確:這一環有沒有權力中斷?
★ Go 的長相
① 顯式鏈(經典版)
type Approver interface {
Approve(ctx context.Context, req Request) (Decision, error)
SetNext(Approver)
}
type manager struct{ next Approver }
func (m *manager) SetNext(n Approver) { m.next = n }
func (m *manager) Approve(ctx context.Context, req Request) (Decision, error) {
if req.Amount <= 10_000 {
return Decision{By: "manager", OK: true}, nil // ★ 我處理,鏈到此為止
}
if m.next == nil {
return Decision{}, ErrNoApprover // ★ 鏈末仍無人處理
}
return m.next.Approve(ctx, req) // ★ 往下傳
}② 切片版(★ Go 更常用,順序一目了然)
type Rule func(ctx context.Context, req Request) (Decision, bool) // bool = 我處理了嗎
func Chain(rules ...Rule) func(context.Context, Request) (Decision, error) {
return func(ctx context.Context, req Request) (Decision, error) {
for _, r := range rules {
if d, handled := r(ctx, req); handled {
return d, nil // ★ 有人處理就停
}
}
return Decision{}, ErrNoApprover
}
}
approve := Chain(autoApproveSmall, managerRule, directorRule, ceoRule)★ 切片版勝過「每個 handler 持有 next 指標」的版本:
整條鏈的順序在一個地方看得到,而不是散落在各個 handler 的SetNext呼叫裡。
這和 狀態模式 選「轉換表」而非「狀態物件」是同一個理由。
③ middleware 版(可中斷)
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !authorized(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return // ★ 不呼叫 next —— 鏈中斷
}
next.ServeHTTP(w, r)
})
}真實案例
| 案例 | 鏈 |
|---|---|
| HTTP middleware | auth → rate limit → 業務 handler |
| DOM 事件冒泡 | ★ 子元素 → 父元素 → document,stopPropagation() 就是中斷 |
| 日誌分級 | DEBUG handler → INFO handler → ERROR handler |
| 審批流 | 組長 → 經理 → 總監 → CEO(依金額) |
| 例外處理 | ★ 語言的 try/catch 本質上就是一條責任鏈 |
| Servlet Filter / Express middleware | 同 HTTP middleware |
TypeScript 的長相
type Rule = (req: Request) => Decision | undefined; // undefined = 我不處理
const chain = (...rules: Rule[]) => (req: Request): Decision => {
for (const r of rules) {
const d = r(req);
if (d) return d;
}
throw new Error("no approver");
};★
Decision | undefined比(Decision, bool)更自然——TS 用可選型別表達「我不處理」。
搭配strictNullChecks(見 工具-tsconfig基準設定),編譯器會逼你處理 undefined 的情況。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 最大的風險:沒有人處理,而且沒人發現。
請求默默地走完整條鏈然後消失。
→ 鏈末一定要有一個明確的行為:回錯誤、丟到 dead letter、或一個保證處理的預設 handler。
不要讓「沒人處理」變成靜默的 no-op。 - ★ 順序就是語義,而且很容易錯。
認證要在限流之前還是之後?(★ 限流應該在認證之後,否則未認證的請求也消耗配額;
但若要防阻斷攻擊,廉價的 IP 級限流該在最前面——兩層都要。)
→ 把順序的理由寫成註解,並且測試它。 - 除錯困難:「為什麼我的請求沒被處理」要沿著整條鏈追。
→ 每一環都 log「我處理了 / 我往下傳了」,並帶 trace id。 - 效能:每個請求都要走過整條鏈。鏈很長且多數請求落在末端時,考慮用查表直接路由
(就像 工廠方法模式 的註冊表取代長 switch)。 - 不要做成雙向或有分支。責任鏈是線性的;需要分支就用明確的路由或狀態機。
- ★ 只有兩三個固定的檢查 → 直接寫 if 就好。 責任鏈的價值在「環是可插拔、順序可調、數量會變」。
- 鏈的建構要集中。散落在各處的
SetNext會讓實際順序無法被讀出來。