如何處理前任程序員留下的代碼
作為軟體工程師不可避免會遇到的一個場景是:我們在改變或添加一個功能到不是我們創建的、我們不熟悉的、與我們負責的系統部分無關的代碼中時,會遇到麻煩。雖然這可能會是一個繁瑣而艱巨的任務,但是由於使用其他開發人員編寫的代碼有很大的靈活性,所以我們可以從中得到大大的好處,包括增加我們的影響範圍,修復軟體腐爛以及學習我們以前不了解的系統部分(更何況,還可以學習其他程序員的技術和技巧)。
考慮到使用其他開發人員編寫的代碼既有其厭煩之處,又有其優勢所在,所以我們必須小心不要犯一些嚴重的錯誤:
我們的自我意識
:我們可能會覺得自己知道得最多,但通常事實並非如此。我們要更改的是我們知之甚少的代碼——我們不知道原作者的意圖、導致此代碼的決策以及原作者在寫代碼時可用的工具和框架,等等。謙遜的品質價值千金,你值得擁有。原作者的自我意識
:我們即將接觸的代碼是由另一個開發人員所編寫的,另一種風格、約束、期限和個人生活(消耗他或她工作之外的時間)。只有當我們開始質疑他或她做出的決定或質疑代碼為什麼這麼不幹凈的時候,那人才會自我反省,不至於夜郎自大。我們應該盡一切努力讓原作者幫助我們工作,而不是妨礙我們。對未知的恐懼
:很多時候,我們將要接觸的代碼是我們知之甚少或完全一無所知的。令人害怕的是:我們將對我們所做的任何改變負責,但是我們基本上就像是在沒有光線的黑暗屋子裡走動一樣。其實我們不需要擔心,而是應該構建一種使我們能夠在大小不一的改變中感到舒適的結構,並允許我們確保沒有破壞現有的功能。
由於開發人員,包括我們自己,是人,所以在處理其他開發人員編寫的代碼時,處理好很多人的天性問題是很有用的。在這篇文章中,我們將通過我們可以使用的五種技術來確保將對人性的理解成為我們的優勢,從現有代碼和原作者汲取儘可能多的幫助,並使得其他開發人員編寫的代碼最後變得比原來更優秀。雖然這裡列出的5個方法並不全面,但是使用下面的技術將確保在結束改動其他開發人員編寫的代碼時,我們有信心保持現有功能的工作狀態,同時確保我們的新功能與現有的代碼庫協調一致。
1.確保測試的存在
要想確保在其他開發人員編寫的代碼中所存在的現有功能實際能夠按照預期的方式工作,並且我們對其進行的任何更改都不會影響到功能的實現,唯一真正令人信心十足的方式是用測試來支持代碼。當我們遇到另一位開發人員編寫的代碼時,代碼有兩種所處的狀態:(1)沒有足夠的測試水平,或(2)有足夠的測試水平。遇到前一種情況,我們得負責創建測試,而在後一種情況下,我們可以使用現有的測試來確保我們做出的任何更改都不會破壞代碼,並儘可能多地從測試去了解代碼的意圖。
創建新測試
這是一個悲傷的例子:我們在改變其他開發人員的代碼時,要對更改結果負責,但是我們沒有辦法保證我們在進行更改時不破壞任何東西。抱怨是沒有用的。無論我們發現代碼處在什麼樣的條件下,我們總歸是要接觸代碼,因此如果代碼壞掉了,就是我們的責任。所以我們在改變代碼時,一定要掌控自己的行為。確定不會破壞代碼的唯一方法是自己寫測試。
雖然這是乏味的,但它允許我們通過編寫測試來學習,這是它的主要優點。假設代碼現在可以正常工作,而我們需要編寫測試,以便預期的輸入會導致預期的輸出。在我們完成這個測試的過程中,我們逐漸了解到代碼的意圖和功能。例如,給出以下代碼
public class Person {
private int age;
private double salary;
public Person(int age, double salary) {
this.age = age;
this.salary = salary;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
public class SuccessfulFilter implements Predicate<Person> {
@Override
public boolean test(Person person) {
return person.getAge() < 30 &&
((((person.getSalary() - (250 * 12)) - 1500) * 0.94) > 60000);
}
}
我們對代碼的意圖以及為什麼在代碼中使用Magic number知道得並不多,但是我們可以創建一組測試,已知輸入產生已知輸出。例如,通過做一些簡單的數學和解決構成成功的閾值薪水問題,我們發現如果一個人的年齡在30歲以下,且每年大概賺68,330美元,那麼他被認為是成功的(按照本規範的標準)。雖然我們不知道那些magic number是什麼,但是我們知道它們確實減少了初始的薪水值。因此,68,330美元的閾值是扣除前的基本工資。通過使用這些信息,我們可以創建一些簡單的測試,例如:
public class SuccessfulFilterTest {
private static final double THRESHOLD_NET_SALARY = 68330.0;
@Test
public void under30AndNettingThresholdEnsureSuccessful() {
Person person = new Person(29, THRESHOLD_NET_SALARY);
Assert.assertTrue(new SuccessfulFilter().test(person));
}
@Test
public void exactly30AndNettingThresholdEnsureUnsuccessful() {
Person person = new Person(30, THRESHOLD_NET_SALARY);
Assert.assertFalse(new SuccessfulFilter().test(person));
}
@Test
public void under30AndNettingLessThanThresholdEnsureSuccessful() {
Person person = new Person(29, THRESHOLD_NET_SALARY - 1);
Assert.assertFalse(new SuccessfulFilter().test(person));
}
}
通過這三個測試,我們現在對現有代碼的工作方式有了大致的了解:如果一個人不到30歲,且每年賺$ 68,300,那麼他被認為是成功人士。雖然我們可以創建更多的測試來確保臨界情況(例如空白年齡或工資)功能正常,但是一些簡短的測試不僅使我們了解了原始功能,還給出了一套自動化測試,可用於確保在對現有代碼進行更改時,我們不會破壞現有功能。
使用現有測試
如果有足夠的代碼測試組件,那麼我們可以從測試中學到很多東西。正如我們創建測試一樣,通過閱讀測試,我們可以了解代碼如何在功能層面上工作。此外,我們還可以知道原作者是如何讓代碼運行的。即使測試是由原作者以外的人(在我們接觸之前)撰寫的,也依然能夠為我們提供關於其他人對代碼的看法。
雖然現有的測試可以提供幫助,但我們仍然需要對此持保留態度。測試是否與代碼的開發更改一起與時俱進是很難說的。如果是的話,那麼這是一個很好的理解基礎;如果不是,那麼我們要小心不要被誤導。例如,如果初始的工資閾值是每年75,000美元,而後來更改為我們的68,330美元,那麼下面這個過時的測試可能會使我們誤入歧途:
@Test
public void under30AndNettingThresholdEnsureSuccessful() {
Person person = new Person(29, 75000.0);
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。執行的第一個重構是提取方法,以更好地封裝個人凈工資的邏輯:
public class SuccessfulFilter implements Predicate<Person> {
@Override
public boolean test(Person person) {
return person.getAge() < 30 && getNetSalary(person) > 60000;
}
private double getNetSalary(Person person) {
return (((person.getSalary() - (250 * 12)) - 1500) * 0.94);
}
}
在我們進行這種改變之後,我們重新編譯並運行我們的測試套件,測試套件將繼續通過。現在更容易看出,成功是通過一個人的年齡和凈薪酬定義的,但是getNetSalary方法似乎並不像Person類一樣屬於SuccessfulFilter(指示標誌就是該方法的唯一參數是Person,該方法的唯一調用是Person類的方法,因此對Person類有很強的親和力)。 為了更好地定位這個方法,我們執行一個Move方法將其移動到Person類:
public class Person {
private int age;
private double salary;
public Person(int age, double salary) {
this.age = age;
this.salary = salary;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
public double getNetSalary() {
return ((getSalary() - (250 * 12)) - 1500) * 0.94;
}
}
public class SuccessfulFilter implements Predicate<Person> {
@Override
public boolean test(Person person) {
return person.getAge() < 30 && person.getNetSalary() > 60000;
}
}
為了進一步清理此代碼,我們對每個magic number執行符號常量替換magic number行為。為了知道這些值的含義,我們可能得和原作者交流,或者向具有足夠領域知識的人請教,以引領正確的方向。我們還將執行更多的提取方法重構,以確保現有的方法儘可能簡單。
public class Person {
private static final int MONTHLY_BONUS = 250;
private static final int YEARLY_BONUS = MONTHLY_BONUS * 12;
private static final int YEARLY_BENEFITS_DEDUCTIONS = 1500;
private static final double YEARLY_401K_CONTRIBUTION_PERCENT = 0.06;
private static final double YEARLY_401K_CONTRIBUTION_MUTLIPLIER = 1 - YEARLY_401K_CONTRIBUTION_PERCENT;
private int age;
private double salary;
public Person(int age, double salary) {
this.age = age;
this.salary = salary;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
public double getNetSalary() {
return getPostDeductionSalary();
}
private double getPostDeductionSalary() {
return getPostBenefitsSalary() * YEARLY_401K_CONTRIBUTION_MUTLIPLIER;
}
private double getPostBenefitsSalary() {
return getSalary() - YEARLY_BONUS - YEARLY_BENEFITS_DEDUCTIONS;
}
}
public class SuccessfulFilter implements Predicate<Person> {
private static final int THRESHOLD_AGE = 30;
private static final double THRESHOLD_SALARY = 60000.0;
@Override
public boolean test(Person person) {
return person.getAge() < THRESHOLD_AGE && person.getNetSalary() > THRESHOLD_SALARY;
}
}
重新編譯和測試,發現系統仍然按照預期的方式工作:我們沒有改變外部行為,但是我們改進了代碼的可靠性和內部結構。有關更複雜的重構和重構過程,請參閱Martin Fowler的Refactoring Guru網站。
5.當你離開的時候,代碼比你發現它的時候更好
最後這個技術在概念上非常簡單,但在實踐中很困難:讓代碼比你發現它的時候更好。當我們梳理代碼,特別是別人的代碼時,我們大多會添加功能,測試它,然後前行,不關心我們會不會貢獻軟體腐爛,也不在乎我們添加到類的新方法會不會導致額外的混亂。因此,本文的全部內容可總結為以下規則:
每當我們修改代碼時,請確保當你離開的時候,代碼比你發現它的時候更好。
前面提到過,我們需要對類造成的損壞和對改變的代碼負責,如果它不能工作,那麼修復是我們的職責。為了戰勝伴隨軟體生產而出現的熵,我們必須強制自己做到離開時的代碼比我們發現它的時候更佳。為了不逃避這個問題,我們必須償還技術債務,確保下一個接觸代碼的人不需要再付出代價。說不定,將來可能是我們自己感謝自己這個時候的堅持呢。
※Butter Knife註解框架的精細點滴
※ajax跨域問題(三種解決方案)
TAG:程序員小新人學習 |