🎯 什麼情境該想到我
當你看到「好幾個各自獨立的條件判斷,但它們的結果/動作其實完全一樣」的時候——一連串 if 各自 return 相同的東西,讀起來像是好多個檢查,其實是同一件事。
⚙️ 怎麼用(步驟 / 公式)
意圖:把一串「導向相同結果」的條件判斷,用邏輯運算子(&&、||)合併成單一條件,再提煉成一個有名字的函式。這樣做能明白宣告「這其實是一個判斷」,而不是好幾個彼此無關的檢查。
小步安全進行:
- 確認這些條件式都沒有副作用(純判斷);有副作用要先處理。
- 用
||(其中之一成立)或&&(全部成立)把相鄰、結果相同的條件合併成一個判斷式。 - 執行測試。
- 對合併後的條件施以提煉函式,取一個說明「為什麼」的名字(如
isNotEligibleForDisability())。
★ before / after(Go)
// before:三個條件、同一個結果 —— 讀者要讀三次才知道它們其實是一件事
func disabilityAmount(e Employee) float64 {
if e.Seniority < 2 { return 0 }
if e.MonthsDisabled > 12 { return 0 }
if e.IsPartTime { return 0 }
…
}
// after:一個條件 + 一個名字
func disabilityAmount(e Employee) float64 {
if isNotEligibleForDisability(e) { return 0 }
…
}
func isNotEligibleForDisability(e Employee) bool {
return e.Seniority < 2 || e.MonthsDisabled > 12 || e.IsPartTime
}★ 合併本身只是第一步,價值在「現在這串條件有一個名字了」——
也就是接著做 分解條件式/提煉函式。合併完停在if a || b || c沒有太大收益。
★ 前提檢查:這三個條件真的等價嗎
唯一的判準:它們的「後果」完全一樣。
if e.Seniority < 2 { return 0 }
if e.MonthsDisabled > 12 { return 0 }
if e.IsPartTime { log.Warn("part-time"); return 0 } // ★ 多了一行!不能合併差一行日誌、差一個錯誤訊息、差一個 metric —— 就不是同一件事。
硬合併會讓你丟掉那個差異,而且通常沒有測試會發現。
★★ Go 的關鍵陷阱:合併會改變求值次數與順序
// before:後面的條件只在前面為假時才求值
if u == nil { return errNoUser }
if !u.Active { return errInactive }
if u.Quota() <= 0 { return errNoQuota } // ★ Quota() 是一次 DB 查詢
// after ★★ 壞了兩次
if u == nil || !u.Active || u.Quota() <= 0 { return errBad }
// ↑ 這裡還是短路的(|| 也短路),所以 nil 安全
// ↑ 但三個不同的錯誤變成一個 —— ★ 呼叫端分不清原因了兩件事要分開看:
&&和||在 Go 都是短路的 → nil 保護不會壞。- ★ 但「不同的後果」被合併掉了。回傳不同 error 的條件不能合併,這是實務上最常見的誤用。
★ 兩種合併:|| 與 &&
// || —— 「任何一個成立就走這條」(同一結果的多個前提)
if a || b || c { return 0 }
// && —— 「巢狀的 if 每層都沒有 else」
if a { if b { if c { doIt() } } } // before
if a && b && c { doIt() } // after ★ 純 && 的嵌套一律該扁平化★ && 這一種是最沒有爭議的重構——巢狀三層卻沒有任何 else,扁平化後絕對更好讀。
⚠️ 什麼時候反而該拆開(反向操作)
// ★ 條件太長、或每個條件需要各自的錯誤訊息時,衛述句更好
if u == nil { return fmt.Errorf("user required") }
if !u.Active { return fmt.Errorf("user %s inactive", u.ID) }
if u.Quota() <= 0 { return fmt.Errorf("user %s out of quota", u.ID) }Go 的慣例偏向這一種(見 以衛述句取代嵌套條件式):
每個前提一行、各自的錯誤訊息、加新前提不用碰既有的行。
→ ★ 判準:呼叫端需要區分失敗原因嗎?需要 → 不要合併。
TypeScript
// ★ 別把 ?? 和 || 搞混 —— 合併條件時很容易誤用
const n = count || 10; // ★ count 為 0 時給 10(0 是 falsy)
const n = count ?? 10; // ✅ 只有 null/undefined 才給 10🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若這些條件本來就代表「彼此獨立、只是碰巧結果相同」的不同判斷,合併會抹掉它們的差異,反而誤導——此時不該合併。
-
條件式帶有副作用時,合併可能改變執行行為,需先移除副作用。
-
★★ 後果不同就不能合併。回傳不同 error、記不同 log、發不同 metric 的條件是三件事,不是一件事。這是最常見的誤用。
-
★ Go 的慣例常常偏向不合併:一連串衛述句、各自的錯誤訊息,比一個長
||更符合 Go 的錯誤處理風格。 -
合併後的條件超過三項就該提煉成具名函式,否則
if a || b || c || d || e只是換一種難讀。 -
混用
&&和||要加括號。Go 的&&優先於||,但依賴優先序的程式碼 review 時沒人敢改。 -
純
&&的巢狀(沒有 else)扁平化幾乎總是對的——這是本手法最安全的用法。 -
TS:
||對0/''/false也會走 fallback,該用??的地方用||是靜默的 bug。
🔗 相關工具
- 分解條件式 Decompose Conditional(反過來,處理單一但過於複雜的條件)
- 以衛述句取代嵌套條件式 Replace Nested Conditional with Guard Clauses(合併後常可寫成一句提早 return 的衛述句)
- 提煉函式 Extract Function(把合併後的條件包成具名判斷)
- 重構
- 分解條件式 —— 合併之後給它一個名字
- 以衛述句取代嵌套條件式 —— ★ Go 常常偏好的反方向
- 提煉函式 —— 合併後的下一步
- 重複程式碼 —— 多個分支同一結果就是一種重複