08.03 如何處理前任程序員留下的代碼

身為一個軟件工程師,我們不可避免的會遇到這樣一些問題:不得不修改別人的代碼,或者在別人的代碼中添加新的功能。

我們並不熟悉這些代碼,它也可能在整個系統中與我們編寫的部分無關。雖然這樣的工作很困難,容易讓人感到無奈,但是要達到足夠的靈活性來也別的開發者一起編寫代碼,收穫也蠻大的。這些收穫包括提高影響力,修復爛軟件,還能學到系統中以前並不瞭解的部分(還可以從其它程序員那裡學到技術和技巧)。

在其它開發者的代碼中工作時,既會感到鬱悶,又會從中有益,考慮到這些因素,我們必須警惕一些極其容易出錯的地方:

我們的自我意識:我們可能會認為自己最有能耐,但通常都不是。我們對要改變的代碼知之甚少,不瞭解原作者的意圖,也不瞭解多少年有哪些因素導致這些代碼形成,以及作者在編寫這些代碼的時候使用了什麼樣的工具和框架。謙卑價值萬金,我們應該時刻保持這種心態。

原作者的自我意識:我們要接觸的代碼來自另一個開發者,他/她有自己的網絡、約束、最後期限等,當然也有他/她自己的生活(這會佔用一點工作時間)。他/她也是一個人,當我們質疑他/她做出的決定,或者質問為什麼代碼這麼糟糕的時候,他/她會自然地產生防禦性心理。我們應該努力讓原作者與我們合作,而不是成為我們工作的阻礙。

恐懼未知事物:我們很多時候會接觸到只瞭解一點點甚至完全不瞭解的代碼。這似乎是件可怕的事情:我們得對自己做出的改動負責,但我們就像是在一個沒有光亮的黑屋子裡走來走去。我們不需要害怕,而是應該建立起一個框架,可以在裡面安心地進行大大小小的修改,同時確保我們不會破壞現有的功能。

所有開發人員,包括我們自己,都是人。因此在別人編寫的代碼上工作,會受到人性的影響。在本文中,我們會講述五種方法,利用人性的優點,從現有代碼和原作者身上取得儘可能多的收穫,並改善代碼既有的狀態。

雖然這個清單並不全面,但應用這些方法將確保我們在完成對別人代碼的修改工作後,會有信心保持現有功能的工作狀態,同時又能保證新功能融合在現有代碼中。

確保測試的存在

對於別的開發人員寫出來的功能,它確實如預期一樣工作嗎?我們所做的修改是否會妨礙它按照預期工作?對此,能讓人產生信心完成前述問題的方式就是,用測試來支持代碼。我們在閱讀別人的代碼時,會發現兩種可能的狀態:

(1) 沒有達到足夠水平的測試

(2) 有達到足夠水平的測試

對於前者,我們會陷入創建測試的困境;而對於後者,我們可以使用現有的測試來確保我們所做的修改不會破解原來的代碼,同時也能從測試中大量地瞭解到代碼的意圖。

創建新測試

這聽起來可能很慘:我們在更改另一個開發人員的代碼時,要對我們的行為負責,但我們無法保證更改是否會造成破壞。吐槽是沒有用的。不管我們發現代碼是什麼狀態,只要動了代碼,就得對其負責。因此,我們應該在修改代碼的時候控制自己的行為。如果不想造成破壞,那就自己寫測試。

這很枯燥,但我們可以通過編寫測試來了解代碼,這也是它的主要優點。假如現在的代碼工作良好,我們需要編寫測試,使其在獲得預期輸入的情況下產生預期的輸出。在寫測試的過程中,我們會逐漸瞭解代碼的意圖和功能。比如,存在如下代碼:

如何處理前任程序員留下的代碼

我們對其功能和代碼中使用的魔法數字[譯者注:指直接的數字常量]並不瞭解,但我們可以創建一組測試,根據已知的輸入產生已知的輸出。比如,通過簡單的數學運算分析成功人士的薪資。

我們發現如果 30 歲以下的人每年掙大約 $68,330,就會被認為是成功的(按代碼中的標準)。雖然我們不知道那些魔法數字是什麼意思,但我們知道它們會減少原始薪資。這樣,$68,330 這個閾值是扣除前的基本薪資。使用這些信息,我們可以創建一些簡單的測試,如下:

如何處理前任程序員留下的代碼

通過這三項測試,我們對現有代碼的工作原理已經有了一個大致的瞭解:如果一個人年齡在 30 歲以下,並且他們每年賺取 68,300 美元,那麼他們就被認為是成功的。雖然我們可以創建更多的測試以確保邊緣用例(如空年齡或薪資)的正常運行,但是一些簡短的測試不僅讓我們瞭解了原始功能,還提供了一套自動化測試工具,可用於確保在對現有代碼進行改動時不會破壞現有功能。

使用現存測試

