異味程式碼到底有多糟糕?
—— 衡量程式碼異味的影響
作者: Aiko Yamashita 和 Thomas Flemming,翻譯:@龔凌暉
用科學實驗來衡量哪些程式碼異味最難維護。
如何處理有異味的程式碼?
前輩曾經教導我們,作為開發人員,我們最主要的職責就是不要寫爛程式碼。除非你是單兵作戰並且只是寫幾行很快就會棄用的Perl指令碼而已,否則最重要的一點,就是你寫的程式碼必須易於閱讀和理解。在軟體產品的整個生命週期中,可維護的程式碼通常會免除你和你的同事們呆坐在電腦前痛苦絕望的很多時間。
然而,導致程式碼可讀性差的原因卻不總是那麼清晰,這就是為什麼像程式碼編寫標準、準則、程式碼模式和程式語言箴言之類的東西存在的原因之一。一條眾所周知的準則是Martin Fowler【1】寫的《重構》這本書,書裡描述了一系列的程式碼異味以及去掉這些異味的重構策略。儘管擁有這個無可估價的資源,我們依然面臨挑戰——除了要學習很多重構策略,還需要決定哪些的優先順序比較高。顯然,它們不可能同樣重要!我們如何得知現在做的程式碼重構工作會有長期正面效益呢?
問題在於Martin Fowler的重構書沒有提及哪些程式碼異味是關鍵的,哪些不是。Fowler他自己也提到,沒有哪個標準或指標能夠比得上人的直覺。我們作為開發人員來說只能依靠直覺和經驗去決定是否需要重構。這可能是一場噩夢。面對一個有數千個程式碼異味的系統,你要從何處入手?除此之外,任何對程式碼的改動都可能帶來意向不到的副作用。就算有高質量的自動化測試,修改程式碼常常蘊含高風險而且代價昂貴。如果知道哪些程式碼異味是最具破壞性,先把它們處理掉就好了。另外,我們希望向管理層展示,我們並不是浪費時間在為了寫漂亮程式碼而寫漂亮程式碼上,而是我們當前的努力能夠在未來為專案帶來長期效益。
【補充】:在軟體開發領域,程式碼中的任何可能導致深層次問題的症狀都可以叫做程式碼異味。
通常,在對程式碼做簡短的反饋迭代時,程式碼異味會暴露出一些深層次的問題,這裡的反饋迭代,是指以一種小範圍的、可控的方式重構程式碼。基於這些暴露的問題,人們會進一步的檢查設計和程式碼中是否還存在別的程式碼異味,然後再做進一步的重構。從負責重構的開發者的角度來看,程式碼異味可以啟發何時重構,如何重構。因此,可以說程式碼異味推動著重構的進行。(摘自維基百科)
收集關於程式碼異味的事實
2009年,挪威Simula研究所需要把一個內部系統改造成一個新的內容管理系統。這個專案被外包給6個Java開發人員。這個專案被認為是深入研究程式碼異味對可維護性造成影響的機會。
在這個專案持續的三個月中,他們使用了一些工具來衡量程式碼異味,每天跟那些開發人員面談,並且每個開發人員的Eclipse IDE上都安裝了一個日誌記錄工具。這個日誌記錄工具不但記錄修改每個檔案花了多長時間,而且記錄了花在搜尋,瀏覽以及翻閱程式碼所花費的時間。 在這個專案過程中,他們使用一個問題追蹤工具登記下那些開發人員所面臨的問題,然後反向追蹤到那些引起問題的原始碼檔案。除此之外,所有的開發人員都要接受面談。這個過程中收集到的資料遠遠多於常規的僅僅分析程式碼倉庫的方法。以下是所得到的觀察結果:
觀察結果1:萬能類其實很糟糕!
來源:https://simula.no/publications/Simula.simula.1460
“人人都知道”萬能類是不好的,可是到底不好到什麼程度?如下圖所示:
Y軸代表在專案維護期間花費在閱讀修改一個檔案上的時間;X軸代表檔案的大小。
請注意,閱讀和修改最大的檔案(1844行程式碼)要花的時間是大部分檔案(少於600行程式碼)的10倍以上。這個檔案就是一個萬能類。另外請注意,20000秒差不多是5個小時的工作(對於一個檔案來說太長了!)。我們還可以看到編輯另一個大檔案(大約1400行程式碼)沒有花掉很多時間。這個大檔案包含了很多的訪問器和修改器,沒有包含任何邏輯(相對於全能類)。這解釋了為什麼開發人員沒有在上面花太多時間。包含複雜邏輯的大檔案(即全能類)會顯著影響可維護性。
建議:你應該把包含複雜邏輯的大檔案分割成小檔案。一個推薦的閾值是將檔案大小保持在1000行程式碼內。
觀察結果2: 資料團塊沒有你想得那麼糟糕……
來源:https://simula.no/publications/Simula.simula.1456
“資料團塊(Data Clump)”是指一些語義上沒有相關性的方法和變數集合。一般情況下,包含資料團塊的檔案包含了不同型別的變數,後面跟著一系列的訪問器和修改器。比如,下圖中Person這個類包含了不直接跟一個人相關的資訊,因此可以被分割成兩個類。Simula的研究人員建立了一個統計模型來解釋程式碼異味的出現是否增加了開發人員在維護過程中遭遇問題的可能性(他們會記錄維護過程中面臨的所有問題以及那些引起問題的檔案)。他們發現,事實上那些包含資料團塊的檔案引起維護問題的可能性更低!
建議:不要去管那些資料團塊,除非它們包含了其他程式碼異味。
觀察結果3: 堅持介面分離原則
Robert C. Martin(Bob大叔)介紹了介面分離原則(ISP)作為 SOLID 原則【2】的一部分。
介面分離原則指出,任何軟體庫的呼叫者都不應該被強迫依賴於它沒有呼叫的方法。介面分離原則把非常龐大的介面分割成更小更加明確的介面,從而使得呼叫者只需要知道他們所關心的方法。
軟體工程的研究者們【3】提出瞭如何使用程式碼量化標準去鑑別一些程式碼異味。有些商業工具實現了這些量化標準,但是你也可以提出自己的探測策略並且在工具中實現它們,比如Java中的SonarQube或者.Net中的NDepend。下圖展示了Simula使用的探測策略,這個策略是基於研究員Radu Marinescu【4】的工作提出的。
這個類的類“介面寬度”(即公開的方法和屬性個數)為10,類的“呼叫者”數目為8,“平均介面用度”(即呼叫者所使用的方法或屬性個數除以呼叫者數目)為0.375。根據這個探測策略,這個類可能違反了介面分離原則.
關於資料團塊的分析同樣涵蓋了違反介面分離原則的檔案,結果發現當一個檔案違反了介面分離原則時,它存在問題的概率的概率會增加。
如下圖所示,違反介面分離原則的檔案比沒有違反該原則的檔案出問題的概率要高(大約30%),這也印證了上述觀點。
建議:分割那些過於寬泛,用途繁多的介面可以減少維護時出現問題的風險。
觀察結果4:程式碼異味往往成群結隊出現
來源:https://simula.no/publications/Simula.simula.1508
在同一個實驗中,某些程式碼異味表現出在同一個檔案中一起出現的趨勢。如下圖所示,識別出來的程式碼異味往往出現在同一個檔案裡。“異味囤積者”本質上就是那些想要囤積所有系統功能的檔案。“混亂因素”是那些在檔案中引起困惑的程式碼異味。另外兩組,“寬泛介面”和“資料容器”的含義則是不言自明的。
建議:如果你在一個檔案中發現了某個型別的程式碼異味,你大概想要檢查它是否包含其他的“異味小夥伴”。同個檔案中的程式碼異味組合會增加風險,減少可維護性!
長文略讀
就像Fowler所說,我們要追隨自己的直覺,經驗和判斷來決定哪些需要重構,但是下列根據科學研究得出的準則可以幫助你來區分優先順序:
- 1. 分割那些包含過多邏輯的大檔案(多於1000行程式碼)
- 2. 關鍵不在於重構資料團塊
- 3. 把那些寬泛而用途繁多的介面按照用途來分割成不同介面
- 4. 包含某些程式碼異味的檔案往往同時會有更多的“不受歡迎的小夥伴”,注意識別它們!
快樂地重構吧!
引用:
【1】《重構:改善既有程式碼的設計》
【2】《敏捷軟體開發(原則模式與實踐) 》
【3】http://sewiki.iai.uni-bonn.de/research/cultivate/tutorial_exploring_smells_and_metrics
【4】http://loose.upt.ro./download/thesis/thesis.zip
補充:常見的程式碼異味
- 重複程式碼: 相同或者相似的程式碼存在於一個以上的地方。
- 長方法: 一個非常長的方法、函式或者過程。
- 巨類: 一個非常龐大的類。
- 太多的引數: 函式或者過程的冗長的引數列表使得程式碼可讀性和質量非常差。
- 特性依戀: 一個類過度的使用另一個類的方法。
- 親密關係: 一個類依賴另一個類的實現細節。
- 拒絕繼承: 子類以一種‘拒絕’的態度,覆蓋基類中的方法,換句話說,子類不想繼承父類中的方法,參考Liskov substitution principle。
- 冗餘類 / 寄生蟲: 一個功能太少的類。
- 人為的複雜: 在簡單設計已經滿足需求的時候,強迫使用極度複雜的設計模式。
- 超長識別符號: 尤其,在軟體工程中,應該毫無保留的使用命名規則來消除歧義。
- 超短識別符號: 除非很明顯,一個變數名應該反映它的功用。
- 過度使用字面值: 為提高可讀性和避免編碼錯誤,應該使用命名常量。此外,字面值可以且應該在可能的情況下,獨立存放於資原始檔或者指令碼中,在軟體部署到不同區域時,可以很方便的本地化。(摘自維基百科)