1.確保測試的存在
要想確保在其他開發人員編寫的代碼中所存在的現有功能實際能夠按照預期的方式工作, 並且我們對其進行的任何更改都不會影響到功能的實現,
創建新測試
這是一個悲傷的例子:我們在改變其他開發人員的代碼時, 要對更改結果負責, 但是我們沒有辦法保證我們在進行更改時不破壞任何東西。 抱怨是沒有用的。 無論我們發現代碼處在什麼樣的條件下, 我們總歸是要接觸代碼,
雖然這是乏味的, 但它允許我們通過編寫測試來學習, 這是它的主要優點。 假設代碼現在可以正常工作, 而我們需要編寫測試, 以便預期的輸入會導致預期的輸出。 在我們完成這個測試的過程中, 我們逐漸瞭解到代碼的意圖和功能。 例如, 給出以下代碼
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我們對代碼的意圖以及為什麼在代碼中使用Magic number知道得並不多, 但是我們可以創建一組測試, 已知輸入產生已知輸出。 例如, 通過做一些簡單的數學和解決構成成功的閾值薪水問題, 我們發現如果一個人的年齡在30歲以下,
通過這三個測試, 我們現在對現有代碼的工作方式有了大致的瞭解:如果一個人不到30歲, 且每年賺$ 68,300, 那麼他被認為是成功人士。 雖然我們可以創建更多的測試來確保臨界情況(例如空白年齡或工資)功能正常, 但是一些簡短的測試不僅使我們瞭解了原始功能, 還給出了一套自動化測試, 可用於確保在對現有代碼進行更改時, 我們不會破壞現有功能。
使用現有測試
如果有足夠的代碼測試元件,
雖然現有的測試可以提供説明, 但我們仍然需要對此持保留態度。 測試是否與代碼的開發更改一起與時俱進是很難說的。 如果是的話, 那麼這是一個很好的理解基礎;如果不是, 那麼我們要小心不要被誤導。 例如, 如果初始的工資閾值是每年75,000美元, 而後來更改為我們的68,330美元, 那麼下面這個過時的測試可能會使我們誤入歧途:
這個測試還是會通過的, 但沒有了預期的作用。 通過的原因不是因為它正好是閾值, 而是因為它超出了閾值。 如果此測試元件包含這樣一個測試用例:當薪水低於閾值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在我們進行這種改變之後,我們重新編譯並運行我們的測試套件,測試套件將繼續通過。現在更容易看出,成功是通過一個人的年齡和淨薪酬定義的,但是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為了進一步清理此代碼,我們對每個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重新編譯和測試,發現系統仍然按照預期的方式工作:我們沒有改變外部行為,但是我們改進了代碼的可靠性和內部結構。有關更複雜的重構和重構過程,請參閱Martin Fowler的Refactoring Guru網站。
5.當你離開的時候,代碼比你發現它的時候更好
最後這個技術在概念上非常簡單,但在實踐中很困難:讓代碼比你發現它的時候更好。當我們梳理代碼,特別是別人的代碼時,我們大多會添加功能,測試它,然後前行,不關心我們會不會貢獻軟體腐爛,也不在乎我們添加到類的新方法會不會導致額外的混亂。因此,本文的全部內容可總結為以下規則:
每當我們修改代碼時,請確保當你離開的時候,代碼比你發現它的時候更好。
前面提到過,我們需要對類造成的損壞和對改變的代碼負責,如果它不能工作,那麼修復是我們的職責。為了戰勝伴隨軟體生產而出現的熵,我們必須強制自己做到離開時的代碼比我們發現它的時候更佳。為了不逃避這個問題,我們必須償還技術債務,確保下一個接觸代碼的人不需要再付出代價。說不定,將來可能是我們自己感謝自己這個時候的堅持呢。
因為它們也許仍然能為我們提供關於原作者(或最近撰寫測試的開發人員)意圖的一些文檔,但它們可能包含過時或不正確的資料。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在我們進行這種改變之後,我們重新編譯並運行我們的測試套件,測試套件將繼續通過。現在更容易看出,成功是通過一個人的年齡和淨薪酬定義的,但是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為了進一步清理此代碼,我們對每個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重新編譯和測試,發現系統仍然按照預期的方式工作:我們沒有改變外部行為,但是我們改進了代碼的可靠性和內部結構。有關更複雜的重構和重構過程,請參閱Martin Fowler的Refactoring Guru網站。
5.當你離開的時候,代碼比你發現它的時候更好
最後這個技術在概念上非常簡單,但在實踐中很困難:讓代碼比你發現它的時候更好。當我們梳理代碼,特別是別人的代碼時,我們大多會添加功能,測試它,然後前行,不關心我們會不會貢獻軟體腐爛,也不在乎我們添加到類的新方法會不會導致額外的混亂。因此,本文的全部內容可總結為以下規則:
每當我們修改代碼時,請確保當你離開的時候,代碼比你發現它的時候更好。
前面提到過,我們需要對類造成的損壞和對改變的代碼負責,如果它不能工作,那麼修復是我們的職責。為了戰勝伴隨軟體生產而出現的熵,我們必須強制自己做到離開時的代碼比我們發現它的時候更佳。為了不逃避這個問題,我們必須償還技術債務,確保下一個接觸代碼的人不需要再付出代價。說不定,將來可能是我們自己感謝自己這個時候的堅持呢。