在現有代碼中存在足夠測試的情況下,我們也可以從測試中瞭解不少東西。就像我們創建測試一樣,我們可以通過閱讀測試從功能級別來了解代碼是如何工作的。另外,我們也可以瞭解到原作者所理解的代碼功能。就算測試不是原作者,而是其他人(在我們之前)寫的,它仍然可以向我們提供其他人對代碼意圖的理解。

即使現在的測試很有幫助,我們仍然要保持謹慎。我們很難判斷測試是否和代碼的變化保持一致。如果一致,我們就擁有理解代碼的堅實基礎;如果不一致,我們就必須小心不要被誤導。比如,如果原薪資閾值是每年 $75,000,後來改為我們知道的 $68,330,那麼這個過時的測試可能會把我們引入歧途:

如何處理前任程序員留下的代碼

這個測試仍然會通過,但不是預期的效果。它能通過不是因為正確的閾值,而是因為它超過了閾值。如果這個測試集中包括一個測試用例,其薪資只比閾值少 $1 時返回 false,那麼第二個測試會失敗,這表示閾值是錯誤的。

如果套件沒有這樣的測試,那麼舊的數據很容易對我們瞭解代碼的實際意圖產生誤導。當存在疑問的時候,請相信代碼:正如我們前端所展示的,解決閾值的問題表明測試並未針對實際的閾值。

此外,參考代碼庫日誌(比如 Git 日誌)來了解代碼和測試用例:如果最後更新代碼的時間比最後更新測試的時間要新得多(並且代碼中存在重大的代碼,比如修改閾值),那麼測試可能已經過時,需要謹慎對待。注意,不要完全忽略它們,因為它們還可能為我們提供一些原作者(或最近編寫測試的開發者)的資料,不它們可能包含過時或錯誤的數據。

和編寫代碼的人談談

在任何涉及多個人的工作中,溝通都至關重要。無論是在公司中、越野旅行中或是在項目中,缺少溝通都極易產生嚴重後果。儘管我們在創建新代碼的時候進行溝通,但當我們接觸既存代碼時,風險還是會增加。因為我們對既存代碼的瞭解有限,我們所瞭解的東西有可能受到了誤導,也有可能過於片面,因此,為了真正理解現有的代碼,我們需要與編寫它的人交談。

在問問題的時候,我們要確保問題是有針對性的,能達到我們理解代碼的目的。比如:

哪裡可以找到概述系統的文檔?

你有沒有相關的設計方案或圖表?

有我需要注意的坑嗎?

某個組件或類是做什麼用的?

有沒有你本想寫進代碼,當時卻沒有寫的東西?為什麼?

始終保持謙虛,並從原作者那裡尋找真正的答案。幾乎每個開發者都有在他或她查看他人代碼時的這種案例,他們會自問:“為什麼他/她要這樣做?為什麼他們不這樣做?” 僅花費數小時才能得出與原作者相同的結論。

大多數開發人員都是有才華的程序員,所以這種情況下可能是一個好主意:假設如果我們遇到了一個看起來很糟糕的決策,可能存在一個好的理由需要這麼做(這也有可能沒有,但較好是在進入別人的代碼之前假設有一個很好的理由;如果沒有,我們可以通過重構來使得這個改動變得有意義)。

軟件開發中,溝通也存在一定的副作用。康威定律,這個最初於 1967 年由 Melvin Conway 提出的定律:

任何在設計系統的組織...都不可避免的會產生設計,其結構是組織溝通結構的副本。

也就是說,一個大團隊緊密溝通,就有可能產生整體的、緊密耦合的代碼,而一組相對較小的團隊可能會產生更多獨立、松耦合的代碼(更多相關信息,請閱讀康威定律解密)。對於我們來說,我們的通信結構不僅影響我們某段代碼,還會影響整個代碼庫。因此,與原作者保持緊密的溝通是一個好辦法,但我們應該避免過於依賴原作者。過分依賴會讓原作者感厭煩,也可能在代碼中產生不可預料的耦合。

雖然這可能有助於深入研究我們的代碼,但這是我們假設可以接觸原作者的情況下。在很多時候,原作者可能已離開公司,或者不在身邊的(例如休假)。我們在這種情況下要做什麼呢? 詢問可能對此代碼有想法的人。這並不一定是一個真正從事編碼工作的人,但也可能是周圍的某人或熟悉編寫代碼之人的人。只要從原作者身上得到哪怕一個想法,也有可能揭示一些代碼中的未知片段。

幹掉所有警告

在心理學上有一個著名的概念叫“破窗理論”,這個理論由 Andrew Hunt 和 Dave Thomas 在程序員修煉之道(4-6頁)揭示。這一理論,最初發展自 James Q. Wilson 和 George L. Kelling:

想像一棟有幾扇破窗戶的建築。如果窗戶沒有修好,那麼破壞者會趨於打破更多窗戶。最終甚至有人會強行進入這棟建築,如果這棟建築沒有住人,它可能會被佔用甚至會有人在裡面生火。也可以想像一下堆積著一些枯枝落葉的人行道。很快,就會產生更多的垃圾。最終,人們逐漸會在那裡扔掉外賣的垃圾袋甚至報廢的汽車。

