🎯 什麼情境該想到我
當你「呼叫一個函式時要傳一長串參數,得反覆對照才知道每個位置是什麼」的時候——參數列變成理解與使用的負擔。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:過長的參數列難記、易傳錯順序,還常暗示這些資料本該被組織成一個概念,或函式承擔了太多不該由呼叫端提供的東西。
通常用哪些重構手法對治:
- 引入參數物件(Introduce Parameter Object):把總是一起出現的幾個參數包成一個物件。
- 保持物件完整(Preserve Whole Object):與其從物件拆出幾個值再傳進去,不如直接傳整個物件。
- 以查詢取代參數(Replace Parameter with Query):若某參數能由函式內部或別的參數推導出來,就別要求呼叫端傳。
- 用**函式組合成類別(Combine Functions into Class)**把共同參數變成欄位。
★ 怎麼具體認出來
| 訊號 | 說明 |
|---|---|
| 超過 3~4 個參數 | 純數量門檻,最粗但有用 |
| ★ 連續的同型別參數 | f(a, b, c string) —— 呼叫端傳錯順序,編譯完全通過 |
多個 bool | f(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) errorGo 沒有具名引數(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ⁿ 個函式(見 移除旗標參數)。
-
改動匯出函式的簽章是破壞性變更,走遷移式(見 改變函式宣告)。
🔗 相關工具
- 資料泥團 Data Clumps(總是成群的參數,正是引入參數物件的訊號)
- 基本型別偏執 Primitive Obsession(一串基本型別參數常可換成領域物件)
- 過長函式 Long Function(縮短函式時別讓參數列跟著膨脹)
- 重構
- 引入參數物件 —— 主要對治手法
- 工具-Go函式參數太多怎麼辦 —— params struct 與 Functional Options 的判準
- 移除旗標參數 —— bool 參數的專門對治
- 搬移函式 —— 參數都來自同一型別時
- 以物件取代基本型別 —— 單一參數帶行為時
- 資料泥團 —— 參數成群出現的根因