代碼異味:它們是什麼以及它們與技術債的關係

代碼異味:它們是什麼以及它們與技術債的關係

程式碼異味並非程式缺陷。有缺陷的程式會崩潰、傳回錯誤結果或測試失敗。而存在程式碼異味的程式可能運行多年都毫無問題,但每次修改都會增加不必要的成本,每個新功能都會帶來意想不到的風險,每次重構都會暴露出先前未知的依賴關係。程式碼異味是程式碼中預示未來問題的結構性特徵:它們不會立即導致故障,但會使未來的每一次修改都變得更加困難、緩慢和危險。

「重構」一詞由 Martin Fowler 和 Kent Beck 在 Fowler 的著作《重構:改進現有程式碼的設計》(1999 年)中推廣開來。書中列舉了 22 種程式碼異味,並為每種異味搭配了一種對應的重構技巧。目錄至今仍是權威參考,Fowler 列舉的這些代碼異味,例如長方法、上帝類、重複代碼、特性偏好、發散性變更、亂槍式修改等等,如今仍然出現在 SonarQube 規則集、靜態分析工具和代碼審查清單中​​。

清除程式碼異味

SMART TS XL 幫助在複雜系統中映射和修復它們。

更多資訊

什麼是代碼異味?

程式碼異味是指原始碼表面的一些特徵,這些特徵暗示著更深層的結構或設計問題。程式碼可以編譯、通過測試並產生正確的輸出,但其結構上的某些問題使得程式碼難以閱讀、擴展或安全修改。福勒的定義是:“一種表面跡象,通常對應於系統中更深層次的問題。”

程式碼異味並非像文法錯誤或斷言失敗那樣意義上的違規行為。它們是指示符,是經驗豐富的開發者能夠識別的警告信號,即使沒有立即顯現的故障。危險在於它們的累積性:在一個擁有 10,000 行程式碼的程式庫中,一個冗長的方法可能只是一個小問題。但如果數百個冗長的方法、分佈在數十個模組中的重複邏輯,以及位於依賴關係圖中心的“上帝類”,那麼這個系統就變得難以安全地進行修改。

程式碼異味、漏洞與技術債

這三個概念雖然相關但又有所區別,混淆它們會導致優先排序錯誤:

概念定義立即失敗?怎麼找
問題導致錯誤行為的程式碼是的,測試失敗,使用者回報錯誤測試、監控、錯誤日誌
代碼氣味預測未來問題的結構模式不,程式碼運作正常。程式碼審查、靜態分析
技術債務過去走捷徑與糟糕決策的累積代價不,但會隨著時間而複利指標、複雜度分析、重構工作量估算

程式碼異味是技術債累積的機制。程式碼庫中每個冗長的方法都會增加一個技術債單位;其利息支出體現在未來每個開發者理解該方法所花費的額外時間,以及未來每次修改都需花費時間來避免其冗長帶來的副作用。

SonarQube 中的程式碼異味是什麼?

SonarQube 將程式碼問題分為三類:缺陷(絕對錯誤)、漏洞(安全性問題)和程式碼異味(可維護性問題)。 SonarQube 的程式碼異味直接對應 Fowler 的程式碼異味分類,包括方法過長(超過可設定的行數閾值)、重複程式碼區塊、參數過多、認知複雜度過高、缺少錯誤處理以及架構耦合性問題等規則。 SonarQube 的程式碼異味規則是業界應用最廣泛的 Fowler 原始分類法的自動化操作化實作。

Martin Fowler 的《代碼異味:經典分類法》

Fowler最初提出的22種代碼異味,依類別組織,至今仍是標準參考。所有主流靜態分析工具的規則集都源自於這個分類體系。

項目類別代碼異味
脹氣程式碼已經變得難以管理冗長的方法、龐大的類別、對原始值的執著、過長的參數列表、資料塊
物件導向濫用者濫用物件導向原則switch語句、臨時欄位、拒絕遺贈、具有不同介面的替代類
變革阻礙者使變革變得困難發散性變化、霰彈槍式手術、平行繼承層級
可有可無不必要的程式碼註解過多、重複程式碼、惰性類別、資料類別、死程式碼、推測性泛化
成色過度耦合特徵嫉妒、不恰當的親密行為、訊息鏈、中間人