這一理論認為,人性會放棄照管某個似乎已經無人照管的事務。比如,人們更容易去破壞顯得凌亂的建築。就軟件而言,如果開發人員發現代碼已經是一團糟,那麼繼續搞亂就很正常。從本質上來說,我們對自己說(儘管字不太多),“如果前任都不在乎,我為什麼要在乎?”或者“我搞亂的東西會被隱藏在這個爛攤子下面”。

不過,這不應該成為我們的藉口。我們應該停止推卸負責。一旦我們接觸到他人留下的代碼,就要對它負責,如果它出現問題,我們就得接受責問。為了確保我們能戰勝這一人性發展的必須趨勢,我們需要小步前進,逐步改善代碼的凌亂狀況(更換壞掉的窗戶)。

有一個簡單的方法是去掉整個包或模塊中的所有警告,刪除掉未使用或註釋掉的代碼。如果我們以後需要這些代碼,可以從代碼庫之前的提交中找到它。如果存在不能解決的警告(如原始類型警告),對方法或者其調用添加 @SuppressWarnings 註解。這確保我們對代碼進行了深思熟慮:它們不是因為疏忽造成的警告,而是已經注意到的警告(比如原始類型)。

一旦我們刪除或明確禁止所有警告,我們必須確保代碼保持無警告狀態。這有兩個主要的含義:

它迫使我們對我們所創建的任何代碼保持慎重。

它減少了代碼腐爛的改動,這樣警告會導致以後的錯誤。

這對他人或我們自己都有心理暗示作用,即我們是真的關心我們正在處理的代碼。這不再是一個集合空間,其中我們盲目做出修改,提交,過後不再查看。相反,我們要對此代碼的責任慎重一些。這也有助於未來的發展,向未來的開發者展示:這不是一個破窗的倉庫:它是一個維護良好的代碼庫。

重構

在過去幾十年中,重構已經發展成為一個非常強大的述語,近年來它成為了變更工作代碼的同義詞。儘管重構確實涉及到對工作代碼的修改,但這並不是它的完整意義。Martin Fowlerd 在它的開創性著作《重構》中將重構定義為:

對軟件內部結構進行更改而不改變其表現的行為,使其更易於理解、更易於修改。

這個定義的關鍵在於它涉及的變化並不會改變系統的行為表現。也就是說,我們在重構代碼的時候,必須保證代碼對外部可見的行為不會發生變化。在我們的示例中就是指我們自己修改或創建的測試集。為了保證我們沒有改變系統的外部行為,每次改變我們都應該重新編譯並完整地進行測試。

此外,並非我們所做的每一次修改都可以被認為是重構。比如,重命名一個方法使其更好的反映其用途是一種重構,它加入了新功能就不是。為了看到重構的好處,我們會重構 SuccessfulFilter。我們首先要使用抽取方法這一重構手段來更好的封裝計算個人淨薪資的邏輯:

如何處理前任程序員留下的代碼

做出這個修改之後,重新編譯並運行測試集,保持通過。現在的代碼已經很容易看到成功的依據是年齡和淨薪資,但是 getNetSalary 方法似乎並不屬於 SuccessfulFilter,它應該是 Person 類(這樣說是因為這個方法的參數是 Person 對象,也只調用了 Person 的方法,所以它更接近 Person)。為了更好的放置這個方法,我們使用移動方法將它移動到 Person 類。

如何處理前任程序員留下的代碼

如何處理前任程序員留下的代碼

為了進一步清理這段代碼,我們對魔法數字分別執行將魔法數字替換為符號常量。為了找到每一個值的含義,我們可能要與原作者或者有足夠相關領域知識的人交談,以獲得正確的結果。我們還會多次執行抽取方法重構以確保現在的方法儘可能簡單。

如何處理前任程序員留下的代碼

如何處理前任程序員留下的代碼

重新編譯,然後測試,發現系統仍然如預期運行:我們沒有改變外部行為,但我們已經改善了代碼的內部結構和可靠性。想了解更多更復雜的重構方法和重構過程,請閱讀 Martin Fowler 的重構,以及非常棒的重構大師網站。

讓代碼比你發現它的時候更好

最後的方法在概念上很簡單,做起來卻很難:讓代碼比你發現的時候更好。我們在梳理代碼,特別是別人的代碼時,我們傾向於添加功能,測試新功能,然後繼續,而不會關注我們為其貢獻代碼的軟件存在糟糕的代碼,或者我們新添加到某個類的方法可能會造成混淆。因此,本文總的來說可以歸納為如下原則:

當我們對代碼進行更改時,確保它會比我發現它的時候更好。

如前所述,現在我們在對所修改代碼負責,如果它有問題,我們會負責修復問題。為了抵禦生產軟件帶來的負面影響,我們必須強制自己動過的代碼會比原來更好。我們償還技術債務而不是迴避問題,確保下一個接觸到這段代碼的人不需要付出代價,並對其產生興趣。沒人知道以後如何,也許我們以後會感謝自己的及時修補。


分享到:


相關文章: