優秀的程式設計師如何清晰表達程式碼的意圖

edithfang發表於2015-01-23
當讀者用所喜愛的IDE來工作,並用到了其中各種吸引人的附加功能(語法檢查、自動完成、靜態分析和其他功能)時,會不會感嘆缺少一個尚未被髮明的特定功能?對,筆者指的就是意圖檢查器。對此大家都很瞭解。當我們在思考時,就會需要這樣的功能:“我希望它能按照我的意思而不是按照我敲的東西來程式設計!”或許在奮力編寫一個棘手的演算法時,就會想要這個功能。可能在呼叫了這個功能後,就發現了一個愚蠢的敲錯一個字元這樣的bug。不管在什麼情況下,所面臨的就是將意圖轉化成實現的複雜性。

另一方面,大家以前都問過像這樣的問題:“這段程式碼是做什麼的?”或者更極端地問:“這個開發人員當時是怎麼想的?”測試所做的所有事情,就是要驗證實現與顯性或隱性的開發意圖之間的匹配性:顯性表現在程式碼應該要完成一些目標;隱性表現在程式碼在完成上述目標時,也應該具有特定的可用性、強壯性等這些特徵,而不管這些是否被特別地考慮過[ 重要的是要考慮到,上述意圖會發生在軟體開發過程的許多層面上。開發人員將使用者所想要的,轉化為前者所認為後者所想要的,哪些功能應該被新增以解決後者的需要,如何將這些特性對映到現有或期望的應用上,如何將這些納入系統的架構和設計之中,以及如何編寫程式碼。本章所討論的許多觀點,都可以外推到其他層面上。]。

意圖都被放到哪裡去了

意圖是一個捉摸不定的東西。在更廣闊的社會裡,存在著“意圖式生活”(intentional living)這個詞。在實踐意圖式生活時,會試圖把所做的每一個行為當作是刻意的,而不是當作習慣性的或偶然的,同時要考慮到行為發生的地點和在這些行為的背景下會出現的後果。意圖的明確性也常常會與極簡主義的那些方法聯絡起來。上述道理雖然顯而易見,但是要實踐這種生活,需要用更多的紀律和努力。

軟體需要同樣的專注力。筆者在參與開發了多個有關人身安全方面的專案和產品之後,對於所寫程式碼的後果變得很敏感。這些後果可以在某種程度上擴充套件到整個軟體。如果電子郵件程式將郵件傳送給了錯誤的收件人,那麼就會違反保密性、破壞信任,有時還會帶來重大的金融和政治影響。一個字處理程式的惱人崩潰似乎微不足道,特別是在沒有造成資料丟失的情況下,但如果這種情況出現幾百萬次,那麼它就會演變成大量的刺激,喪失生產力。

極簡主義也適用於軟體。程式碼編寫得越少,要維護的內容也就越少。要維護的內容越少,必須要理解的內容也就越少。必須要理解的內容越少,犯錯誤的機會也就越少。更少的程式碼成就更少的bug。

筆者有意囊括了超乎安全性、金錢和生產力之外的因素。漸漸地,軟體被整合到了周圍一切的事物中,包括被整合到伴隨日常生活的各種裝置中。這種無處不在使得軟體對我們的生活質量和精神狀態所施加的影響不斷增大。所以上文中包含了像“信任”和“刺激”這樣描述影響的字眼。產品的意圖包含了非功能性的方面。那些取得巨大成功的公司,不僅擷取了使用者的頭腦,也俘獲了他們的內心[ 若想深入瞭解這個話題,可以參考Wiley出版社1995年出版的Alan Cooper的著作《互動設計精髓》(About Face)及其後續版本。]。

將意圖與實現分離

實現僅僅是完成意圖的眾多方式中的一種而已。如果一個人能夠對實現與意圖之間的邊界有一個清楚的認識,那麼他在編寫和測試軟體時就能有一個更好的心智模式(mental model)。實現與意圖極其相像的情況屢見不鮮。例如,一個“獎勵獲勝玩家”的意圖可以實現為“查詢獲勝使用者,遍歷他們,然後為每人的成績清單中新增一個徽章”。實現的語言與意圖的表述緊密對應,但這兩者並不相同。