了解某種氣味屬於哪一類有助於確定修復的優先順序:膨脹者和變更阻止者與高重構成本直接相關;耦合者與架構脆弱性直接相關;可有可無者是最安全的移除對象。

最常見的程式碼異味:快速參考

程式碼異味它看起來像什麼主要風險
重複程式碼同樣的邏輯在多處出現。錯誤修復必須應用於所有地方;副本會隨著時間推移而出現差異。
長方法方法超過 20-30 行,且涉及多個職責認知負荷高;難以測試孤立行為
神級/大型班一個包羅萬象的類每次功能變更都會影響同一個類別;合併衝突,脆弱性
長參數列表需要 4 個或更多參數的方法容易傳遞錯誤值;呼叫站點難以讀取。
功能嫉妒使用其他類別的資料多於自身資料的方法。緊密耦合;一個類別的改變會破壞另一個類別。
分歧變化一個班級因多種不同原因進行了修改違反單一責任原則;不可預測的副作用
霰彈槍手術一項更改需要對多個類別進行編輯變更成本高;容易錯過實例
死程式碼從未被調用或執行的程式碼令開發人員感到困惑;多年累積;使遷移變得複雜
原始痴迷使用基本型別(字串、整數)而非領域對象驗證分散在各處;表達能力差
資料區塊同一組場地反覆一起經過應該是一個領域對象;這顯示缺少抽象。
推測性普遍性為設想的未來需求而寫的程式碼不必要的複雜性;沒人明白它存在的意義。
不一致的錯誤處理靜默捕獲,不同的例外策略故障無法被偵測到;調試耗時更長

程式碼異味定義及範例

重複程式碼

大型系統中,最常見且成本最高的程式碼異味就是程式碼重複。重複程式碼的產生源自於複製貼上式的開發模式、時間壓力以及各自為政、獨立解決相同問題的團隊。其直接後果是維護成本的增加:共享邏輯的任何變更都必須應用到所有副本中。

Java的

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

當業務規則發生變化(閾值變為 1500,比率變為 12%)時,其中一個副本會更新,而另一個副本則不會。現在,兩個模組在基本業務邏輯上存在分歧,這種差異是在生產環境的審計過程中而不是在測試中暴露出來的。

解決方法:將共享邏輯提取到一個單獨的函數、實用程式類別或共享庫中,供兩個呼叫者引用。

長方法

隨著時間的推移,一種方法承擔了更多職責,使其功能超出了最初的預期。閱讀一個 200 行的方法與閱讀 20 個 10 行的方法在認知負荷上存在質的差異,而不僅僅是數量上的差異。冗長的方法難以測試,因為它們執行的操作太多,無法單獨進行測試;也難以理解,因為讀者必須將整個執行上下文保存在工作記憶中。

偵測閾值:超過 20-30 行的方法需要審查;超過 50 行,幾乎總是需要重構。在 COBOL 中,超過 100 行的段落也具有相同的閾值。

蟒蛇

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

此方法中的每項職責都應該是一個獨立的類別或函數。將它們捆綁在一起意味著,未來對發票、庫存或通知的每一次更新都可能導致整個訂單處理流程不穩定。

神級

一個類別承擔了跨多個領域的職責,嚴重違反了單一職責原則,以至於它成為了程式碼庫的重心:一切都依賴它,並且更改其中的任何內容都需要了解關於它的一切。

偵測訊號:一個類別有 20-30 個以上的公共方法,或一個類別的名稱包含“Manager”、“Processor”、“Handler”、“Utils”或“Helper”,並且套用到多個不相關的領域。

分歧變化

這是一個因各種不同且互不相關的原因而被修改的類別。每次資料庫架構更改時,都需要編輯這個類別;每次定價規則更改時,都需要編輯這個類別;每次通知格式更改時,也需要編輯這個類別。這個類別承擔了太多職責,應該要拆分。

定義:一種因各種原因而不斷改變的情況。與「霰彈式手術」相反。

霰彈槍手術

一個概念上的改變,卻需要對多個不同的類別進行修改。例如,更改稅率需要在五個不同的地方修改後端計算、前端驗證、資料庫觸發器、批次作業和報表查詢。任何一個地方的疏忽都會導致行為不一致。

