🎯 什麼情境該想到我

當你「呼叫一個函式時要傳一長串參數,得反覆對照才知道每個位置是什麼」的時候——參數列變成理解與使用的負擔。

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

這味道是什麼徵兆:過長的參數列難記、易傳錯順序,還常暗示這些資料本該被組織成一個概念,或函式承擔了太多不該由呼叫端提供的東西。

通常用哪些重構手法對治:

  • 引入參數物件(Introduce Parameter Object):把總是一起出現的幾個參數包成一個物件。
  • 保持物件完整(Preserve Whole Object):與其從物件拆出幾個值再傳進去,不如直接傳整個物件。
  • 以查詢取代參數(Replace Parameter with Query):若某參數能由函式內部或別的參數推導出來,就別要求呼叫端傳。
  • 用**函式組合成類別(Combine Functions into Class)**把共同參數變成欄位。

★ 怎麼具體認出來

訊號說明
超過 3~4 個參數純數量門檻,最粗但有用
★ 連續的同型別參數f(a, b, c string) —— 呼叫端傳錯順序,編譯完全通過
多個 boolf(x, true, false, true) —— 見 移除旗標參數
★ 參數成群一起出現在多個簽章start, end time.Time 到處黏在一起 → 資料泥團
有些參數只是為了往下傳呼叫端自己不用,只是透傳
# revive 的 argument-limit 規則可以設上限(golangci-lint 的 revive linter)

★★ 最危險的不是「長」,是「同型別相鄰」

func Copy(dst, src string) error     // ★★ 傳反了 → 編譯通過、測試可能也過、資料被覆蓋
func Between(lo, hi int) bool
func Transfer(from, to AccountID, amount int) error

Go 沒有具名引數(named arguments),所以呼叫端看不到參數名:

Copy(backupPath, livePath)      // ★ 哪個是來源?要跳去看定義

三個解法:

// ① ★ 具名型別 —— 零成本,編譯期擋住
type SrcPath string
type DstPath string
func Copy(dst DstPath, src SrcPath) error      // ★ 傳反了編譯不過
 
// ② params struct —— 呼叫端變成具名的
func Copy(p CopyParams) error
Copy(CopyParams{Src: livePath, Dst: backupPath})
 
// ③ 讓函式名說明順序
func CopyTo(dst string, src string) error      // ★ 比較弱,但零成本

★ Go 的四種對治,以及各自適用的情況

做法什麼時候代價
引入參數物件(params struct)參數總是一起出現、大多必填★ 零值分不出「沒給」和「給 0」
★ Functional Options大多是有預設值的選項、未來還會增加每個選項一個函式,樣板較多
搬移函式參數幾乎都來自同一個型別 → 把函式搬到它上面可能撞循環 import
以物件取代基本型別單一參數帶著驗證與行為序列化邊界要自己補
// ★ Functional Options 的判準:這些參數是「選項」還是「資料」?
func NewServer(addr string, opts ...Option) (*Server, error)
NewServer(":8080", WithTLS(cert), WithTimeout(5*time.Second))
// → 加一個選項不是破壞性變更;呼叫端只寫自己在意的

→ 完整判準見 工具-Go函式參數太多怎麼辦。

★★ 什麼時候這其實不是味道

情況說明
★ context.Context 是第一個參數它不算在「參數太多」裡。Go 的慣例就是每個會阻塞的函式都帶 ctx
★ 建構函式收齊必填欄位NewRange(lo, hi)、NewRect(x, y, w, h)——逼呼叫端一次給齊,勝過事後才發現漏填
標準庫既有慣例os.OpenFile(name, flag, perm)、http.NewRequest(method, url, body)
參數型別各不相同且意義明確f(ctx, userID string, amount int, at time.Time) —— 傳錯順序編譯不過

★ 判準不只是數量,更是「呼叫端會不會搞錯」。
四個型別互異的參數比兩個相鄰的 string 安全得多。

⚠️ params struct 的兩個實務陷阱

// ① ★ 零值歧義
Search(Query{Offset: 20})         // Limit 是 0 —— 想要 0 筆,還是忘了填?
// → 讓零值等於「用預設」,或提供建構函式塞預設值(不要輕易改用指標欄位)
 
// ② ★ params struct 會變成垃圾桶
type RequestContext struct {       // ★ 三十個欄位,每個呼叫者用三個
    UserID, TraceID, TenantID, Locale, … string
}
// → 這已經退化成 [[全域資料]]。params struct 要有名字、有邊界

TypeScript

TS 的物件字面量天然具名,這個味道在 TS 幾乎不存在:

copy({ src: livePath, dst: backupPath });      // ★ 順序無關,漏填編譯錯誤
function copy({ src, dst, overwrite = false }: CopyOptions) { … }

★ 所以 TS 的痛點不是參數數量,是「這個物件型別有沒有被命名」——
inline 的 { a: string; b: number } 散落在五個簽章裡,才是 TS 版的這個味道。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 「保持物件完整」與「以查詢取代參數」會讓函式依賴整個物件或內部狀態;若你刻意想降低函式與該物件的耦合(例如要保持純函式、方便測試),反而該保留明確的參數。

  • 參數本身彼此獨立、數量合理時,攤平列出比硬包成物件更清楚。

  • ★ context.Context 不算在參數數量裡,它是 Go 的慣例。

  • ★ 建構函式收齊必填參數是好設計,不要為了「參數少」改成可選——那會把「漏填」從編譯期推到執行期。

  • ★★ 相鄰的同型別參數比參數數量危險。Copy(dst, src string) 傳反了完全不報錯。用具名型別擋。

  • params struct 有零值歧義:分不出「沒給」和「給了零值」。

  • params struct 容易變成垃圾桶。三十個欄位的 RequestContext 已經是 全域資料。

  • 多個 bool 要走 Functional Options,不要拆成 2ⁿ 個函式(見 移除旗標參數)。

  • 改動匯出函式的簽章是破壞性變更,走遷移式(見 改變函式宣告)。

🔗 相關工具