明確地劃分意圖與實現之間的界限,能有助於將測試工作的規模與軟體相匹配。在不摻雜實現元素的前提下,對意圖測試得越多,測試耦合到程式碼的情況就越少。耦合得少了,就不用被迫隨著實現中的變化更新或重寫測試了。測試改變得越少,花費在測試上的精力也就越少,而這會增加測試保持正確的可能性。所有這一切都會使得在驗證、維護和擴充套件軟體上的花費更少,在長遠來看更是如此。
也可以將程式碼的意圖與功能相分離。此處所說的分離是將程式碼原本的工作用意與它實際的行為分隔開。當需要測試實現時,應該對程式碼本應做的事情進行測試。而對程式碼所編寫出的那些行為進行測試時,若該程式碼編寫得不正確,那麼這種測試就會造成一個安全的假象。一個執行通過的測試會告訴我們一些有關程式碼質量和程式碼與目的之間契合度的資訊。而一個不應執行成功的測試若執行通過了,那麼該測試就會在上述資訊上對我們撒謊。

當編寫程式碼時,要使用程式語言和框架中的特性來最清晰地表達意圖。在Java語言中將變數宣告為final或private,在C++中宣告為const,在Perl中宣告為my,或者在JavaScript中宣告為var,都是表達了有關該變數用途的意圖。在像Perl和JavaScript這樣帶有弱引數需求的動態語言中,在Perl中傳遞雜湊引數[PBP]和在JavaScript中傳遞物件引數[JTGP]時,引數值本身的命名能用於在程式碼內部更加清晰地記錄意圖。

一個能引發思考的簡單例子

讓我們看看一個使用Java語言的例子。程式碼清單2-1顯示了一個簡單的Java類,該類帶有幾條線索,展示了在構造該類時所表現出來的意圖。考慮ScoreWatcher類,它是一個跟蹤體育比賽分數系統的一部分。它封裝了從一個新聞源(a news feed)獲得比賽分數的功能。

程式碼清單2-1 一個簡單的Java類展示了帶有意圖的構造

class ScoreWatcher {
  private final NewsFeed feed;
  private int pollFrequency; // Seconds
  public ScoreWatcher(NewsFeed feed, int pollFrequency) {
    this.feed = feed;
    this.pollFrequency = pollFrequency;
  }
  public void setPollFrequency(int pollFrequency) {
    this.pollFrequency = pollFrequency;
  }
  public int getPollFrequency() {
    return pollFrequency;
  }
  public NewsFeed getFeed() {
    return feed;
  }
  ...
}


首先,看一下該類所定義的那些屬性(attribute)。編寫該類的作者把feed定義為final[ 在Java語言中,final關鍵字本身就表明了,由它所宣告的變數一旦初始化,其值就不能再改變了。]的屬性,卻沒有把pollFrequency也定義為final。這告訴了我們什麼?它表達了這樣一個意圖:feed應該只能在類構建時被賦值一次,但pollFrequency能夠在該物件的整個生命週期中被修改。接下來,在程式碼中所看到的pollFrequency同時具備getter和setter,而feed僅有一個getter,又強化了這一點。

但這僅僅讓我們瞭解了實現上的意圖。上面的做法可能會支援哪個功能性的意圖呢?可以根據程式碼的上下文做出一個合理的結論,即對於每一個能夠使用的新聞源,應該只恰好分配一個類來封裝它。還可以繼續推論,或許對於每一個要被監測的比賽分數,也應該恰好只存在一個用來初始化ScoreWatcher的NewsFeed。還可以繼續推測,如果存在多個新聞源,那麼多個源的管理可能會隱藏在一個新聞源的介面後。這一點需要驗證,但是在目前的情況下看起來是合理的。

然而,或許是由於Java語言在表達能力上的限制,上述假設有一個弱點。即便不知道NewsFeed類的構造情況,我們也能推測出:即使feed這個引用本身不能被改變,但還是有可能通過它來修改它所引用的物件。在C++語言中,可以這樣宣告屬性:

const NewsFeed * const feed;


這個宣告不僅表達了指標不能被改變,而且還表達了不能使用指標來改變它所指向的物件。這在C++語言中提供了一個額外的上下文不變性(contextual immutability)的標記,而這一點在Java語言中並不存在。在Java語言中,想讓一個類的所有例項都不可變(immutable)還是比較容易的。但是想讓一個特定的引用所引用的物件不可變,就需要花費相當多的努力了,或許需要建立一個處理不可變性的代理來封裝該物件例項。