SQL

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

現在將 0.05 更改為 0.07 需要查找 SQL 檔案、預存程序和應用程式程式碼中所有出現的位置。

功能嫉妒

如果一個方法花費更多時間使用另一個類別的資料和方法,而不是使用自己的類,這表示該行為可能更適合放在另一個類別中。

Java的

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

死程式碼

程式碼庫中存在但從未被生產環境中的任何執行路徑呼叫的程式碼。隨著功能的移除、替換或重構,舊程式碼往往會被保留下來,導致死程式碼的累積。這不僅會給程式碼審查帶來幹擾,還會讓新加入的開發人員感到困惑,使遷移分析更加複雜,並且偶爾會被意外地重新啟動。

發現靜態分析工具包括 SonarQube、Knip(用於 TypeScript/JavaScript)等。 SMART TS XL 找出程式碼庫中不可達的函數、未呼叫的方法和未使用的變數。

DRY原則違反

「不要重複自己」(DRY)原則指出,系統中的每一項知識都必須有單一且明確的表示形式。違反 DRY 原則是導致程式碼重複、資料堆積以及許多「亂槍打鳥」式修改場景的根本原因。當業務邏輯在多個地方表示時,這些表示形式必然會有所差異。 DRY 是原則本身;代號重複則是表示違反 DRY 原則的「異味」。

蟒蛇

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

偵測閾值:代碼何時會變成異味?

代碼異味檢測需要可衡量的閾值。以下是一些常用的指標以及表明存在需要關注的程式碼異味的閾值:

公制它衡量什麼警告閾值臨界閾值
圈複雜度方法中的決策分支數量上述10上述20
方法長度(行)方法/函數中的行數上述20上述50
參數計數方法接受的參數數量上述4上述7
班級長度一個類別中的行數上述200上述500
重複率重複程式碼的百分比3%以上10%以上
認知複雜性這段程式碼有多難理解上述15上述25
傳入耦合(Ca)依賴該類別的類別的數量上述15上述30
傳出耦合(Ce)本課程所依賴的課程數量上述15上述30

這些閾值在 SonarQube 中是可配置的,大多數靜態分析平台也允許基於這些指標自訂規則。達到關鍵閾值的類別和方法是優先順序最高的重構目標:它們最容易導致未來的缺陷,也是維護成本最高的元件。

代碼氣味檢測工具

對於大型程式碼庫而言,自動化檢測是唯一可擴展的程式碼異味識別方法。人工審查只能發現自動化工具發現的一小部分異味,而且人工審查無法擴展到擁有數百萬行程式碼的遺留系統。

工具母語它檢測到什麼
SonarQube / SonarCloudJava、Python、JS/TS、C# 等完整的福勒氣味分類法、安全熱點、重複項
Checkstyle + PMDJava的風格違規、重複、複雜度指標
ESLint + typescript-eslintJavaScript、打字稿冗長的函數、複雜的程式碼、未使用的程式碼
皮林特 + 氡蟒蛇複雜性、風格、可維護性指數
ReSharper / RiderC#冗餘程式碼、冗長的方法、耦合問題
大眼夾Rust 中常見的程式碼異味,即不合慣用的慣用模式。
代碼氣候支持多語言複雜性、重複性、可維護性評分
SMART TS XLCOBOL、JCL、Java、Python、RPG、SQL、.NET跨語言重複、死程式碼、耦合、依賴漂移

Rust 中的程式碼異味 主要由 Clippy 捕獲,它強制執行 Rust 的慣用模式。最常見的 Rust 特有代碼異味包括不必要的克隆、濫用… unwrap() 在生產路徑中,過度嵌套的匹配表達式以及應該返回值的函數 Result 但應該使用 panic 來代替。

代碼異味與技術債:二者之間的聯繫

技術債是指過去為了追求速度而犧牲品質所累積的成本。代碼異味則是這種債務在代碼結構中反映出來的機制。二者關係直接:每個未解決的代碼異味都代表著一筆技術債務,而利息則是未來每次變更都必須花費額外時間來規避它。

正如在軟體變更管理的影響分析中所描述的那樣,程式碼異味所表明的結構性問題(過度耦合、重複邏輯、死程式碼累積)會直接增加每次變更的範圍,因為它們使得隔離任何給定變更將影響的內容變得更加困難。

