Clean Code(無瑕的程式碼)心得
Chap01 動機
- 神就藏在細節裡: 一致性的縮排是程式低錯誤率的最顯著指標
- Later equals NEVER: 不及時清理 → 累積愈多難看的程式碼 → 愈難清理,所以更不想清理 → 直到修改的成本太高,只好重寫
- 不夠好的程式碼使維護成本太高(你看得懂自己寫的code嗎?)
讓開發速度變快的方法:隨時保持clean code
Chap02 Clean Code 的定義
認為自己的code應該要有的樣子
CleanCode學派(作者)對此的定義
- 每個函式、類別、模組都能表達單一意圖,降低程式相依性
- 易讀:不該使人猜測程式的意思
- 因為在寫新的程式碼前,要先花時間了解舊程式碼
- 每個看到的程式,執行結果都與你想得差不多
- 抽象化:程式碼不重複
Chap03 原則
任何原則在特殊情形都是可以違反的,不須過於拘泥
- 童子軍規則:離開的code比剛來時更乾淨
- 寫軟體如同寫作,先把想法寫下來,然後開始啄磨,直到讀起來很通順。第一份初稿通常是粗糙而雜亂無章的,修改之後才會改善到想要的樣子。程式設計大師在寫程式時,並不認為自己是在寫程式,而是在說故事。
- 寫程式時,只能專注在 「讓程式運作」或「讓程式整潔」其中之一,要先程式能動再清理;或是讓程式架構明確易懂再實作都可以。
有意義的命名
- 有辦法唸出來的名稱,愈具體愈好
- 用 define, enum, const 代替 常數
enum color{black, white}
- 86400 →
SECOND_PER_DAY
- 容易了解的數字就不用
circumference = radius * Math.PI * 2
: 不用將2換成實際名稱
- class:名詞或名詞片語
board
→chessGameBoard
address
→portAddress
,EmailAddress
day
→elapsedTimeInDays
- method:動詞或動詞片語
flag
→isFlagged()
Complex c = Complex(23.0)
→Complex c = Complex.FromRealNumber(23.0)
- 使用靜態工廠
- 對特定功能使用一致的用詞
fetch
,retreive
,get
...
- 避免
data
,info
,manager
等意義較廣的字accountList
→accountGroup
(除非此變數真的是list型態)add()
→insert()
,append()
- 使用專有名詞
jobqueue
timestamp
- 使用範圍較大的變數用較長的名稱
- 愈少用的函數名稱可以愈長
- for迴圈範圍較小,變數可以用
i
,j
1 | //which is better? |
函式
- 一個函式只做在同一層級上的一件事情
- 以「無法再分割」為標準
- 長度:小於二十行(或一個螢幕的長度)
- 不用switch:switch容易違反
- 單一職責原則
- 開放閉合原則
- 解法:抽象工廠
- 使用switch和多型
- 參數
- 愈少愈好($\leq$ 三個)
- 太多參數時需要記順序,像是
strcpy()
- 例外:四維象限中的座標(只算作一個參數),
printf()
(只算作兩個參數)
- 太多參數時需要記順序,像是
- 避免用參數當回傳值
- 如
void saveResult(FILE* f, int* returnStatus)
,使用returnStatus來作為回傳 - 解法一:呼叫擁有變數的class,用其method來修改
- 解法二:用return
- 用例外處理取代回傳錯誤碼
- 如
- 通常只使用一個參數
- 用途一:取得參數的性質
boolean isFileExists("MyFile")
- 用途二:使用參數,然後回傳操作後的結果
InputStream fileOpen("MyFile")
- 用途一:取得參數的性質
- 避免flag
- 代表函式不只做一件事
- 有flag和沒有flag做的事不同
render(bool isSuite)
→renderForSuite()
+renderForSingleTest()
- 代表函式不只做一件事
- 減少參數的方法
- 將多個參數合成一個class
- 拆成多個函式
- 愈少愈好($\leq$ 三個)
- 命名
- 以「不用重複查看函式定義」為原則
write(name)
→writeNameField(name)
assertEquals(expected, actual)
→assertExpectedEqualsActual(expected, actual)
- 描述可能的副作用
getOOS()
→createOrReturnOOS()
: 呼叫時若無OOS的時候,可能會create
- 以「不用重複查看函式定義」為原則
- 指令(command)與查詢(query)分離
- 此函式同時有查詢和進行動作的功能:
if(setAttribute("username", "unclebob")) ...
- 改進:
if(attributeExists("username")) setAttribute("username", "unclebob");
- 此函式同時有查詢和進行動作的功能:
- 適當的靜態(static)宣告
- 靜態方法用到的資料都從參數而來,而不是從任何擁有這個方法的物件得來
- 結構化程式設計準則
- 每個函式的區塊都應該只有一個進入點和一個離開點(沒有break, continue, goto,只有一個return)
- 在函式夠短的情況下沒有必要
- 暴露時序耦合
- 必須先執行
A()
再執行B()
的函式 - 原本C為member,改成傳參數:
cls.A(); cls.B();
→C = cls.A(); cls.B(C);
- 必須先執行
- 常數宣告:放在適當的層級
- 將預設的常數放在呼叫的參數,而非被呼叫的函式內
getPageNameOrDefault(request, "FrontPage")
//default is "FrongPage"
- 放在愈高階就愈容易修改
- 將預設的常數放在呼叫的參數,而非被呼叫的函式內
註解
註解是輔助程式碼來表達意圖的工具
-
有註解代表程式碼不夠易懂
-
愈少愈好
-
與其寫註解,不如把程式碼弄整潔
-
註解通常缺少維護
- 容易產生許多過時的註解
- 錯誤的註解比沒有註解可怕
-
必要的註解
- 版權宣告
- 舉例示範
- 解釋意圖
- 對某個問題的解決方法
- 使用的演算法
- 解釋自己無法修改的程式碼(函式庫等)
- 警告
- 不希望被修改的地方
- 暫時記錄:
TODO
,BUG
...
-
糟糕的註解
- 浪費時間看,最後被忽略
- 沒有提供更多資訊
printBoard() // print board
- 過多的資訊
- 被強迫寫的(通常就是不必要的)
- 沒有提供更多資訊
- 已被版本控制軟體取代的功能
- 版本變動記錄
- 註解掉的程式碼
- 過度使用標誌
- 如
// comment //////////////////
- 如
- 浪費時間看,最後被忽略
排版
- 偏好小檔案(200-500行)
- 報紙型編排:先出現標題(高階概念、演算法),再來是內容(低階函式)
- 最重要的概念先出現,用最少的資訊來表達,再來才是實作細節
- 垂直距離:類似的概念應盡可能靠近
- 空白行用來分隔思緒,概念(類似文章分段)
- 做相似工作的函式愈近愈好
- 變數宣告的位置:靠近變數被使用的地方
- 若函式夠短,可在函式最上方宣告
- 降層準則
- 函式後面為其呼叫的函式,易於閱讀
- 空白行用來分隔思緒,概念(類似文章分段)
- 將常數宣告放在一個大家比較容易找到的地方
- 寬度:不要超過螢幕
- 通常會限制在80字
- 不過現在都是寬螢幕了,影響不大
- 使用空白強調運算子的優先權
b*b - 4*a*c
- 通常會限制在80字
物件及資料結構
你知道的太多了...
變數保持私有的理由,不希望有人依賴此變數,保持一個自由的空間,讓我們能自由的更改變數的型態,或是在突如其來的奇想或衝動時,能自由的變更實現內容的程式碼。
那為什麼有這麼多的程式設計師,自動替他們的物件加上getter和setter,讓他們的私有度數如同公用變數呢?
1 | //完全暴露實現 |
利用抽象化的詞彙來表達資料。這並不是只透過介面及讀取、設定函式就能完成。想辨法找到最能詮釋「資料抽象概念」的方式。
而最差的作法,則是天真地加上讀取函式及設定函式而已。
1.物件
- private member,public method
- 將實現的過程隱藏(封裝)
- 用抽象詞彙表達資料
getGallonsOfGasoline()
→getPercentFuelRemaining()
- 要讓每件事物都是一個物件是一個神話(Java表示:)
2.資料結構: map, set, array ... - 暴露資料(public member)
- 為資料結構設getter和setter是多此一舉
物件與資料結構的互補
- 物件:新資料型態的彈性 ↔ 資料結構:新行為的彈性
- 資料結構容易添加函式,而不用更改現有的資料結構
- 物件容易添加新的類別(繼承),而不用更改現有的函式
- 混合體只會得到兩者的缺點
The law of Demeter
- 模組不該知道關於它所操縱物件的內部運作
- 違反原則: 火車連結
String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()
- 改進(仍然知道底層操作)
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
String outputDir = scratchDir.getAbsolutePath();
- 若 ctxt, options 為資料結構,則以下可行
outputDir = ctxt.options.scratchDir.absolutePath
- 若是物件,則我們應該要告訴 ctxt 去做某件事情
- 和其他程式碼合併 :
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName)
- 和其他程式碼合併 :
可調整的資料應放在高層次
- 底層不應存放參數
- 放在高階層比較好找,也比較好修改
One Switch原則
- 對於給定的變數,不應該有超過一個以上的switch敘述。在那個唯一的Switch敘述中的多個case,必須建立多型物件以取代其他case。
錯誤處理
- 定義正常的流程
- 使用特殊情況物件(special case pattern)替代
if()
檢查- 將特殊情況包在特殊情況物件
- 特殊情況物件處理例外
- 包裹第三方程式庫
- 減少依頼,容易更換
- 使用特殊情況物件(special case pattern)替代
- 使用 例外處理(try-catch-finally) 取代回傳error code
- 例外處理是「一件事」
- error code 必須在呼叫結束之後立即檢查錯誤
- 提取try和catch的內容,成為新函式
- 用class包裹例外,此class只處理例外
- 例外處理是「一件事」
- 不要傳遞NULL
- 要檢查值是不是NULL,很麻煩
- 所有函式都不return NULL → 都不用檢查
- 解決
- 回傳特殊情況物件
- 例:找不到時,回傳空list
- 使用例外處理
- 回傳特殊情況物件
- 要檢查值是不是NULL,很麻煩
- Java:使用不檢查型例外:,(檢查和不檢查型例外),因為其破壞開放閉合原則,修改低階函式時也需要修改高階函式
- 將邊界條件的檢查放置於同一個地方,不要散佈在程式的各個角落
邊界
註:從這章之後大多是一章一個作者,所以頗有矛盾和重複的地方
使用第三方API
- 學習性測試
- 寫一些測試程式來了解第三方軟體
- 第三方軟體改版後也能用來確定行為是否改變
- 包裹第三方API
public void open() { try{innerPort.open();} ... }
- 好處
- 掌握控制程式的權利
- 在API未知的情況可以繼承此interface,創造一個Fake API來測試
單元測試
有了測試程式(和版本控制系統)就不會害怕修改程式!
測試驅動開發(TDD)
The Three Rules of TDD
- 先寫測試程式,再寫對應的實作程式
- 只寫剛好無法通過的單元測試
- 測試無法通過時,應該要修復實作程式
- 只寫剛好能通過測試的程式
- 測試無法通過時,只能寫和測試有關的實作程式,不能寫其他功能
測試程式和產品程式一樣重要
測試方法
- 使用涵蓋率工具(Coverage Tool)
- 檢查是否每行程式都有被執行過
- 測試邊界條件
原則
- 測試的程式碼也需要整潔
- 最重要的是可讀性,效率是其次
- 建造-操作-檢查(BUILD-OPERATE-CHECK)
- 產生數據,操作之,再檢查正確性
- GIVEN-WHEN-THEN
- Given: 前提、環境
- When: 發生事情
- Then: 預期結果
- 一個測試只用一個assert
- 在錯誤的程式碼附近進行詳盡的測試
- 因為Bug往往聚集
測試程式的 F.I.R.S.T. 法則
- Fast:快速(足夠快即可)
- Independent:可個別獨立執行
- Repeatable:可在任何環境重複執行
- Self-Validating:輸出bool值(成功/失敗),而不是記錄檔
- 不用做額外的檢查
- Timely:先寫測試再實作
類別
- 凝聚性:方法內使用愈多變數,代表這個方法更屬於這個類別
- 為了保持凝聚性,會產生許多小類別
把工具放在有許多標記的小型工具箱裡,比少量的大抽屜,然後將所有的東西都丟進去好。
不應該對相依的模組有預先的假設(即邏輯上的相依),應該清楚的詢問所需的有關訊息(即實體上的相依)。
系統
如同建造城市,某些人負責整體規劃,其它人專注在細節執行。
進行抽象化和模組化,將所有關注的事分離開來
- 工廠模式:將建造和使用分離
相依性注入(1)(2)
- 控管反轉
- 將第二個職責移到其它專注於該職責的物件裡
擴大
- 誰有辦法預期小鎮的成長,而在鎮上先鋪好六線道高速公路?
- 讓系統一開始就做對,是一個神話
- 應該只實現今天的故事(即目前的需求),然後重構它,並且明天再進行系統的擴充
- 剖面導向程式設計AOP(1)(2)
- 剖面(aspect)
- 系統中某個關注點的行為,需要用一致性的方式修改
- 如日誌記錄(log)、資料庫、安全性、暫存快取
- 紀錄檔功能往往橫跨系統中的每個業務模組,即「橫切」所有有紀錄檔需求的類及方法體
- 為應用程式基礎架構
- 系統中某個關注點的行為,需要用一致性的方式修改
- 保持適當的關注點分離
- 剖面(aspect)
不需要「先作大型設計」,因為不希望浪費先前的努力,這個設計會阻止你改進程式架構。
有時候拖延至最後一刻是最好的作法,這讓我們得以運用最多的資訊進行選擇。
系統需要特定領域的語言(Domain-Specific Language), 讓領域專家可以把程式寫得像是散文的結構,而且領域概念和實作程式碼相似,減少轉換錯誤。
剖面範例
- Java代理機制
- 代理可以呼叫被代理物件的函式,也可以進行剖面的行為
- Java AOP框架
- Spring AOP
- JBoss AOP
- AspectJ
一個理想的架構,包含模組化的關注點,每個關注點都用一個普通物件實作。 不同領域之間利用最小侵入性的剖面工具整合。 這樣的架構就可以是測試驅動的(test-driven),如同程式碼一樣。
簡單設計守則
- 執行完所有的測試
- 程式重構:重要性 1 > 2 > 3
- 消除重複
- 樣版方法(Template Method)
- 大部份設計模式都是在提供消除重複的策略
- 物件導向是用來組織模組和消除重複的策略
- 具表達力
- 最小化類別及方法數量
- 消除重複
平行化
物件是處理過程的抽象化,執行緒是排程的抽象化
將「做什麼」和「何時做」分解開來,像是Web應用的Servlet模型
當獲得一個網頁請求時,servlet會以非同步方式執行,不需要管理所有的請求,每一個servlet都在其自我的小小世界裡執行。
缺點
- 需要修改程式碼/架構
- 速度不一定變快(有overhead)
- 很難寫正確,因為很難重現bug
建議
- 將平行化的程式碼和其他程式碼分開
- 保護資料
- 限制共享資料的存取次數
- 減少critical section的使用次數
- 限制資料的視野
- mutex lock(c), synchronized(java)
- 避免在一個共享物件上使用多個方法
- 使用資料的複本
- 由一個執行緒負責合併結果
- 每個執行緒盡可能獨立運行
- 限制共享資料的存取次數
- 測試
- 偽造/產生失敗以測試
- 讓平行化的程式可以開關平行化的功能
- 在不同的平台測試
- 執行比處理器數量還多的執行緒
- 強迫進行task swapping
- 調整參數(執行緒數量、重複執行次數)
- 正確的關閉程式
- 防止deadlock
- 用IBM contest找BUG
- 不一定會照順序執行
實作
- ReentrantLock
- 一般的鎖
- Semaphore號誌
- 有計數功能的鎖
- CountDownLatch
- 先等待指定數量的事件,使得所有的執行緒都有公平的機會,在同時間啟動
模型
- 生產-消費
- 有限資源
- 生產者放工作入queue
- 消費者取出工作
- 讀取-寫入
- 平衡reader和writer的需求,避免某一方飢餓(starvation)
- 簡單方法:writer等到沒有reader使用時再write
- writer容易starve
- 哲學家用餐
- 雙手都拿到刀叉時才能用餐
- 大程式的資源爭奪問題
詳見平行程式設計
程式碼的異味
聖人見微以知萌,見端以知末,故見象箸而怖,知天下不足也。
韓非子.說林上
程式碼的異味(Code Smell):
程式碼中的任何可能導致深層問題的症狀
- 一份檔案有多種程式語言
- 明顯該有的行為未實現
- 在邊界條件的不正確行為
- 封裝邊界條件: 將邊界條件的集中處理
int nextLevel = level+1; //boundary condition
- 封裝邊界條件: 將邊界條件的集中處理
- 無視編譯器警告
- 否定的判斷式
2. 改善if(!buffer.isFull())
→if(buffer.isNotFull())
- 過多的資訊
4. 類別擁有的變數、方法數量愈少愈好、方法擁有的變數愈少愈好 - 不一致性
5. 命名的一致性:對於同一個物件,在不同函式中的實體使用相同的名稱
6. 行為的一致性:用同樣的方式來完成所有類似的事 - 特色留戀(Feature Envy)
7. 使用其他物件的getter或setter
8. 有時候是必要之惡:放入別的物件會違反其他OOP的原則(此例:單一職責、開放閉合原則等)
1 | //demo of feature envy |
參考資料
- 30天快速上手 TDD
- The Principles of Good Programming