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

做爲軟件工程師不可避免會遇到的一個場景是:咱們在改變或添加一個功能到不是咱們建立的、咱們不熟悉的、與咱們負責的系統部分無關的代碼中時,會遇到麻煩。雖然這可能會是一個繁瑣而艱鉅的任務,可是因爲使用其餘開發人員編寫的代碼有很大的靈活性,因此咱們能夠從中獲得大大的好處,包括增長咱們的影響範圍,修復軟件腐爛以及學習咱們之前不瞭解的系統部分(更況且,還能夠學習其餘程序員的技術和技巧)。程序員

考慮到使用其餘開發人員編寫的代碼既有其厭煩之處,又有其優點所在,因此咱們必須當心不要犯一些嚴重的錯誤:框架

  • 咱們的自我意識:咱們可能會以爲本身知道得最多,但一般事實並不是如此。咱們要更改的是咱們知之甚少的代碼——咱們不知道原做者的意圖、致使此代碼的決策以及原做者在寫代碼時可用的工具和框架,等等。謙遜的品質價值千金,你值得擁有。
  • 原做者的自我意識:咱們即將接觸的代碼是由另外一個開發人員所編寫的,另外一種風格、約束、期限和我的生活(消耗他或她工做以外的時間)。只有當咱們開始質疑他或她作出的決定或質疑代碼爲何這麼不乾淨的時候,那人才會自我檢討,不至於夜郎自大。咱們應該盡一切努力讓原做者幫助咱們工做,而不是妨礙咱們。
  • 對未知的恐懼:不少時候,咱們將要接觸的代碼是咱們知之甚少或徹底一無所知的。使人懼怕的是:咱們將對咱們所作的任何改變負責,可是咱們基本上就像是在沒有光線的黑暗屋子裏走動同樣。其實咱們不須要擔憂,而是應該構建一種使咱們可以在大小不一的改變中感到溫馨的結構,並容許咱們確保沒有破壞現有的功能。

因爲開發人員,包括咱們本身,是人,因此在處理其餘開發人員編寫的代碼時,處理好不少人的天性問題是頗有用的。在這篇文章中,咱們將經過咱們可使用的五種技術來確保將對人性的理解成爲咱們的優點,從現有代碼和原做者汲取儘量多的幫助,並使得其餘開發人員編寫的代碼最後變得比原來更優秀。雖然這裏列出的5個方法並不全面,可是使用下面的技術將確保在結束改動其餘開發人員編寫的代碼時,咱們有信心保持現有功能的工做狀態,同時確保咱們的新功能與現有的代碼庫協調一致。ide

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

1.確保測試的存在工具

要想確保在其餘開發人員編寫的代碼中所存在的現有功能實際可以按照預期的方式工做,而且咱們對其進行的任何更改都不會影響到功能的實現,惟一真正使人信心十足的方式是用測試來支持代碼。當咱們遇到另外一位開發人員編寫的代碼時,代碼有兩種所處的狀態:(1)沒有足夠的測試水平,或(2)有足夠的測試水平。遇到前一種狀況,咱們得負責建立測試,而在後一種狀況下,咱們可使用現有的測試來確保咱們作出的任何更改都不會破壞代碼,並儘量多地從測試去了解代碼的意圖。學習

建立新測試測試

這是一個悲傷的例子:咱們在改變其餘開發人員的代碼時,要對更改結果負責,可是咱們沒有辦法保證咱們在進行更改時不破壞任何東西。抱怨是沒有用的。不管咱們發現代碼處在什麼樣的條件下,咱們總歸是要接觸代碼,所以若是代碼壞掉了,就是咱們的責任。因此咱們在改變代碼時,必定要掌控本身的行爲。肯定不會破壞代碼的惟一方法是本身寫測試。網站

