臨時變數是軟體程式碼中造成不必要複雜性的最常見來源之一。它們會在冗長的方法中不斷累積,導致計算值的名稱模糊不清,並且使得提取、測試或重複使用其中的邏輯變得更加困難。 Martin Fowler 在《重構:改進現有程式碼的設計》一書中提出的「用查詢取代暫存變數」重構方法直接解決了這個問題:它不是將計算值儲存在局部變數中,而是將計算過程提取到一個命名方法(即查詢)中,並在需要該值的地方呼叫它。
最終得到的程式碼能夠清晰地傳達意圖,而不是將其隱藏起來。計算不再隱藏在冗長方法頂部的變數賦值語句中;它擁有名稱、位置,並且可以單獨測試。本文將全面介紹這項技術,包括臨時變數的概念、它們何時會引發問題、如何在 Java、Python 和 TypeScript 中逐步進行重構、何時應該使用以及何時不應該使用,以及它與重構目錄中其他相關技術的聯繫。
程式設計中的臨時變數(Temp)是什麼?
暫存變數(通常簡稱為temp)是函數或方法內部的局部變量,用於儲存中間結果,供該作用域內使用。它只計算一次,儲存在一個命名變數中,然後在同一函數中被引用。此變數僅在函數呼叫期間存在;在函數外部無法訪問,也不會儲存在物件的狀態中。
蟒蛇
# Python: base_price is a temp variable
def calculate_total(quantity, item_price):
base_price = quantity * item_price # temp: computed once, used below
if base_price > 1000:
return base_price * 0.95
return base_price * 0.98
Java的
// Java: basePrice is a temp variable
double basePrice = quantity * itemPrice; // temp
if (basePrice > 1000) {
return basePrice * 0.95;
}
return basePrice * 0.98;
打字稿
// TypeScript: basePrice is a temp variable
const basePrice = quantity * itemPrice; // temp
if (basePrice > 1000) return basePrice * 0.95;
return basePrice * 0.98;
臨時變數本身並沒有錯。它們有其合理用途:例如,捕獲重複執行會造成浪費的耗時操作的結果;將複雜的多步驟計算分解成易於理解的階段;或保存在循環迭代中累積的值。問題在於,當臨時變數被濫用於簡單的派生值(這些值如果用命名方法會更清晰),或者當它們在冗長的方法中累積,迫使讀者追蹤多個同時存在的中間值時。
軟體工程中的重構是什麼?
重構是指在不改變程式碼可觀察行為的前提下,對現有程式碼進行結構重組的過程。其目標是提升程式碼的內部質量,包括可讀性、可測試性、可維護性和模組化程度。重構不會增加新功能,也不會修復錯誤;它只是改變程式碼的結構,同時保持程式碼的功能不變。
將 Temp 替換為 Query 是 Martin Fowler 所描述的數十種重構技巧之一。它屬於一類專門處理過長或過複雜方法的技巧:
| 重構技術 | 它能做什麼 |
|---|---|
| 用查詢取代臨時變數 | 將臨時變數的計算結果提取到一個命名方法中 |
| 提取方法 | 將一段程式碼提取到一個新的命名方法中 |
| 線上溫度 | 直接用表達式取代簡單的臨時變量 |
| 拆分臨時變數 | 將用於不同用途的臨時變數拆分為不同的變數。 |
| 將循環替換為管道 | 用函數式管道(map、filter、reduce)取代命令式循環 |
| 引入解釋變數 | 引入命名臨時變數以闡明複雜的表達式 |
這些技術並非孤立使用。 Fowler 將「將臨時變數替換為查詢變數」描述為提取方法之前的關鍵步驟:如果一個方法包含臨時變量,那麼將其一部分提取到新方法中就會變得困難,因為這些臨時變數可能在提取部分之前和之後都會被使用。首先將臨時變數轉換為查詢變量,從而消除它們,為提取過程掃清障礙。
什麼是「用查詢取代臨時檔案」?
將臨時變數替換為查詢是一種重構技巧,它將局部臨時變數轉換為方法呼叫。它不再計算值並將其賦給局部變量,而是將計算過程提取到一個私有方法(即查詢方法)中,該方法在被呼叫時會傳回計算結果。凡是使用臨時變數的地方,都應將其替換為對查詢方法的呼叫。
福勒《重構》一書中的經典例子:
之前:
Java的
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
後:
Java的
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
private double basePrice() {
return _quantity * _itemPrice;
}
查詢方法 basePrice() 現在它是一個已命名的、獨立的計算函數。它可以從類別中的任何其他方法調用,可以獨立測試,可以在子類別中重寫,並且無需事先閱讀調用方法即可理解。
臨時變數的問題
它們將邏輯碎片化到方法中
臨時變數將計算過程拆分成兩個獨立的部分:賦值(計算值)和使用(讀取值)。在較短的方法中,這種拆分並無大礙。但在一個長達三五十行的方法中,賦值和使用之間可能隔著許多行其他邏輯。讀者必須向上捲動才能找到賦值語句,將其意義記在工作記憶中,然後再向下捲動才能找到使用語句。每個額外的臨時變數都會增加這種認知負擔。
他們阻斷萃取法
臨時變數最顯著的實際問題是它們會阻礙其他重構。考慮一個包含複雜條件分支的方法,如果將其提取到單獨的方法中會更有效率。如果該分支使用了先前在方法中賦值的臨時變量,那麼提取過程就需要將臨時變量作為參數傳遞、將其設為實例變量,或者在提取後的方法中重新計算其值。這些方法都不夠簡潔。如果首先透過用查詢語句取代臨時變數來消除此障礙,則可以完全避免這個問題。
它們鼓勵重複使用和變異
臨時變數有時會在同一個方法中重複用於不同的目的,Fowler 將這種做法稱為「臨時變數糾纏」。一個名為 temp or result 多次重新賦值的臨時變數不提供任何語義訊息,反而會誤導讀者,讓他們對其在任何給定時刻所代表的含義產生誤解。即使是用途單一的臨時變量,也會不斷累積,最終導致方法的作用域充斥著讀者必須同時追蹤的中間值。
逐步指南:如何應用“將臨時值替換為查詢”
此轉換過程分為四個步驟,可以安全地應用於任何語言:
步驟 1:確認臨時變數只被賦值一次,且從未被修改。如果方法後續步驟中臨時變數被重新賦值,請先使用「拆分臨時變數」函數將其拆分。
步驟 2:將賦值語句的右邊擷取到一個私有方法。 給方法一個描述其計算內容而非計算方式的名稱。 basePrice() 比...更好 calculateQuantityTimesPrice().
步驟 3:將所有對臨時變數的引用替換為對新方法的呼叫。大多數 IDE 可以自動完成此操作:右鍵單擊臨時變數 → 重構 → 內聯變量,然後選擇內聯表達式上的「提取方法」。
步驟 4:刪除臨時變數宣告。如果提取完成,該臨時變數現在應該沒有任何引用,可以將其刪除。
Java:完整工作範例
Java的
// Before: Order class with temporary variables
public class Order {
private int quantity;
private double itemPrice;
public double getPrice() {
double basePrice = quantity * itemPrice; // temp 1
double discountFactor; // temp 2
if (basePrice > 1000)
discountFactor = 0.95;
else
discountFactor = 0.98;
return basePrice * discountFactor;
}
}
Java的
// After: temps extracted to query methods
public class Order {
private int quantity;
private double itemPrice;
public double getPrice() {
return basePrice() * discountFactor();
}
private double basePrice() {
return quantity * itemPrice;
}
private double discountFactor() {
return basePrice() > 1000 ? 0.95 : 0.98;
}
}
方法 getPrice() 現在,它以單一表達式的形式清晰地表達了計算過程。每個提取出來的查詢都可以獨立閱讀、測試和擴展。請注意: discountFactor() 電話 basePrice()這是正確的,因為 basePrice() 這是一個純粹的計算,沒有任何副作用,所以呼叫兩次不會帶來任何風險。
Python:將 Temp 替換為 Property
在 Python 中,查詢方法的自然等價物是 @property這樣就可以不用括號呼叫該方法,其讀法與屬性存取完全相同:
蟒蛇
# Before: temporary variables in a method
class Order:
def __init__(self, quantity, item_price):
self.quantity = quantity
self.item_price = item_price
def get_price(self):
base_price = self.quantity * self.item_price # temp
discount = 0.95 if base_price > 1000 else 0.98 # temp
return base_price * discount
蟒蛇
# After: temps replaced with properties (query methods in Python)
class Order:
def __init__(self, quantity, item_price):
self.quantity = quantity
self.item_price = item_price
def get_price(self):
return self.base_price * self.discount_factor
@property
def base_price(self):
return self.quantity * self.item_price
@property
def discount_factor(self):
return 0.95 if self.base_price > 1000 else 0.98
使用 @property 手段 self.base_price 讀取方式與實例變數完全相同,這使得呼叫程式碼 self.base_price * self.discount_factor 完全自然。每項屬性均可獨立測試:
蟒蛇
def test_base_price():
order = Order(10, 150)
assert order.base_price == 1500
def test_discount_factor_high_value():
order = Order(10, 150) # base_price = 1500 > 1000
assert order.discount_factor == 0.95
def test_get_price():
order = Order(10, 150)
assert order.get_price() == 1500 * 0.95
這種程度的可測試性是使用基於臨時檔案的版本無法實現的:內部計算 base_price 以及 discount_factor 無法從方法外部存取。
TypeScript:查詢方法和 Getter
TypeScript 同時支援基於方法的查詢和屬性取得器,分別與 Java 和 Python 中可用的模式相符:
打字稿
// Before: temporary variables
class Order {
constructor(private quantity: number, private itemPrice: number) {}
getPrice(): number {
const basePrice = this.quantity * this.itemPrice; // temp
const discount = basePrice > 1000 ? 0.95 : 0.98; // temp
return basePrice * discount;
}
}
打字稿
// After: TypeScript getters replace temps
class Order {
constructor(private quantity: number, private itemPrice: number) {}
getPrice(): number {
return this.basePrice * this.discountFactor;
}
private get basePrice(): number {
return this.quantity * this.itemPrice;
}
private get discountFactor(): number {
return this.basePrice > 1000 ? 0.95 : 0.98;
}
}
命名查詢方法良好
查詢方法的名稱起著至關重要的作用。命名糟糕的提取方法比它替換的臨時方法更糟糕,因為它會造成一種不透明的間接性:呼叫者必須跳到方法定義才能理解其作用,這完全違背了命名的初衷。
好的查詢方法名稱應遵循以下原則:
指出它代表什麼,而不是它是如何計算的。 basePrice() 傳達商業理念。 getQuantityTimesItemPrice() 描述的是計算過程,而不是概念本身。當計算過程改變時,概念名稱也會隨之改變,而這種差異就顯得格外重要。 basePrice() 即使配方改變,其性質依然穩定。
使用名詞片語來表示價值觀。 查詢方法傳回值;它們不是命令。 discountFactor(), totalAmount(), isEligible() 遵循命名傳回值的方式。 calculateDiscount(), processAmount() 遵循命令的命令式約定,這對於純粹計算並傳回結果的方法來說會造成困惑。
布林查詢應該像問句一樣閱讀。 isHighValue(), hasDiscount(), meetsThreshold() 表明返回值是布林值,並且呼叫者正在詢問一個是/否問題。 bool 変数名 (布林變數命名,Search Console 資料中的一個查詢)恰恰反映了這種擔憂:布林變數和方法需要名稱,以便在使用時能夠清楚地表達其含義。
保持相關方法名稱的一致性。 If basePrice() 由...使用 discountFactor()命名的一致性告訴讀者: discountFactor 取決於 basePrice命名不一致會破壞這種隱式文檔。
何時應用“將臨時值替換為查詢值”
在以下情況下套用此重構:
- 臨時工職位只會分配一次,絕不會重新分配。
- 該計算是一個純表達式:它從字段或參數讀取數據,但不修改外部狀態,不調用網路服務,也不依賴時間或隨機性。
- 計算過程要麼複雜到需要命名才能提高可讀性,要麼簡單到溫度值只會造成乾擾。
- 您即將對使用臨時變數的程式碼區塊應用提取方法
最常見的理想場景是派生值:價格、總價、折扣、格式化字串、條件分類。這些值完全從物件的欄位派生而來,沒有任何副作用,它們自然而然地應該作為物件的屬性,而不是作為方法內部的中間計算結果。
何時不應使用“查詢”替換臨時數據
對性能要求較高的操作。如果計算成本很高,例如資料庫查詢、網路呼叫、O(n²) 循環,或兩次呼叫查詢方法都會增加雙倍的成本。臨時變數的存在正是為了避免這種情況。在這些情況下,要么保留臨時變量,要么對查詢方法進行記憶化(在第一次呼叫後緩存結果):
蟒蛇
# Memoized property: computed once, cached
from functools import cached_property
class Order:
@cached_property
def expensive_validation(self):
return self.external_service.validate(self.data) # called once, cached
副作用操作。如果臨時變數保存的是只應執行一次的操作結果(例如產生唯一 ID、記錄日誌、寫入檔案),則將其轉換為查詢語句會在每次呼叫時執行該操作。這不僅會改變程式的結構,還會改變其行為。請勿將此重構應用於副作用的臨時變數。
循環迭代過程中累積的溫度。 一個溫度計,它是蓄壓器中的一個部件。 for 循環中, total += item.price該值不適合使用「將臨時值替換為查詢」功能。它不是派生值,而是在迭代過程中累積的狀態。如果問題出在循環上,請考慮使用「將循環替換為管道」功能。
相關重構技巧
將臨時檔案替換為查詢語句屬於一系列旨在消除方法中不必要複雜性的技術。了解這些技術有助於開發人員為當前問題選擇合適的技術:
提取方法是最常用的輔助方法。將臨時變數替換為查詢變數通常可以清除變量,從而避免在提取部分和方法的其餘部分之間進行繁瑣的參數傳遞,進而使提取方法得以實現。
內聯臨時變數與引入臨時變數相反:它直接在程式碼中用表達式取代臨時變數。當臨時變數沒有增加程式碼清晰度,且其表達式本身已清晰易讀時,請使用內聯臨時變數。
當同一個臨時變數在同一方法中重複用於多個用途時,可以使用「拆分臨時變數」功能。將其拆分為多個單獨的變量,每個變量的名稱反映其用途,然後對任何生成的僅使用一次的臨時變量應用“將臨時變量替換為查詢”功能。
引入解釋性變數則相反:如果複雜的表達式難以閱讀,引入一個帶有描述性名稱的臨時變數可以提高清晰度。這種方法與「用查詢取代臨時變數」之間存在矛盾,開發人員必須判斷哪種方法更有利於特定程式碼的改進。
將循環替換為管道 針對一種常見模式,即具有臨時累加器的循環可以被鍊式管道操作所取代(map, filter, reduce),這種方式更具陳述性,也更容易閱讀。
SMART TS XL 支持大規模重構。
將 Temp 替換為 Query 屬於局部重構:它只轉換一個方法中的一個變數。對於任何規模較大的程式碼庫來說,更重要的問題不是“如何應用這種重構?”,而是“應該在整個程式碼庫的哪些部分應用這種重構,以及應用後哪些部分會受到影響?”
SMART TS XL 它提供跨程式碼庫的結構分析,使回答這個問題變得系統化。它能辨識出在多個位置以臨時變數形式執行相同計算的情況,而「用查詢取代臨時變數」旨在將這種模式整合到一個命名查詢方法中。它還能追蹤重構後的查詢方法在提取後的使用情況,從而在重構之前就明確其範圍。而且它支援跨語言:對於企業系統,即使 COBOL 程式、Java 服務和 Python 管道都操作相同的數據,它也同樣適用。 靜態程式碼分析 以及 影響分析 識別同一邏輯計算在不同語言中以不同形式出現的地方,這是「用查詢取代臨時表」在單語言層面解決的問題的更深層形式。
對於從事以下工作的團隊 遺產現代化, SMART TS XL“ 依賴關係可視化 這樣就可以在更改重構元件之前查看它們在哪裡被使用,從而確保將計算提取到查詢方法中不會破壞期望原始結構的呼叫者。
臨時變數和自文檔代碼
最終決定用查詢語句取代臨時變量,取決於程式碼應該傳達什麼訊息。臨時變數傳達實作細節:這是我為了得到這個值而執行的計算。查詢方法傳達定義:這個值代表什麼。在 Order 類別中, basePrice() 告訴讀者,該概念在該領域中存在。 double x = quantity * itemPrice 向讀者介紹一種算術運算。
隨著程式碼的演進,領域概念需要穩定的歸宿。嵌入在臨時變數中的計算可能會發生變化,在多個方法中重複出現,或被下一個閱讀它的開發者誤解。而命名查詢方法則可以被找到、測試、記錄,並可依需求進行演進。這種穩定性,體現在所有需要該計算的地方,以及所有使用它的開發者之間,使得「用查詢取代臨時變數」不僅僅是語法變更。它關乎程式碼庫如何清晰地傳達其所解決的問題。


