🎯 什麼情境該想到我
當你「發現同一組資料項目老是成群結隊地出現在多個函式的參數清單裡」的時候——把它們組成一個物件一起傳。這不只讓參數變短,更揭示了這群資料本來就屬於同一個概念,還能把處理它們的行為搬進去。
⚙️ 怎麼用(步驟 / 公式)
意圖:把常一起出現的一組參數組成一個物件來傳遞。
動作要點(小步安全進行):
- 若還沒有合適的結構,先建立一個新類別(或記錄)來代表這組資料,給它一個能表達整體概念的名字。
- 用改變函式宣告 Change Function Declaration把這個新物件加進函式的參數清單。
- 逐一調整呼叫端,改成建立並傳入這個新物件。
- 把函式內原本使用個別參數的地方,改成從新物件取值。
- 移除已被取代的舊個別參數,測試。
- 後續可把「操作這組資料的邏輯」搬進這個新類別,讓它從資料袋長成有行為的物件。
★ before / after(Go)
// before:start / end 這兩個參數在整個 codebase 一直黏在一起出現
func ReadingsOutsideRange(station Station, min, max float64) []Reading
func AmountInvoiced(startDate, endDate time.Time) float64
func Recipe(startDate, endDate time.Time) []Item
// after
type DateRange struct{ Start, End time.Time }
func AmountInvoiced(r DateRange) float64
func Recipe(r DateRange) []Item★★ 真正的收益在第二步:新型別會長出方法
參數物件本身只是「少打幾個字」。價值在於現在有地方放邏輯了:
func (r DateRange) Contains(t time.Time) bool {
return !t.Before(r.Start) && !t.After(r.End)
}
func (r DateRange) Days() int { return int(r.End.Sub(r.Start).Hours() / 24) }
func (r DateRange) Valid() error {
if r.End.Before(r.Start) { return errors.New("end before start") }
return nil
}原本散落在五個呼叫點的 if t.After(start) && t.Before(end)(而且有兩處把 >= 寫成 >)
全部收斂到一個方法。→ 這條路的終點是 值物件。
Go 的三個選項與判準
| 做法 | 什麼時候用 |
|---|---|
| 直接多個參數 | ≤3 個、型別各異、不會一起出現在別處 |
| ★ params struct | 這組參數總是一起出現、且大多是必填。func Do(p Params) |
| ★ Functional Options | 大多是選項(有合理預設)、未來還會增加。func Do(x string, opts ...Option) |
★ 判準:這些欄位是「必填的一組資料」還是「可選的調整」?
資料 → struct(而且該長出方法);選項 → options(見 工具-Go函式參數太多怎麼辦)。
⚠️ params struct 的零值陷阱
type Query struct {
Limit int
Offset int
}
Search(Query{Offset: 20}) // ★ Limit 是 0 —— 你是想要 0 筆,還是忘了填?Go 的 struct 分不出「沒給」和「給了零值」。 三種解法:
// ① 建構函式塞預設值
func NewQuery(opts ...QueryOpt) Query { q := Query{Limit: 50}; … }
// ② 零值就是合理預設(★ 最 Go 的做法)
if q.Limit == 0 { q.Limit = 50 } // 讓 0 的意思等於「用預設」
// ③ 真的需要分辨時才用指標
type Query struct{ Limit *int } // ★ 代價:呼叫端要寫 &n,而且到處都要判 nilTypeScript
TS 用物件字面量當「具名參數」是主流寫法,而且 ? 天然分得出「沒給」:
function search({ limit = 50, offset = 0 }: { limit?: number; offset?: number }) { … }
search({ offset: 20 }); // ★ limit 自動是 50,不會有 Go 的零值歧義→ 這個手法在 TS 幾乎是免費的,所以 TS 的痛點不是「參數太多」而是「物件型別散落」(見 工具-satisfies與型別標註)。
工具支援
⚠️ gopls 沒有這個功能。Extract parameter struct 自 2024 年列為計畫,
到 2026-09-25 查證仍未實作(go.dev/issue/65552)→ 在 Go 只能手動做。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若這些參數只是偶爾湊在一起、彼此沒有概念上的關聯,硬包成物件反而增加無意義的間接。
-
★ 別把不相關的參數硬綁成一包。判準是「這幾個參數在別的函式簽章裡也總是一起出現」(那就是 資料泥團)。只在這一個函式裡同時出現,不算。
-
停在「只有欄位的 struct」就白做了。沒有長出方法的參數物件,只是把長參數列換成長欄位列——問題沒解決。
-
★ params struct 的零值歧義(見上)。這是 Go 用 struct 當參數最常出的 bug。
-
會被大量呼叫的熱路徑:大 struct 傳值會複製。用指標或 profile 過再說——但不要為了猜測的效能提前用指標,指標會帶來 nil 與別名(aliasing)的新問題。
-
參數物件會變成上帝物件。
RequestContext塞了三十個欄位,每個呼叫者只用三個——那已經退化成 全域資料。
🔗 相關工具
- 改變函式宣告 Change Function Declaration(引入參數物件透過改宣告來完成)
- 以物件取代基本型別 Replace Primitive with Object(同樣是把零散資料升級成有概念的物件)
- 提煉類別 Extract Class(參數物件長出行為後,往往就成了獨立類別)
- 重構
- 資料泥團 —— 觸發本手法的壞味道
- 過長參數列 —— 另一個觸發訊號
- 值物件 —— 參數物件長出方法與不可變性之後的終點
- 以物件取代基本型別 —— 單一參數版的同一件事
- 工具-Go函式參數太多怎麼辦 —— struct 與 Functional Options 的選擇
- 工具-satisfies與型別標註 —— TS 的對應寫法