2022年10月17日 星期一

Clean Code 無瑕的程式碼 讀書心得

沒有留言:
 


最近團隊裡辦讀書會,挑選到這本經典又不算太難的書,不同時期閱讀感覺很不相同;在業務時程壓力下,要寫作品質優良且切合需求的程式碼,對程式設計師的確是一大考驗。寫程式像作畫一樣,知識和實作同等重要;反覆揣摩習得的知識,才能逐漸累積踏實的開發自信。


前言



本書介紹撰寫 clean code (無瑕的程式碼) 的一些基本原則和實踐,主要從物件導向程式設計原則敏捷軟體開發的角度切入,從最基礎的命名/註解/編排,到物件導向/單元測試/重構案例等實戰演練,逐步將壞氣味的程式碼轉變成好維護的程式碼。

作者為 Robert C. Martin,人稱 Uncle Bob,程式設計經驗超過40年,是敏捷軟體開發方法 (Agile Software) 的提倡者之一。

 


書目

 

第1章  無瑕的程式碼      (Clean Code)
第2章  有意義的命名      (Meaningful Names)
第3章  函式          (Functions)
第4章  註解          (Comments)
第5章  編排          (Formatting)
第6章  物件及資料結構     (Objects and Data Structures)
第7章  錯誤處理        (Error Handling)
第8章  邊界          (Boundaries)
第9章  單元測試        (Unit Tests)
第10章  類別         (Classes)
第11章  系統         (Systems)
第12章  羽化         (Emergence)
第13章  平行化        (Concurrency)
第14章  持續地精煉      (Successive Refinement)
第15章  JUnit 的內部結構    (JUnit Internals)
第16章  重構SerialDate      (Refactoring SerialDate)
第17章  程式碼的氣味和啟發  (Smells and Heuristics)

附錄 A  平行化之二      (Concurrency II)
附錄 B  org.jfree.date.SerialDate (第16章,重構的目標與結果)
附錄 C  啟發的相互參照    (Cross References of Heuristics)

 


Clean Code 線上資源

書裡的範例主要由 java 編寫,我在練習的時候有找目前開發上比較常用的 python 版本作對照,也比較切合目前業務開發上的需要,這邊列出 Python 和 JavaScript 的資源。


Clean Code Python:

Clean Code JavaScript:

 

另外是 Uncle Bob 針對 clean code 書籍的線上演講和網站


"Coding Better World Together" is a set of master lessons from the famous Uncle Bob (Robert Cecil Martin), where he gives us a broad vision of the importance and future of Software in today's society.

p.s. 一些演講中跟 clean code 無關的小題目還滿有趣的! (e.g. 測量太陽到地球的距離)

 

 

一、整潔程式碼寫作原則



第1章至第5章主要介紹一些一般程式碼寫作原則,整潔的程式碼寫作目標主要如下:

  • 表達單一意圖:程式碼專注一件事、具有表達力;每個看到的程式,執行結果都與你想得差不多。

  • 抽象化:程式碼不重複,具抽象概念,降低程式相依性。

 

心態上,寫作程式碼要

  • 持續地保持程式編寫的整潔

  • 童子軍規則:離開時比剛來前更乾淨

 

一般程式碼寫作原則,命名、函式、註解相關注意事項

  • 有意義的命名

    • 讓名稱代表意圖

    • 宣告常數名稱常數 (SECOND_PER_DAY = 86400)

    • 方法用動詞 (getXX, setXX, isXX)、 class 用名詞

  • 函式

    • 一個函式只做一件事情 (同一層次上)

    • 參數愈少愈好 (原則上要小於等於 3 個)

    • 指令查詢分離

    • 使用例外處理取代回傳錯誤代碼

    • 不要重複自己

    • 結構化程式設計(no one return, no break/continue/goto, 短程式可以視情況)

  • 註解

    • 註解用途是:彌補程式碼表達意圖的失敗,註解應該最少,只留必要的註解

      • 註解容易隨著時間失真,常常會沒有跟著程式碼一起改到(只有程式碼是真的,活水有在流動執行)

      • 提升程式碼本身表達力,透過修改程式碼來移除註解,彰顯意圖

    • 使用 git 等版本控制系統後,就不需要的註解:版本歷史 / 程式碼修改 / 作者等資訊

    • 有益的註解種類:法律型註解、資訊型註解 (function doc)、意圖的解釋、後果告誡、TODO、放大重要性

 