用程式碼異味來解釋技術債:如果程式碼庫中有 40% 的重複程式碼,那麼每次修復 bug 的成本將是正常成本的 1.4 倍。如果核心處理類別是所有功能都依賴的“上帝類”,那麼每次添加新功能都需要理解和測試整個類別。如果錯誤處理不一致,那麼每次生產事故都需要更多的時間進行調查,因為故障訊號不可靠。技術債並非抽象概念,而是這些效率低下問題累積的結果。

CISQ 的研究始終表明,開發人員花費 30-40% 的時間來解決技術債務,而不是開發新功能。程式碼異味密度是衡量技術債累積程度最直接的指標。

SMART TS XL 大規模檢測程式碼異味

像 SonarQube 和 Clippy 這樣的獨立工具只能在單一語言環境下運作。但在企業環境中,COBOL 程式會寫入由 Java 服務讀取的資料集,JCL 作業流會呼叫多種語言編寫的程序,而且相同的業務邏輯可能在三個不同年代編寫的三個不同系統中被獨立複製,在這種情況下,單語言工具無法掌握全局。

SMART TS XL“ 靜態程式碼分析 能夠同時偵測環境中所有語言的程式碼異味:COBOL 副本和 Java 實用程式類別之間的重複邏輯、RPG 程式中沒有 JCL 作業呼叫的死程式碼、COBOL 程式中的上帝類別模式(其中單一段落完成 50 個段落的工作)以及跨語言邊界的不一致錯誤處理模式。

應用程式依賴關係映射功能可以識別單一檔案級工具無法看到的架構異味:哪些元件具有最高的傳入耦合(最受依賴,更改時破壞的風險最高),哪些模組之間存在循環依賴關係(這些模組本應是獨立的),以及哪些重複的業務邏輯在不同的系統中獨立維護,而兩個副本彼此之間並不知曉。

影響分析功能使程式碼異味變得可控:在重構任何高耦合元件之前,影響分析會列舉所有需要測試、驗證或更新的依賴元件。這改變了團隊在大型程式碼庫中遇到的「重構癱瘓」局面,將其轉化為結構化、範圍明確的修復計劃,其中每次變更都有明確的範圍,而不是未知的風險。

對於進行遺留系統現代化改造的團隊來說,程式碼異味分析是現代化改造計畫的基礎:在遷移開始之前消除無用程式碼(縮小範圍),將重複的邏輯合併為規範的實現,最後現代化耦合度最高的元件(在所有依賴它們的元件都被處理之後),並在轉換為新語言之前分解上帝類別,因為將上帝類別轉換為 Java 中的上帝類別。

解決程式碼異味:優先權排序框架

並非所有程式碼異味都需要立即重構。正確的做法是基於風險的優先順序:

優先權 1:高變更率元件中的程式碼異味。頻繁變更且複雜度或耦合度高的程式碼最容易產生缺陷。這些組件每次變更的成本最高,也最容易引發生產事故。請優先修復這些問題。

優先權 2:架構邊界處的異味。上帝類別和高度耦合的元件(所有元件都依賴它們)是最難修改的,但也是代價最高的。這些都需要在重構之前先進行最仔細的影響分析。

優先權 3:跨系統邊界的重複程式碼。當相同的業務邏輯存在於多個系統中時,必須同時協調所有副本的變更。整合這些重複程式碼可以減少協調開銷並防止分歧。

優先權 4:移除死代碼。死程式碼是最安全的處理類別:移除它不會破壞現有功能,只會暴露先前隱藏的依賴關係。應在任何遷移或轉換之前移除死程式碼,以避免浪費精力轉換永遠不會被呼叫的程式碼。

優先級 5:低風險區域的風格和結構異味。對於穩定且低變更頻率的程式碼,可以抓住機會,在附近程式碼因其他原因需要更改時,同時重構周圍的異味,以解決過長的方法和參數列表過長的問題。

系統地檢測、測量和解決程式碼異味,而不是在異味已經導致生產故障時才被動地處理,這種做法區分了能夠長期保持交付速度的開發團隊和隨著系統增長而逐漸放慢速度的開發團隊。