雖然這是乏味的,但它容許咱們經過編寫測試來學習,這是它的主要優勢。假設代碼如今能夠正常工做,而咱們須要編寫測試,以便預期的輸入會致使預期的輸出。在咱們完成這個測試的過程當中,咱們逐漸瞭解到代碼的意圖和功能。例如,給出如下代碼this

 
  1. public class SuccessfulFilterTest { 
  2.     private static final double THRESHOLD_NET_SALARY = 68330.0; 
  3.     @Test 
  4.     public void under30AndNettingThresholdEnsureSuccessful() { 
  5.         Person person = new Person(29, THRESHOLD_NET_SALARY); 
  6.         Assert.assertTrue(new SuccessfulFilter().test(person)); 
  7.     } 
  8.     @Test 
  9.     public void exactly30AndNettingThresholdEnsureUnsuccessful() { 
  10.         Person person = new Person(30, THRESHOLD_NET_SALARY); 
  11.         Assert.assertFalse(new SuccessfulFilter().test(person)); 
  12.     } 
  13.     @Test 
  14.     public void under30AndNettingLessThanThresholdEnsureSuccessful() { 
  15.         Person person = new Person(29, THRESHOLD_NET_SALARY - 1); 
  16.         Assert.assertFalse(new SuccessfulFilter().test(person)); 
  17.     } 

咱們對代碼的意圖以及爲何在代碼中使用Magic number知道得並很少,可是咱們能夠建立一組測試,已知輸入產生已知輸出。例如,經過作一些簡單的數學和解決構成成功的閾值薪水問題,咱們發現若是一我的的年齡在30歲如下,且每一年大概賺68,330美圓,那麼他被認爲是成功的(按照本規範的標準)。雖然咱們不知道那些magic number是什麼,可是咱們知道它們確實減小了初始的薪水值。所以,68,330美圓的閾值是扣除前的基本工資。經過使用這些信息,咱們能夠建立一些簡單的測試,例如:.net

 
  1. public class SuccessfulFilterTest { 
  2.     private static final double THRESHOLD_NET_SALARY = 68330.0; 
  3.     @Test 
  4.     public void under30AndNettingThresholdEnsureSuccessful() { 
  5.         Person person = new Person(29, THRESHOLD_NET_SALARY); 
  6.         Assert.assertTrue(new SuccessfulFilter().test(person)); 
  7.     } 
  8.     @Test 
  9.     public void exactly30AndNettingThresholdEnsureUnsuccessful() { 
  10.         Person person = new Person(30, THRESHOLD_NET_SALARY); 
  11.         Assert.assertFalse(new SuccessfulFilter().test(person)); 
  12.     } 
  13.     @Test 
  14.     public void under30AndNettingLessThanThresholdEnsureSuccessful() { 
  15.         Person person = new Person(29, THRESHOLD_NET_SALARY - 1); 
  16.         Assert.assertFalse(new SuccessfulFilter().test(person)); 
  17.     } 

經過這三個測試,咱們如今對現有代碼的工做方式有了大體的瞭解:若是一我的不到30歲,且每一年賺$ 68,300,那麼他被認爲是成功人士。雖然咱們能夠建立更多的測試來確保臨界狀況(例如空白年齡或工資)功能正常,可是一些簡短的測試不只使咱們瞭解了原始功能,還給出了一套自動化測試,可用於確保在對現有代碼進行更改時,咱們不會破壞現有功能。設計

使用現有測試

若是有足夠的代碼測試組件,那麼咱們能夠從測試中學到不少東西。正如咱們建立測試同樣,經過閱讀測試,咱們能夠了解代碼如何在功能層面上工做。此外,咱們還能夠知道原做者是如何讓代碼運行的。即便測試是由原做者之外的人(在咱們接觸以前)撰寫的,也依然可以爲咱們提供關於其餘人對代碼的見解。

雖然現有的測試能夠提供幫助,但咱們仍然須要對此持保留態度。測試是否與代碼的開發更改一塊兒與時俱進是很難說的。若是是的話,那麼這是一個很好的理解基礎;若是不是,那麼咱們要當心不要被誤導。例如,若是初始的工資閾值是每一年75,000美圓,然後來更改成咱們的68,330美圓,那麼下面這個過期的測試可能會使咱們誤入歧途:

 
  1. @Test 
  2. public void under30AndNettingThresholdEnsureSuccessful() { 
  3.     Person person = new Person(29, 75000.0); 
  4.     Assert.assertTrue(new SuccessfulFilter().test(person)); 

這個測試仍是會經過的,但沒有了預期的做用。經過的緣由不是由於它正好是閾值,而是由於它超出了閾值。若是此測試組件包含這樣一個測試用例:當薪水低於閾值1美圓時,過濾器就返回false,這樣第二個測試將會失敗,代表閾值是錯誤的。若是套件沒有這樣的測試,那麼陳舊的數據會很容易誤導咱們弄錯代碼的真正意圖。當有疑問時,請相信代碼:正如咱們以前所表述的那樣,求解閾值代表測試沒有對準實際閾值。

另外,要查看代碼和測試用例的存儲庫日誌(即Git日誌):若是代碼的最後更新日期比測試的最後更新日期更近(對代碼進行了重大更改,例如更改閾值),則測試可能已通過時,應謹慎查看。注意,咱們不該該徹底忽視測試,由於它們也許仍然能爲咱們提供關於原做者(或最近撰寫測試的開發人員)意圖的一些文檔,但它們可能包含過期或不正確的數據。

2.與編寫代碼的人交流

在涉及多我的的任何工做中,溝通相當重要。不管是企業,越野旅行仍是軟件項目,缺少溝通是損害任務最有效的手段之一。即便咱們在建立新代碼時進行溝通,可是當咱們接觸現有的代碼時,風險會增長。由於此時咱們對現有的代碼並不太瞭解,所以咱們所瞭解的內容多是被誤導的,或只表明了其中的一小部分。爲了真正瞭解現有的代碼,咱們須要和編寫它的人交流。

當開始提出問題時,咱們須要肯定問題是具體的,而且旨在實現咱們理解代碼的目標。例如:

  • 這個代碼片斷最適合放到系統的哪裏?
  • 你有什麼設計或圖表嗎?
  • 我應該注意什麼陷阱?
  • 這個組件或類是作什麼的?
  • 有沒有什麼你想放到代碼裏,但當時沒有作的?爲何?

始終要保持謙虛的態度,積極尋求原做者真正的答案。幾乎每一個開發人員都碰到過這樣的場景,他或她看着別人的代碼,自問自答:「爲何他/她要這樣作?爲何他們不這樣作?」而後花幾個小時來得出原本只要原做者回答就能獲得的結論。大多數開發人員都是有才華的程序員,因此即便若是咱們遇到一個看似糟糕的決定,也有可能有一個很好的理由(可能沒有,但研究別人的代碼時最好假設他們這樣作是有緣由的;若是真的沒有,咱們能夠經過重構來改變)。

溝通在軟件開發中起次要反作用。1967年最初由Melvin Conway創立的康威定律規定:

  • 設計系統的任何組織…都將不可避免地產生一種設計,該設計結構反映了組織的通訊結構。

這意味着,一個龐大、緊密溝通的團隊可能會生成一體化,緊密耦合的代碼,但一些較小的團隊可能會生成更獨立、鬆散耦合的代碼(有關此相關性的更多信息,請參閱《Demystifying Conway’s Law》)。對於咱們來講,這意味着咱們的通訊結構不只影響特定的代碼段,也影響整個代碼庫。所以,與原做者密切溝通絕對是一個好辦法,但咱們應該自檢不要太過於依賴於原做者。這不只可能會惹惱原做者,還可能在咱們的代碼中產生無心識的耦合。

雖然這有助於咱們深刻研究代碼,但這是在假設能夠接觸原做者的狀況下。在不少時候,原做者可能已經離開了公司,或恰巧不在公司(例如正在休假)。在此種狀況下咱們該作什麼?詢問可能對代碼有所瞭解的人。這我的不必定要曾真正工做於代碼,他能夠是在原做者編寫代碼時就在周圍,也能夠是認識原做者。哪怕僅是從原開發者周圍的人中獲得隻言片語,也可能會啓迪其餘未知的代碼片斷。

3.刪除全部警告

心理學中有一個衆所周知的概念,稱爲「破窗理論」,Andrew Hunt和Dave Thomas在《 The Pragmatic Programmer 》(第4-6頁)中詳細描述了這個概念。這個理論最初是由James Q.Wilson和George L. Kelling提出的,描述以下:

假設有一個建築物有幾扇破了的窗戶。若是窗戶沒有修好,那麼破壞者會趨向於打破更多的窗戶。最終,他們甚至可能會破門而入,若是建築物是沒人住的,那麼他們可能會非法佔有或者在裏面點火。也能夠考慮人行道的狀況。若是道路上面有垃圾堆積,那麼不久以後,就會有更多的垃圾累積。最終,人們甚至會開始往那裏扔外賣垃圾,甚至打破汽車。

這個理論指出,若是彷佛已經沒人關心這個物品或事物,那麼咱們就會忽視對物品或事物的照顧,這是人的天性。例如,若是一棟建築物看上去已經凌亂不堪,那麼它更有可能被肆意破壞。在軟件方面,這個理論意味着若是開發人員發現代碼已是一團糟,那麼人的本性會讓他弄壞代碼。從本質上說,咱們內心想的是(即便心理活動沒有這麼豐富),「既然最後一我的不在意這代碼,我爲何要在意?」或「都是亂糟糟的代碼,誰知道是誰寫的。」

可是,這不該該成爲咱們的藉口。只要咱們接觸之前屬於其餘人的代碼,那麼咱們就要對這些代碼負責,而且若是它不能有效工做的話,咱們得擔負後果。爲了打敗這種人的天性行爲,咱們須要採起一些小措施以免咱們的代碼更少地被弄髒(及時更換破掉的窗戶)。

一個簡單方法是刪除來自咱們正在使用的整個包或模塊中的全部警告。至於未使用或添加註釋的代碼,刪除它。若是咱們稍後須要這部分代碼,那麼在存儲庫中,咱們老是能夠從先前的提交中檢索它。若是存在沒法直接解決的警告(例如原始類型警告),那麼使用@SuppressWarnings註解註釋該調用或方法。這樣能夠確保咱們對代碼進行過仔細的考慮:它們不是由於疏忽而發出的警告,而是咱們明確地注意到了警告(如原始類型)。

一旦咱們刪除或明確地禁止全部警告,那麼咱們就必須確保代碼保持免除警告。這有兩個主要做用:

  • 迫使咱們仔細考慮咱們建立的任何代碼。
  • 減小代碼腐敗的變化,如今的警告會致使之後的錯誤。

這對其餘人,以及咱們本身都有心理暗示做用——咱們其實關心咱們正在處理的代碼。它再也不是條單行線——咱們強逼着本身更改代碼,提交,而後永不回頭。相反,咱們認識到咱們須要對這代碼負責。這對以後的軟件開發也是有幫助的——它向未來的開發人員展現,這不是一間窗戶都破了的倉庫:而是一個維護良好的代碼庫。

4.重構

在過去幾十年中,重構已經成爲了一個很是重要的術語,而且最近被看成是對當前工做代碼作任何改變的代名詞。雖然重構確實涉及對當前正在工做的代碼的更改,但並不是整個大局。Martin Fowler在他關於這個話題的重要着做——《Refactoring》一書中將重構定義爲:

  • 對軟件的內部結構進行更改,使其更容易理解而且修改起來更便宜,而不改變其可觀察的行爲。

這個定義的關鍵在於它涉及的更改不會改變系統可觀察的行爲。這意味着當咱們重構代碼時,咱們必需要有方法來確保代碼的外部可見行爲不會改變。在咱們的例子中,這意味着是在咱們繼承或本身開發的測試套件中。爲了確保咱們沒有改變系統的外部行爲,每當咱們進行改變時,都必須從新編譯和執行咱們的所有測試。

此外,並非咱們所作的每個改變都被認爲是重構。例如,重命名方法以更好地反映其預期用途是重構,但添加新功能不是。爲了看到重構的好處,咱們將重構SuccessfulFilter。執行的第一個重構是提取方法,以更好地封裝我的淨工資的邏輯:

 
  1. public class SuccessfulFilter implements Predicate<Person> { 
  2.     @Override 
  3.     public boolean test(Person person) { 
  4.         return person.getAge() < 30 && getNetSalary(person) > 60000; 
  5.     } 
  6.     private double getNetSalary(Person person) { 
  7.         return (((person.getSalary() - (250 * 12)) - 1500) * 0.94); 
  8.     } 

在咱們進行這種改變以後,咱們從新編譯並運行咱們的測試套件,測試套件將繼續經過。如今更容易看出,成功是經過一我的的年齡和淨薪酬定義的,可是getNetSalary方法彷佛並不像Person類同樣屬於SuccessfulFilter(指示標誌就是該方法的惟一參數是Person,該方法的惟一調用是Person類的方法,所以對Person類有很強的親和力)。 爲了更好地定位這個方法,咱們執行一個Move方法將其移動到Person類:

 
  1. public class Person { 
  2.     private int age; 
  3.     private double salary; 
  4.     public Person(int age, double salary) { 
  5.         this.age = age; 
  6.         this.salary = salary; 
  7.     } 
  8.     public void setAge(int age) { 
  9.         this.age = age; 
  10.     } 
  11.     public int getAge() { 
  12.         return age; 
  13.     } 
  14.     public void setSalary(double salary) { 
  15.         this.salary = salary; 
  16.     } 
  17.     public double getSalary() { 
  18.         return salary; 
  19.     } 
  20.     public double getNetSalary() { 
  21.         return ((getSalary() - (250 * 12)) - 1500) * 0.94; 
  22.     } 
  23. public class SuccessfulFilter implements Predicate<Person> { 
  24.     @Override 
  25.     public boolean test(Person person) { 
  26.         return person.getAge() < 30 && person.getNetSalary() > 60000; 
  27.     } 

爲了進一步清理此代碼,咱們對每一個magic number執行符號常量替換magic number行爲。爲了知道這些值的含義,咱們可能得和原做者交流,或者向具備足夠領域知識的人請教,以引領正確的方向。咱們還將執行更多的提取方法重構,以確保現有的方法儘量簡單。

 
  1. public class Person { 
  2.     private static final int MONTHLY_BONUS = 250; 
  3.     private static final int YEARLY_BONUS = MONTHLY_BONUS * 12; 
  4.     private static final int YEARLY_BENEFITS_DEDUCTIONS = 1500; 
  5.     private static final double YEARLY_401K_CONTRIBUTION_PERCENT = 0.06; 
  6.     private static final double YEARLY_401K_CONTRIBUTION_MUTLIPLIER = 1 - YEARLY_401K_CONTRIBUTION_PERCENT; 
  7.     private int age; 
  8.     private double salary; 
  9.     public Person(int age, double salary) { 
  10.         this.age = age; 
  11.         this.salary = salary; 
  12.     } 
  13.     public void setAge(int age) { 
  14.         this.age = age; 
  15.     } 
  16.     public int getAge() { 
  17.         return age; 
  18.     } 
  19.     public void setSalary(double salary) { 
  20.         this.salary = salary; 
  21.     } 
  22.     public double getSalary() { 
  23.         return salary; 
  24.     } 
  25.     public double getNetSalary() { 
  26.         return getPostDeductionSalary(); 
  27.     } 
  28.     private double getPostDeductionSalary() { 
  29.         return getPostBenefitsSalary() * YEARLY_401K_CONTRIBUTION_MUTLIPLIER; 
  30.     } 
  31.     private double getPostBenefitsSalary() { 
  32.         return getSalary() - YEARLY_BONUS - YEARLY_BENEFITS_DEDUCTIONS; 
  33.     } 
  34. public class SuccessfulFilter implements Predicate<Person> { 
  35.     private static final int THRESHOLD_AGE = 30; 
  36.     private static final double THRESHOLD_SALARY = 60000.0; 
  37.     @Override 
  38.     public boolean test(Person person) { 
  39.         return person.getAge() < THRESHOLD_AGE && person.getNetSalary() > THRESHOLD_SALARY; 
  40.     } 

從新編譯和測試,發現系統仍然按照預期的方式工做:咱們沒有改變外部行爲,可是咱們改進了代碼的可靠性和內部結構。有關更復雜的重構和重構過程,請參閱Martin Fowler的Refactoring Guru網站。

5.當你離開的時候,代碼比你發現它的時候更好

最後這個技術在概念上很是簡單,但在實踐中很困難:讓代碼比你發現它的時候更好。當咱們梳理代碼,特別是別人的代碼時,咱們大多會添加功能,測試它,而後前行,不關心咱們會不會貢獻軟件腐爛,也不在意咱們添加到類的新方法會不會致使額外的混亂。所以,本文的所有內容可總結爲如下規則:

  • 每當咱們修改代碼時,請確保當你離開的時候,代碼比你發現它的時候更好。

前面提到過,咱們須要對類形成的損壞和對改變的代碼負責,若是它不能工做,那麼修復是咱們的職責。爲了打敗伴隨軟件生產而出現的熵,咱們必須強制本身作到離開時的代碼比咱們發現它的時候更佳。爲了避免逃避這個問題,咱們必須償還技術債務,確保下一個接觸代碼的人不須要再付出代價。說不定,未來多是咱們本身感謝本身這個時候的堅持呢。

 

 

原文鏈接http://click.aliyun.com/m/33936/

相關文章
相關標籤/搜索