1. 程式人生 > >如何處理前任程式設計師留下的程式碼

如何處理前任程式設計師留下的程式碼

身為一個軟體工程師,我們不可避免的會遇到這樣一些問題:不得不修改別人的程式碼,或者在別人的程式碼中新增新的功能。我們並不熟悉這些程式碼,它也可能在整個系統中與我們編寫的部分無關。雖然這樣的工作很困難,容易讓人感到無奈,但是要達到足夠的靈活性來也別的開發者一起編寫程式碼,收穫也蠻大的。這些收穫包括提高影響力,修復爛軟體,還能學到系統中以前並不瞭解的部分(還可以從其它程式設計師那裡學到技術和技巧)。

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

  • 我們的自我意識:我們可能會認為自己最有能耐,但通常都不是。我們對要改變的程式碼知之甚少,不瞭解原作者的意圖,也不瞭解多少年有哪些因素導致這些程式碼形成,以及作者在編寫這些程式碼的時候使用了什麼樣的工具和框架。謙卑價值萬金,我們應該時刻保持這種心態。
  • 原作者的自我意識:我們要接觸的程式碼來自另一個開發者,他/她有自己的網路、約束、最後期限等,當然也有他/她自己的生活(這會佔用一點工作時間)。他/她也是一個人,當我們質疑他/她做出的決定,或者質問為什麼程式碼這麼糟糕的時候,他/她會自然地產生防禦性心理。我們應該努力讓原作者與我們合作,而不是成為我們工作的阻礙
  • 恐懼未知事物:我們很多時候會接觸到只瞭解一點點甚至完全不瞭解的程式碼。這似乎是件可怕的事情:我們得對自己做出的改動負責,但我們就像是在一個沒有光亮的黑屋子裡走來走去。我們不需要害怕,而是應該建立起一個框架,可以在裡面安心地進行大大小小的修改,同時確保我們不會破壞現有的功能。

所有開發人員,包括我們自己,都是人。因此在別人編寫的程式碼上工作,會受到人性的影響。在本文中,我們會講述五種方法,利用人性的優點,從現有程式碼和原作者身上取得儘可能多的收穫,並改善程式碼既有的狀態。雖然這個清單並不全面,但應用這些方法將確保我們在完成對別人程式碼的修改工作後,會有信心保持現有功能的工作狀態,同時又能保證新功能融合在現有程式碼中。

1. 確保有測試

對於別的開發人員寫出來的功能,它確實如預期一樣工作嗎?我們所做的修改是否會妨礙它按照預期工作?對此,唯一能讓人產生信心完成前述問題的方式就是,用測試來支援程式碼。我們在閱讀別人的程式碼時,會發現兩種可能的狀態:(1) 沒有達到足夠水平的測試,或者 (2) 有達到足夠水平的測試。對於前者,我們會陷入建立測試的困境;而對於後者,我們可以使用現有的測試來確保我們所做的修改不會破解原來的程式碼,同時也能從測試中大量地瞭解到程式碼的意圖。

建立新測試

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

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

1234567891011121314151617181920212223242526272829303132333435 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> {    <ahref='http://www.jobbole.com/members/wx610506454'>@Override</a>    public boolean test(Person person) {        return person.getAge() <30&&((((person.getSalary()-(250*12))-1500)*0.94)> 60000);    }}

我們對其功能和程式碼中使用的魔法數字[譯者注:指直接的數字常量]並不瞭解,但我們可以建立一組測試,根據已知的輸入產生已知的輸出。比如,通過簡單的數學運算分析成功人士的薪資。我們發現如果 30 歲以下的人每年掙大約 $68,330,就會被認為是成功的(按程式碼中的標準)。雖然我們不知道那些魔法數字是什麼意思,但我們知道它們會減少原始薪資。這樣,$68,330 這個閾值是扣除前的基本薪資。使用這些資訊,我們可以建立一些簡單的測試,如下:

12345678910111213141516171819202122 public class SuccessfulFilterTest {    private static final double THRESHOLD_NET_SALARY = 68330.0;    <ahref='http://www.jobbole.com/members/q1531454480'>@Test</a>    public void under30AndNettingThresholdEnsureSuccessful() {        Person person = new Person(29, THRESHOLD_NET_SALARY);        Assert.assertTrue(new SuccessfulFilter().test(person));    }    <ahref='http://www.jobbole.com/members/q1531454480'>@Test</a>    public void exactly30AndNettingThresholdEnsureUnsuccessful() {        Person person = new Person(30, THRESHOLD_NET_SALARY);        Assert.assertFalse(new SuccessfulFilter().test(person));    }    <ahref='http://www.jobbole.com/members/q1531454480'>@Test</a>    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,那麼這個過時的測試可能會把我們引入歧途:

12345 <ahref='http://www.jobbole.com/members/q1531454480'>@Test</a>public void under30AndNettingThresholdEnsureSuccessful() {    Person person = new Person(29, 75000.0);    Assert.assertTrue(new SuccessfulFilter().test(person));}

這個測試仍然會通過,但不是預期的效果。它能通過不是因為正確的閾值,而是因為它超過了閾值。如果這個測試集中包括一個測試用例,其薪資只比閾值少 $1 時返回 false,那麼第二個測試會失敗,這表示閾值是錯誤的。如果套件沒有這樣的測試,那麼舊的資料很容易對我們瞭解程式碼的實際意圖產生誤導。當存在疑問的時候,請相信程式碼:正如我們前端所展示的,解決閾值的問題表明測試並未針對實際的閾值。

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

2. 和編寫程式碼的人談談

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

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

  • 這段程式碼對應於系統藍圖的哪個部分?
  • 你有沒有相關的設計方案或圖表?
  • 有我需要注意的坑嗎?
  • 某個元件或類是做什麼用的?
  • 有沒有你本想寫進程式碼,當時卻沒有寫的東西?為什麼?

保持謙卑,從原作者那裡尋找答案。幾乎每個開發者都出現過這樣的場景,他/她在那裡看著別人的程式碼,問自己“他/她為什麼要那樣做?他們為什麼不這麼做?”然後花幾個小時來得出本來只要原作者回答就能得到的結論。多數開發者都有能幹的程式設計師,所以最好是假設我們看似糟糕的決定背後有個合理的理由(也可能沒有,但在看別人程式碼的時候最好假設他有不錯的理由;如果確實沒有,我們可以通過重構來修改)。

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

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

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

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

3. 幹掉所有警告

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

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

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

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

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

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

  1. 它迫使我們對我們所建立的任何程式碼保持慎重。
  2. 它減少了程式碼腐爛的改動,這樣警告會導致以後的錯誤。

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

4. 重構

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

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

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

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

1234567891011 public class SuccessfulFilter implements Predicate<Person> {    <ahref='http://www.jobbole.com/members/wx610506454'>@Override</a>    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 方法似乎並不屬於 SuccessfulFilter,它應該是 Person 類(這樣說是因為這個方法的唯一引數是 Person 物件,也只調用了 Person 的方法,所以它更接近 Person)。為了更好的放置這個方法,我們使用移動方法將它移動到 Person 類。

1234567891011121314151617181920212223242526272829303132333435363738 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> {    <ahref='http://www.jobbole.com/members/wx610506454'>@Override</a>    public boolean test(Person person) {        return person.getAge() <30&&person.getNetSalary()> 60000;    }}

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