[用心去感覺] PEP8 和 pythonic 規範

以 python 開發來說,在 python 的 PEP8 規範下,基本上很多排版、註解、命名、類別函式的相關規範會比上面的寫作原則更嚴格,日常可以用 pylint 或其他 linter 工具去做相關的自動更正或提示,省心又符合規範。

 


二、物件導向程式設計基本原則:SOLID


物件導向程式設計的五個基本原則,也是測試驅動開發和敏捷開發相關領域的重要概念,這五個 SOLID 原則分別為:

  • 單一職責原則 (Single Responsibility Principle, SRP)

  • 開放封閉原則 (Open-Close principle, OCP)

  • 里氏替換原則 (Liskov substitution principle, LSP)

  • 接口遠離原則 (Interface segregation principle, ISP)

  • 依賴反轉原則 (Dependency inversion principle, DIP)

主要目的是藉由物件導向的特性,讓程式設計的變更和擴展更容易 (高内聚、低耦合),程式結構更強健。

 

1. 單一職責原則 (Single Responsibility Principle, SRP)

"There should never be more than one reason for a class to change."

基於內聚原則(Cohesion),一個模組應該只有一個改變的原因 (一個職責)。一個檢查的方法是如果你能夠想到多於一個的動機去改變一個模塊,那麼這個模塊就具有多於一個的職責。

 

2. 開放封閉原則 (Open-Close principle, OCP)

"Software entities ... should be open for extension, but closed for modification."

軟體中的模塊應該對於擴展是開放的,但是對於修改是封閉的;也就是說在不改變現有原始碼的前提下,變更它的行為/實作新的功能。

Bertrand Meyer is generally credited for having originated the term open–closed principle, which appeared in his 1988 book Object Oriented Software Construction.

A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.

 

3. 里氏替換原則 (Liskov substitution principle, LSP)

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

程式中的物件應該是可以在不改變程式正確性的前提下被它的子類所替換的;也就是說,子類別實作,不可違反父類別介面的合約 (合約設計,父類已經簽的合約,子類不可以再推翻);如果父類與子類使用一樣的參數,子類會拋出錯誤或有不可預期的副作用,這就違反了里氏替換原則,例如方形繼承矩形的面積問題。

這個原則最早由美國計算機科學家芭芭拉·利斯科夫 (Barbara Liskov) 提出 (Liskov, Barbara. Data abstraction and hierarchy. 1987)

 

4. 接口遠離原則 (Interface segregation principle, ISP)

"Clients should not be forced to depend upon interfaces that they do not use."

客戶(client)不應被迫使用對其而言無用的方法或功能 (這裡客戶指的是上下層模組關係)。解決方式是拆分非常龐大臃腫的介面成為更小的和更具體的介面。

 

5. 依賴反轉原則 (Dependency inversion principle, DIP)

"Depend upon abstractions, [not] concretions."

指一種特定的解耦(傳統的依賴關係建立在高層次上,而具體的策略設定則應用在低層次的模組上)形式,使得高層次的模組不依賴於低層次的模組的實現細節,依賴關係被顛倒(反轉),從而使得低層次模組依賴於高層次模組的需求抽象。

 

[用心去感覺] 物件導向程式設計

很多時候好的設計是很內化的,甚至 SOLID 說白了有點像物件導向的廢話。遵循 SOLID 的物件導向程式讓使用者幾乎察覺不到其中的設計,只感覺到它的便利,像是使用各 library package 時,不用去想到開放封閉原則、單一職責原則也能很輕鬆的使用裡面的物件。

SOLID 程式範例可以看 github 上的資源實際執行閱讀程式碼感受一下,像是 Clean Code Python:

 

 

三、單元測試和測試驅動開發 (Unit Test and Test-Driven Development, TDD)



在現代的開發方法裡,撰寫自動化的單元測試程式整合進 CI pipeline 中是一個重要的步驟,測試驅動整個開發過程:

  • 開發時:驅動程式設計和功能實作

  • 開發完成後:驅動程式重構發生(重構改動有單元測試保護)

另外,測試驅動也有釐清需求規格和範圍的作用(需求不清楚會寫不出功能的測項),在程式碼開發完成後,測試驅動開發的這些測試會成為單元測試的一個部分。

 

