🎯 什麼情境該想到我
當你「有個暫時變數只是用來保存某段運算的結果」的時候——把它改成一個查詢函式。這樣任何地方都能取用這個計算,也移除了區域變數這個障礙,讓長函式更容易被提煉函式 Extract Function拆開。
⚙️ 怎麼用(步驟 / 公式)
意圖:把只用來保存某運算結果的暫時變數,改成一個查詢(getter)函式。
動作要點(小步安全進行):
- 確認這個變數在使用前確實已被賦定值,且賦值它的運算式沒有副作用(每次算都得到相同結果)。
- 若變數目前可變,先設為不可變並測試,確認它只被賦值一次。
- 把「賦值給該變數的那段運算式」用提煉函式 Extract Function抽成一個查詢函式(函式不得修改任何物件狀態)。
- 用行內變數 Inline Variable把該暫時變數換成對這個查詢函式的呼叫。
- 編譯、測試。
★ before / after(Go)
// before:basePrice 只是一個「算出來的中間值」,卻佔著一個區域變數
func (o *Order) Price() float64 {
basePrice := float64(o.Quantity) * o.ItemPrice
var discountFactor float64
if basePrice > 1000 { discountFactor = 0.95 } else { discountFactor = 0.98 }
return basePrice * discountFactor
}
// after:兩個中間值都變成方法,Price() 只剩一行意圖
func (o *Order) basePrice() float64 { return float64(o.Quantity) * o.ItemPrice }
func (o *Order) discountFactor() float64 {
if o.basePrice() > 1000 { return 0.95 }
return 0.98
}
func (o *Order) Price() float64 { return o.basePrice() * o.discountFactor() }★ 為什麼值得做:這是「提煉類別」的鋪路工
暫時變數把邏輯綁死在一個函式裡——你沒辦法把 basePrice 的計算給別人用。
一旦變成方法,它就能被 提煉函式、搬移函式、提煉類別 搬走。
★ Fowler 明說這是做 提煉函式 之前的常見前置步驟:
消掉暫時變數後,要提煉的那段碼就不再依賴一堆區域狀態,參數列也不會爆掉。
★★ 三個前提,缺一個就不能做
① 這個變數必須只被賦值一次
total := 0
for _, i := range items { total += i.Price } // ★ 累加型變數不能換成查詢② 計算必須是純的(冪等、無副作用)
sessionID := uuid.New().String() // ★ 換成方法 → 每次呼叫拿到不同 ID
now := time.Now() // ★ 同上
body, _ := io.ReadAll(r.Body) // ★★ 最糟:Body 是一次性的,讀第二次拿到空的③ 計算不能太貴
user := db.LoadUser(id) // ★ 換成 u.user() → 一個函式裡呼叫三次 = 三次查詢真的想做就要配快取,而快取要處理失效與併發——通常不如留著變數。
Go 的細節:方法呼叫幾乎免費
Go 編譯器會內聯小方法。實測 Go 1.26.4(2026-09-25):
can inline small with cost 4,內聯預算是 80(go build -gcflags='-m -m' 可看)。
→ basePrice() 這種一行方法,換成查詢後效能差異通常是零。
⚠️ 但介面方法無法內聯(動態分派),把 x := iface.Compute() 換成三處 iface.Compute()
就是真的三次動態呼叫。
TypeScript
TS 有 getter,語法上更順:
private get basePrice() { return this.quantity * this.itemPrice; }
get price() { return this.basePrice * this.discountFactor; }★ 但 TS 的陷阱是 getter 看起來像屬性,讀者不會預期它很貴。
user.orders 背後跑了一個 N+1 查詢 → 沒人會懷疑那一行。貴的東西就叫 loadOrders()。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
只適用於「計算型」暫時變數;若變數是用來累加、迭代中改變狀態,或其運算式有副作用,就不適用。
-
若該計算很昂貴又被頻繁使用、變數是刻意的快取,改成每次查詢要衡量效能。
-
★ 三個前提:只賦值一次、純計算、不貴。缺一個就別做(見上)。
-
io.ReadAll(r.Body)這類「一次性來源」絕對不能換成查詢——第二次讀拿到空的,而且不會報錯。 -
不要把整個函式都變成一連串單行方法。十個
xxxPrice()方法互相呼叫,讀者要在型別裡跳十次——只抽有名字價值的中間概念。 -
反向也是合法的重構:方法被呼叫太多次且很貴時,改回區域變數(提煉變數)才是對的。
🔗 相關工具
- 提煉函式 Extract Function(此手法常是為了讓提煉函式更順而做)
- 行內變數 Inline Variable(替換暫時變數的最後一步)
- 提煉變數 Extract Variable(相反方向:想留下解釋性變數時)
- 重構
- 提煉變數 —— 反向操作
- 提煉函式 —— 本手法最常見的下一步
- 提煉類別 —— 一堆查詢方法圍著同一組欄位時
- 拆分迴圈 —— 累加型變數的正確處理
- 將查詢與修改分離 —— 「查詢」的前提是無副作用