然而,這些又是如何改變測試的呢?類的構造——實現——清楚地規定了賦給那個類的feed在該類的整個宣告週期中不應改變。這是意圖嗎?讓我們看看如程式碼清單2-2所示的驗證這個假設的測試。
程式碼清單2-2 驗證程式碼清單2-1中的新聞源不會改變的測試

class TestScoreWatcher {
  @Test
  public void testScoreWatcher_SameFeed() {
    // Set up
    NewsFeed expectedFeed = new NewsFeed();
    int expectedPollFrequency = 70;

    // Execute SUT[ SUT是Software Under Test(被測軟體)的縮寫。[xTP]]
    ScoreWatcher sut = new ScoreWatcher(expectedFeed,
    expectedPollFrequency);

    // Verify results
    Assert.assertNotNull(sut); // Guard assertion
    Assert.assertSame(expectedFeed, sut.getFeed());

    // Garbage collected tear down
  }
}


在JUnit中,assertSame斷言驗證的是,期望的引用和實際的引用都指向同樣的物件。回到有關該類的意圖的推測上,假設引用到同樣的feed很重要這一點是合理的,但是同樣的NewsFeed這一點是不是在這種情況下有些超出規格所規定的範圍?例如,要是程式碼的實現為了選擇加強初始新聞源的不變性,從getter將其返回之前就克隆其屬性,從而確保任何變化都不會影響ScoreWatcher的NewsFeed的內部狀態,那該怎麼辦?在這種情況下,測試構造器的引數是相同的這一點就不正確了。這種設計的意圖,更有可能需要驗證feed的深度相等性[ 驗證兩個物件的深度相等性,即驗證這兩個物件內部所儲存的各個資料的值都一一相等。——譯者注](deep equality)。

本文來自《優質程式碼:軟體測試的原則、實踐與模式》



本書專門從軟體開發人員和技術人員關注的程式碼質量的角度來講軟體測試的原理、實踐和模式。作者有20多年軟體開發經驗,10多年軟體測試技術的教授經驗。書中積累了來自大量高水準軟體工程師的多年經驗。無論你是在寫一個新系統,還是試圖駕馭一個遺留系統,本書都會讓你高效地開發高質量的程式碼。

測試驅動、測試先行和儘早測試這些開發實踐,正在幫助成千上萬的軟體開發組織改善其軟體。在本書中,作者立足於所有讀者已經熟知的測試驅動開發知識,幫助讀者實現前所未有的優質程式碼。

為了幫助讀者更加全面、有效和輕鬆地測試任何軟體系統,本書使用真實的程式碼示例介紹了測試的模式、原則和20多個技術細節,並通過兩個完整的案例分析,即測試一個全新的Java應用程式和一個未被測試的“遺留”JavaScript jQuery外掛,將本書講述的所有內容整合在了一起。此外,作者還展示了一個概念框架,幫助讀者將精力重點放在改善貫穿整個軟體生命週期的可測試性上,並給讀者提供了簡化程式碼構造的全系列測試的實操指南。

無論是最常見的場景還是多執行緒,本書都會幫讀者學會如何針對每一種情景選擇最好的測試技術;無論是為一個新的創業公司開發前沿程式碼,還是維護一個很難駕馭的老舊系統,本書都會幫讀者交付其真正需要的優質程式碼。

簡化所有程式碼的單元測試,並改善整合測試和系統測試。

  • 詳述意圖和實現,促進更加可靠和可擴充套件的測試。
  • 克服對編寫測試的機制的混淆和誤解。
  • 測試“副作用”、行為特徵和上下文約束。
  • 瞭解軟體設計與可測試性之間微妙的互動,並對其進行利用,而非受困其中。
  • 揭示能夠指導關鍵測試決策的一些核心原則。
  • 探討以下內容的測試:getter/setter、字串處理、封裝、覆寫變化、可見性、單例模式、錯誤條件等。
  • 確定性地重現並測試一些複雜的競態條件。
  • 相關閱讀
    評論(1)

    相關文章