🎯 什麼情境該想到我

當你看到「用一個布林(或列舉)旗標參數去決定函式要走哪種行為」的時候——呼叫端寫著 setDinner(true),光看呼叫根本猜不出 true 是什麼意思。

⚙️ 怎麼用(步驟 / 公式)

意圖:旗標參數會隱藏函式之間的行為差異,也讓呼叫端難以讀懂。把「靠旗標切換的一個函式」拆成幾個各自意圖明確的具名函式,讓呼叫端直接呼叫它要的那一個。

小步安全進行:

  1. 針對旗標的每一種取值,建立一個明確命名的專用函式(如 deliveryDate 拆成 regularDeliveryDate 與 rushDeliveryDate)。
  2. 讓每個新函式內部帶著固定的旗標值去呼叫原函式(或直接內嵌對應那條分支的邏輯)。
  3. 找出呼叫端,依它傳入的旗標值,逐一改呼叫對應的專用函式。
  4. 每改一處就執行測試;全部改完後,若原函式已無人使用可考慮移除。

★ 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 要走遷移式(見 改變函式宣告)。

🔗 相關工具