TDD 的三大法則

  • 第一法則:在撰寫一個單元測試 (測試失敗的單元測試) 前,不可以撰寫任何產品測試。

  • 第二法則:只撰寫剛好無法通過的單元測試,不能編譯也算無法通過。

  • 第三法則:只撰寫剛好能通過當前測試失敗的產品程式。

以上三個步驟可以將我們限制在一個約 30 秒的循環內。

 

整潔測試概念

  • 單元測試與產品一樣重要

  • 單元測試的可讀性愈高愈好(e.g. 建造-操作-檢查模式)

  • 特定領域的測試語言(建立公用程式間接使用系統 API)

  • 一個測試一個概念 & 一個測試一個 Assert

 

整潔測試的 FIRST 原則

  • Fast (快速):測試應該要夠快。當測試執行緩慢時,你就不會想要常常執行他們。

  • Independent (獨立):測試不應該相互依賴。相互依賴會讓錯誤診斷變困難,進而隱瞞缺陷。

  • Repeatable (可重複性):測試程式應該可以在任何環境中重複執行。

  • Self-validating (自我驗證):測試應該輸出布林植,測試要有自我驗證的能力,不用經過人為比較記錄檔的判讀。

  • Timely (及時):撰寫測試程式要及時。TDD 方法會在產品程式之前完成,延遲的測試就是不會寫測試。

 

[用心去感覺] 一些實務上遇到的狀況

  • 初期導入使用 TDD,開發速度會降低,開發方法和工具不習慣,需要團隊成員花功夫下去熟悉,對開發程度要求較高。

  • TDD 容易讓開發者忽略對實際需求的實現,不利於初期概念展示 (手上沒好東西,進會議室…)。

  • 前端邏輯/資料庫(SQL邏輯嵌入/Mock大量資料),在 interface 沒切乾淨的程式碼上,會很難做 unit test 的覆蓋,要重構也曠日廢時,時程也不一定允許這麼做。

 

 

四、程式碼的氣味與技術債



當你在閱讀程式碼時,若某段程式碼讓你在閱讀或開發時非常痛苦,彷彿飄出一股臭味,那就是本章所謂的程式碼異味 (Code smell),第 17 章作者用歸納法整理出很多撰寫程式碼要注意的事項,下面列舉出一些我覺得比較有感觸的部分異味:

  • 重複的程式碼 (Copy & Paste): 修一個功能改 2~30 個地方..

  • 肥大的類別 (Large Class): 假的物件導向

  • 肥大的函式 (長方法、太多的參數): 假的函式

  • 全域變數 (Global variable): 可怕的反模式,誰改了裡面的值? reproduce 都很困難

  • 特色留戀 (Feature Envy): 模組相依性很重纏在一起

  • ...

 

程式碼異味,從專案的角度想,讓我想到另一本書人月神話。人月神話開篇就提到一種狀況:

焦油坑:史前時期最駭人的景象,莫過於一群巨獸在焦油坑裏做垂死前的掙扎。恐龍、長毛象、劍齒虎正在奮力掙脫焦油的束縛,但越掙扎,焦油就纏得越緊,就算他再強壯、再利害,最後,都難逃滅頂的命運。過去十年間,大型系統的軟體開發工作就像是掉進了焦油坑裏……

這其實是同一個狀況的一體兩面,從外部看,軟體產品需要讓人執行、測試、修改和擴充,並適用多種操作環境以及不同情況;從內部看,程式的撰寫、改動的細節和測試的完整性反映了程式碼異味是否侵蝕整個程式碼。

p.s. 程式人在形容敗壞的程式碼和專案總是這麼生動熱情、嫉惡如仇:D

 


結語


日月潭一景

得益於虛擬化/微服務和 CICD pipeline 等開發方法工具的長足發展,現在要從系統面提升自動化檢查改善程式品質,導入基本的 CICD 架構,讓團隊撰寫 clean code,比過去來的容易很多 (敏捷開發比起成書當時已成為顯學);但也正因如此,更大規模的程式服務正在擴展中,相關的系統管理也伴隨著新的挑戰。

這篇比較多 clean code 原則上的說明,後續可能再寫一些實戰上的應用,預計會以 Python API 上開發的一些服務結合 CI pipeline 的單元測試和 code scan 當例子吧。

 


References

The Clean Code Blog by Robert C. Martin (Uncle Bob)

Uncle Bob Martin Programmer, Speaker, Teacher


沒有留言:

張貼留言

技術提供:Blogger.