🎯 什麼情境該想到我
當你看到「用一個布林(或列舉)旗標參數去決定函式要走哪種行為」的時候——呼叫端寫著 setDinner(true),光看呼叫根本猜不出 true 是什麼意思。
⚙️ 怎麼用(步驟 / 公式)
意圖:旗標參數會隱藏函式之間的行為差異,也讓呼叫端難以讀懂。把「靠旗標切換的一個函式」拆成幾個各自意圖明確的具名函式,讓呼叫端直接呼叫它要的那一個。
小步安全進行:
- 針對旗標的每一種取值,建立一個明確命名的專用函式(如
deliveryDate拆成regularDeliveryDate與rushDeliveryDate)。 - 讓每個新函式內部帶著固定的旗標值去呼叫原函式(或直接內嵌對應那條分支的邏輯)。
- 找出呼叫端,依它傳入的旗標值,逐一改呼叫對應的專用函式。
- 每改一處就執行測試;全部改完後,若原函式已無人使用可考慮移除。
★ before / after(Go)
// before:呼叫端完全看不出 true 是什麼意思
func (b *Booking) Price(isPremium bool) float64 { … }
b.Price(true) // ★★ 讀者必須跳去看函式定義才知道 true 代表什麼
b.Price(false)
// after:意圖寫在函式名上
func (b *Booking) PremiumPrice() float64 { … }
func (b *Booking) RegularPrice() float64 { … }★ 判準:呼叫端寫的是「字面常數」還是「變數」?
f(true)/f(false)—— 字面常數 → 拆成兩個函式。
f(isPremium)—— 值是算出來的 → 拆成兩個函式後呼叫端會變成if isPremium { … } else { … },
這時要問:那個if放在呼叫端比較好,還是留在函式裡比較好? 只有一個呼叫點時,留著旗標可能更簡單。
★★ Go 裡最常見的三種旗標,以及各自的解法
① bool 參數 → 拆函式,或用具名型別
// ❌
func Copy(src, dst string, overwrite bool) error
Copy(a, b, true) // ★ true 是什麼?
// ✅ 方案 A:拆
func Copy(src, dst string) error
func CopyOverwrite(src, dst string) error
// ✅ 方案 B:具名常數(參數無法避免時)—— 零成本,可讀性大增
type OverwriteMode bool
const (
KeepExisting OverwriteMode = false
Overwrite OverwriteMode = true
)
Copy(a, b, Overwrite) // ★ 現在讀得懂了,而且傳錯型別編譯不過② 多個 bool → Functional Options
// ❌ 呼叫端是一串不知所云的布林
NewServer(addr, true, false, true)
// ✅
NewServer(addr, WithTLS(cert), WithoutCompression(), WithKeepAlive())★ 見 工具-Go函式參數太多怎麼辦。兩個以上的 bool 參數幾乎一定要換成 options。
③ 字串/int 當模式旗標 → 具名型別 + 分派表
// ❌
func Render(data Data, format string) []byte // "json"? "JSON"? "application/json"?
// ✅
type Format int
const (FormatJSON Format = iota; FormatYAML; FormatCSV)
// 再配 map[Format]renderer(見 [[以多型取代條件式]])⚠️ 什麼時候旗標參數是對的
| 情況 | 例子 |
|---|---|
| 標準庫的既有慣例 | os.OpenFile(name, flag, perm) —— flag 是 bit mask,拆成 2ⁿ 個函式不現實 |
| ★ 旗標值真的是執行期算出來的,且只有一個呼叫點 | 拆完只是把 if 從函式內搬到呼叫端 |
| 會傳給下游的透傳參數 | dryRun bool 一路往下傳;每層都拆成兩個函式 = 2ⁿ 個函式 |
// ★ dry-run 是典型的「拆了會更糟」的旗標
func Apply(ctx context.Context, plan Plan, dryRun bool) error
// 拆成 Apply / ApplyDryRun → 兩者 90% 的碼相同([[重複程式碼]]),而且每個下游函式都要跟著拆一次→ 這時改善方向是讓旗標可讀(具名型別)與讓它只出現在最外層(把決策提前,內層改成注入不同的 executor)。
⚠️ Go 的細節:拆成兩個函式時別複製實作
// ✅ 共同實作留在一個未匯出函式裡
func (b *Booking) PremiumPrice() float64 { return b.price(true) }
func (b *Booking) RegularPrice() float64 { return b.price(false) }
func (b *Booking) price(premium bool) float64 { … } // ★ 旗標退到私有層,對外 API 是清楚的★ 這是最實用的折衷:對外兩個有名字的方法,對內一份實作。
旗標本身不是罪,「讓呼叫端讀不懂」才是。
TypeScript
// ❌
setAlignment(true)
// ✅ 物件參數天然具名,這在 TS 幾乎免費
setAlignment({ center: true })
// ✅ 或字面量聯集 —— 比 boolean 更好,因為可以有第三種
function setAlignment(a: 'left' | 'center' | 'right') { … }★ 'left' | 'center' 勝過 boolean:需求增加第三種對齊方式時,boolean 必須整個改掉。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若旗標會有多個、組合起來爆炸成一堆函式,拆分反而失控——此時保留參數(或改用參數物件)較合理。
-
當旗標值是在執行期才動態決定、無法在呼叫端寫死時,拆成具名函式的好處會打折。
-
★ 拆成 2ⁿ 個函式的組合爆炸。兩個 bool → 四個函式;三個 → 八個。多個旗標要走 Functional Options,不是拆函式。
-
★ 透傳型旗標(
dryRun、verbose、force)通常不該拆——會讓整條呼叫鏈都翻倍。改善方向是具名型別與「決策提前」。 -
不要因此複製實作。對外兩個名字、對內一份實作(私有函式收旗標)。
-
bit mask 是另一回事(
os.O_CREATE|os.O_EXCL)。那不是旗標參數的壞味道。 -
★ 修正呼叫端而不加新 API 也是一種解:如果只有兩個呼叫點,把
f(true)改成f(OverwriteExisting)就夠了,不必增加函式。 -
改動匯出 API 要走遷移式(見 改變函式宣告)。
🔗 相關工具
- 將查詢與修改分離 Separate Query from Modifier(同屬讓函式職責單一、意圖明確的手法)
- 移除死程式碼 Remove Dead Code(拆分後常暴露出走不到的分支可清除)
- 重構
- 改變函式宣告 —— 旗標參數的移除就是它的一個特例
- 工具-Go函式參數太多怎麼辦 —— 多個旗標時走 Functional Options
- 以多型取代條件式 —— 模式旗標的升級版
- 將查詢與修改分離 —— 另一種「一個函式做兩件事」
- 過長參數列 —— 觸發本手法的壞味道