測試:
- 測試優先編程——在寫代碼前先寫好測試用例,儘早發現bug。
- 利用分區與分區邊界來選擇測試用例。
- 白盒測試與聲明覆蓋率。
- 單元測試——將測試模塊隔離開來。
- 自動化迴歸測試杜絕新的bug產生。
還記得好軟件具備的三個屬性嗎?試着將它們和測試聯繫起來:
- 遠離bug 測試的意義在於發現程序中的bug,而“測試優先編程”的價值在於儘可能早的發現這些bug。
- 易讀性 測試並不會使代碼審查變得容易,但是我們也要注意正確書寫測試註釋。
- 可改動性 我們針對改動後的程序進行測試時只需要依賴規格說明中的行爲描述。(這裏的測試針對的是正確性而不是魯棒性)。另外,當我們完成修改後,自動化迴歸測試能夠幫助我們杜絕新的bug產生。
代碼評審:
代碼評審是一種廣泛應用的軟件質量提升方法。它可以檢測出代碼中的各種問題,但是作爲一個初學課程,這篇閱讀材料只提及了下面幾個好代碼通用的原則:
- 不要重複你的代碼(DRY)
- 僅在需要的地方做註釋
- 快速失敗/報錯
- 避免使用幻數
- 一個變量有且僅有一個目的
- 使用好的命名
- 避免使用全局變量
- 返回結果而非打印它
- 使用空白符提升可讀性
下面把代碼評審和我們的三個目標聯繫起來:
- 遠離bug. 通常來說,代碼評審使用人的審查來發現bug。DRY使得你只用在一處地方修復bug,避免bug的遺漏。註釋使得原作者的假設很清晰,避免了別的程序員在更改代碼的時候引入新的bug。快速報錯/失敗使得bug能夠儘早發現,避免程序一直錯更多。避免使用全局變量使得修改bug更容易,因爲特定的變量只能在特定的區域修改。
- 易讀性. 對於隱晦或者讓人困惑的bug,代碼評審可能是唯一的發現方法,因爲閱讀者需要嘗試理解代碼。使用明智的註釋、避免幻數、變量目的單一化、選擇好的命名、使用空白字符都可以提升代碼的易讀性。
- 可更改性. DRY的代碼更具有可更改性,因爲代碼只需要在一處進行更改。返回結果而不是打印它使得代碼更可能被用作新的用途。
規格說明:
一個規格說明就好像是實現者和使用者之間的防火牆。它使得分別開發成爲可能:使用者可以在不理解源代碼的情況下使用模塊,實現者可以在不知道模塊如何被使用的情況下實現模塊。
規格說明和我們三大目標之間的聯繫:
- 遠離bug. 一個好的規格說明會清晰明確的要求實現者和使用者遵守相關的制約。而Bug經常是因爲實現者和使用者對於接口的理解衝突導致的,規格說明會明顯的減小這種可能性。在模塊中使用一些能夠交由機器檢查的特性,例如靜態檢查、異常等而不是註釋會進一步降低bug的可能性。
- 易讀性. 一個簡潔準確的規格說明會比源代碼本身更易讀易懂。
- 可改動性. 規格說明在實現者和使用者之間建立了一個“契約”——只要這兩方遵守這份“契約”,他們可以對自己的代碼進行任何改變。
設計規格說明:
規格說明在使用者和實現者之間起着一道防火牆的作用——對於人和代碼之間也是一樣。正如上篇閱讀談到的規格說明,這使得獨立開發成爲可能:使用者可以在不閱讀模塊源碼的情況下將源碼應用到各個地方,使用者可以不在意模塊被使用的環境(只要他們都遵循規格說明的要求)。
在實際使用中,聲明性的規格說明是最重要的。前置條件(弱化規格說明)使得使用者更困難(確保輸入合法),但是合理的使用會使得實現者能夠做出一些假設,從而選擇更合適的實現方案。
和我們的三個目標聯繫起來:
- 遠離bug. 如果沒有規格說明,即使是最小的更改都有可能使得整個程序崩潰,改動起來也是很麻煩的。一個結構良好、邏輯明確的規格說明會最小化使用者和實現者之間的誤解,並幫助我們進行靜態檢查、測試、代碼評審等等。
- 易於理解. 一個好的規格說明會讓使用者不必去閱讀源碼也能正確安全地使用模塊。例如,你可能永遠不會去閱讀Python dict.update ,但是通過閱讀對應的聲明性規格說明你就能很好的應用它。
- 可改動性. 一個合理的“弱”規格說明會給實現者一定的自由,而一個“強”的規格說明會給使用者一定的自由。我們甚至可以改變規格說明本身:只要我們是加強了它而不是削弱了它(減弱前置條件或者增強後置條件)。
可變性與不變性:
在這篇閱讀中,我們看到了利用可變性帶來的性能優勢和方便,但是它也會產生很多風險,使得代碼必須考慮全局的行爲,極大的增加了規格說明設計的複雜性和代碼編寫、測試的難度。
確保你已經理解了不可變對象(例如String)和不可變索引(例如 final 變量)的區別。畫快照圖能夠幫助你理解這些概念:其中對象用圓圈表示,如果是不可變對象,圓圈有兩層;索引用一個箭頭表示,如果索引是不可變的,用雙箭頭表示。
本文最重要的一個設計原則就是不變性 :儘量使用不可變類型和不可變索引。
接下來我們還是將本文的知識點和我們的三個目標聯繫起來:
- 遠離bug.不可變對象不會因爲別名的使用導致bug,而不可變索引永遠指向同一個對象,也會減少bug的發生。
- 易於理解. 因爲不可變對象和索引總是意味着不變的東西,所以它們對於讀者來說會更易懂——不用一邊讀代碼一邊考慮這個時候對象或索引發生了哪些改動。
- 可改動性. 如果一個對象或者索引不會在運行時發生改變,那麼依賴於這些對象的代碼就不用在其他代碼更改後進行審查。
避免調試:
在這篇閱讀中,我們介紹了幾種最小化調試代價的方法:
- 避免調試
- 使用靜態類型檢查、動態檢查、不可變類型和不可變索引讓bug無法產生。
- 限制bug範圍
- 通過斷言檢查、快速失敗讓bug的影響不擴散。
- 通過增量式開發和單元測試讓bug儘量只存在於剛剛修改的代碼中。
- 最小化變量作用域使得搜尋範圍減小。
最後還是將這次閱讀的內容和我們的三個目標聯繫起來:
- 遠離bug. 本閱讀的內容就是如何避免和限制bug。
- 易於理解. 靜態類型檢查、final以及斷言都是額外的“註釋”——它們體現了你對程序狀態的假設。而縮小作用域使得讀者可以更好的理解變量是如何使用的,因爲他們需要瀏覽的代碼範圍變小了。
- 可改動. 斷言檢查和靜態檢查都是能夠自動檢查的“假設”,所以如果未來有一個程序員錯誤改動了代碼,那麼違背假設的錯誤就能馬上檢測到。
抽象數據類型:
- 抽象數據類型(ADT)是通過它們對應的操作區分的。
- 操作可以分類爲創建者、生產者、觀察者、改造者。
- ADT的標識由它的操作集合和規格說明組成。
- 一個好的ADT應該是簡單,邏輯明確並且表示獨立的。
- 對於ADT的測試應該對每一個操作進行測試,並同時利用到創建者、生產者、觀察者、改造者。
將本次閱讀的內容和我們的三個目標聯繫起來:
- 遠離bug. 一個好的ADT會在使用者和實現者之間建立“契約”,使用者知道應該如何使用,而實現者有足夠的自由決定具體實現。
- 易於理解. 一個好的ADT會將其內部的代碼和信息隱藏起來,而使用者只需要理解它的規格說明和操作即可。
- 可改動. 表示獨立使得實現者可以在不通知使用者的情況下對ADT內部進行改動。
抽象函數與表示不變量:
- 不變量是指對於一個對象,它有一種能夠在整個生命週期保證爲真的屬性。
- 一個好的ADT會確保它的不變量爲真。不變量是由創建者和生產者創建,被觀察者和改造者保持。
- 表示不變量明確了什麼是合法的表示值,並且這些表示應該在運行時調用checkRep()檢查。
- 抽象函數將具體的表示映射到抽象值上。
- 表示暴露會威脅到表示獨立性和表示不變量。
下面將這篇閱讀的知識點與我們的三個目標聯繫起來:
- 遠離bug. 一個好的ADT會確保它的不變量爲真,因此它們不會被使用者代碼中的bug所影響。同時,通過顯式的聲明和動態檢查不變量,我們可以儘早的發現bug,而不是讓錯誤的行爲繼續下去。
- 易於理解. 表示不變量和抽象函數詳細的表述了抽象類型中表示的意義,以及它們是如何聯繫到抽象值的。
- 可改動. 抽象數據類型分離了抽象域和表示域,這使得實現者可以改動具體實現而不影響使用者的代碼。
接口與枚舉:
抽象數據類型是由它支持的操作集合所定義的,而Java中的結構能夠幫助我們形式化這種思想。
這能夠使我們的代碼:
- 遠離bug. 一個ADT是由它的操作集合定義的,而接口就是做了這件事情。當使用者使用接口類型時,靜態檢查能夠確保它們只使用了接口規定的方法。如果實現類寫出了/暴露了其他方法——或者更糟糕,暴露了內部表示——,使用者也不會依賴於這些操作。當我們實現一個接口時,編譯器會確保所有的方法標識都得到實現。
- 易於理解. 使用者和維護者都知道在哪裏尋找ADT的規格說明。因爲接口沒有實例成員或者實例方法的函數體,所以它能更容易的將具體實現從規格說明中分離開。
- 可改動. 我們可以輕鬆地爲已有的接口添加新的實現類。如果我們認爲靜態工廠方法比類構造方法更合適,使用者將只會看到這個接口。這意味着我們可以調整接口中工廠方法的實現類而不用改變使用者的代碼。
Java的枚舉類型能夠定義一種只有少部分不可變值的ADT。和以前使用特殊的整數或者字符串相比,枚舉類型能夠幫助我們的代碼:
- 遠離bug. 靜態檢查能夠確保使用者沒有使用到規定集合外的值,或者是不同枚舉類型的值。
- 易於理解. 將常量命名爲枚舉類型名字而非幻數(或其他字面量)能夠更清晰的做自我註釋。
- 可改動. 無
調試:
在這篇閱讀中,我們學習瞭如何系統的進行調試:
- 構建測試用例復現bug,並將其添加到測試套件中
- 使用科學的方法發現bug:
- 調試提出假設
- 利用探針(print、assert、debugger)來觀察程序的行爲並測試假設的前置條件是否滿足
- 徹底而非草率的修復bug
對於我們課程的三個目標,這篇閱讀主要針對的是遠離bug:我們試着剔除bug,並利用迴歸測試防止bug重新出現。
相等:
- 相等應該滿足等價關係(自反、對稱、傳遞)。
- 相等和哈希必須互相一致,以便讓使用哈希表的數據結構(例如 HashSet 和 HashMap)正常工作。
- 抽象函數是不可變類型相等的比較基礎。
- 索引是可變類型相等的比較基礎。這也是確保相等一致性和保護哈希表不變量的唯一方法。
相等是實現抽象數據類型中的一部分。現在我們將本文的知識點與我們的三個目標聯繫起來:
- 遠離bug. 正確的實現相等和哈希對於聚合類型的使用很重要(例如集合和映射),這也是寫測試時很需要的。因爲每一個對象都會繼承Object中的實現,實現不可變類型時一定要覆蓋它們。
- 易於理解.使用者和其他程序員在閱讀規格說明後會期望我們的ADT實現合理的相等操作。
- 可改動. 爲不可變類型正確實現的相等操作會把索引相等和抽象值相等分離,也對使用者隱藏對象是否進行了共享。爲可變類型選擇行爲相等而非觀察相等幫助我們避開了隱祕的bug。