🎯 什麼情境該想到我

當你「有個暫時變數只是用來保存某段運算的結果」的時候——把它改成一個查詢函式。這樣任何地方都能取用這個計算,也移除了區域變數這個障礙,讓長函式更容易被提煉函式 Extract Function拆開。

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

意圖:把只用來保存某運算結果的暫時變數,改成一個查詢(getter)函式。

動作要點(小步安全進行):

  1. 確認這個變數在使用前確實已被賦定值,且賦值它的運算式沒有副作用(每次算都得到相同結果)。
  2. 若變數目前可變,先設為不可變並測試,確認它只被賦值一次。
  3. 把「賦值給該變數的那段運算式」用提煉函式 Extract Function抽成一個查詢函式(函式不得修改任何物件狀態)。
  4. 用行內變數 Inline Variable把該暫時變數換成對這個查詢函式的呼叫。
  5. 編譯、測試。

★ 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() 方法互相呼叫,讀者要在型別裡跳十次——只抽有名字價值的中間概念。

  • 反向也是合法的重構:方法被呼叫太多次且很貴時,改回區域變數(提煉變數)才是對的。

🔗 相關工具