<span style="font-size: 2.5em; font-weight: bold;">好程式的藝術</span> <span style="font-size: 1.2em; color: gray;">Clean Code Driven Design</span> > 「寫程式就像寫作,任何人都能寫出電腦懂的程式碼,但只有優秀的工程師能寫出人類懂的程式碼。」 > > 本文件是筆者閱讀 《Clean Code》 的筆記整理。希望能幫助讀者**快速理解**其重要性與精神,亦能作為已讀者的**快速複習手冊**。 ### Why? 為什麼要寫好程式、什麼是爛程式的代價? 多數人剛開始學寫程式時,心中只有一個目標:**讓它動** 能跑、能出結果、能交差,似乎就已經完成任務了。 但當你進入真實世界的專案後,很快就會發現,「能動」只是程式生命的起點,而不是終點。 寫程式,更像是在撰寫一份會被長期閱讀、修改與擴充的文件,而不是一次性的作品。 #### 程式不是寫給電腦看的,是寫給人看的。 電腦對程式碼沒有情緒,也沒有理解成本。 只要語法正確,邏輯能跑,電腦就會乖乖執行。 但人不一樣。 根據經驗與研究,工程師花在「讀程式」上的時間,往往是「寫程式」的十倍以上。 當你要加新功能、修 bug、或只是理解一段舊程式時,你其實正在閱讀別人(或過去的自己)留下來的文字。 如果程式碼難讀,那每一次修改都是在痛苦中前進。 如果程式碼清楚,那撰寫新程式反而會變得更快。 這也是為什麼有人說: > 任何人都能寫出電腦懂的程式碼,但只有優秀的工程師能寫出人類懂的程式碼。 #### **爛程式的代價,會隨時間放大** 有一個殘酷但真實的法則,叫做 **LeBlanc’s Law**: > Later equals never > 之後再整理,通常等於永遠不會整理。 一開始偷懶留下來的爛程式碼,未來幾乎不可能「等有空再修」。 因為專案只會越來越大、時程只會越來越趕,而爛程式碼會像債務一樣,不斷累積利息。 結果就是: - 每加一個功能,風險都在上升 - 每修一個 bug,都可能引出新的 bug - 工程師開始害怕改動程式碼,因為「不知道會壞哪裡」 最終,整個系統會變成一個誰都不敢碰的黑盒子。 #### **好程式,其實是在降低「認知成本」** Clean Code 並不是追求炫技、也不是寫給面試官看的漂亮程式。 它真正的價值,在於降低理解的成本。 好的程式碼,會讓你在閱讀時: - 幾乎不用猜作者想幹嘛 - 名稱本身就表達了意圖 - 邏輯直截了當,而不是層層推理 - 每一段程式都只負責一件清楚的事 **當你看到程式碼時,執行結果會和你腦中預期的幾乎一致。** 那一刻,你會感覺這門程式語言彷彿就是為了解決這個問題而存在的。 這種「讀起來順」的感覺,並不是偶然,而是刻意設計的結果。 #### 好程式碼,是一種職業態度 Clean Code 的背後,其實不是技巧,而是一種態度。 它代表著: - 你尊重下一個要維護這份程式碼的人 - 你願意為未來的自己省下時間 - 你相信軟體是會被長期使用與演進的 這也是為什麼會有「童子軍規則」這個比喻: > 離開營地時,讓營地比來的時候更乾淨一點。 寫好程式,不一定代表一次到位、完美無缺。 但它代表你願意在每一次修改時,讓程式碼變得稍微好一點點。 而這種持續性的改善,本身就是一種專業精神。 ### What? 那什麼能被稱作 Clean Code? Clean Code 並不是一種特定的寫法,也不是遵守某幾條規則就能自動達成的狀態。 它更像是一種 **「讀起來毫不費力」的感覺**。 當你打開一份程式碼,不需要來回跳轉、不需要在腦中模擬太久,就能大致理解它在做什麼,這通常就是 Clean Code 的開始。 #### Clean Code 的核心,不是聰明,而是清楚 許多人會誤以為好程式碼是「看起來很厲害的程式碼」。 充滿技巧、濃縮到極限、用最少的行數完成最多的事。 但 Clean Code 剛好相反。 它追求的是: - 邏輯直截了當,而不是機巧取勝 - 抽象清楚,而不是細節堆疊 - 意圖明確,而不是需要猜測 好的程式碼,不會掩蓋設計者的想法,反而會把意圖攤在檯面上。 你不是在解謎,而是在閱讀一段有條理的敘述。 #### 程式碼應該只做好「它該做的那件事」 在 Clean Code 的世界裡,「只做一件事」不是口號,而是判斷設計好壞的重要指標。 無論是 class、function,甚至是一小段邏輯,都應該有一個清楚的責任範圍。 當一個 function 同時處理驗證、轉換、計算、存取資料,那它看似方便,實際上卻是在累積未來的混亂。 一個簡單的判斷方式是: > 如果你能從一個 function 中,再抽出另一個有意義的 function, > 那通常代表它已經做超過一件事了。 Clean Code 偏好把事情拆開,讓每個單位都小、單純、可理解。 #### 好的名稱,本身就是最好的註解 Clean Code 很少需要解釋性註解,因為名稱本身就已經說明了一切。 variable 、 function 、 class 的命名,不是為了節省鍵盤敲擊次數,而是為了傳達意圖。 一個好的名稱,會讓你在使用它的時候,幾乎不用再回頭確認定義。 當名稱夠清楚時: - 程式碼會變得可預測 - 閱讀時會自然「順著念下去」 - 你不需要在腦中對照上下文來猜意思 Clean Code 的理想狀態,是讓程式碼看起來就像在描述問題本身,而不是在展示語言技巧。 #### Clean Code 是「可以被他人安心修改的程式碼」 一份真正乾淨的程式碼,並不專屬於原作者。 它應該能被他人閱讀、理解、修改,並在不破壞既有行為的前提下持續擴充。 因此,Clean Code 往往會伴隨 Test 存在。 不只是因為測試很「專業」,更是因為它讓改動變得安全。 當你知道改一行程式碼不會引發連鎖災難,你才有勇氣讓程式碼變得更好。 ### How? 那我該怎麼寫出像這樣的好程式碼呢? #### 把名字取好 Clean Code 的實作,幾乎都是從命名開始的。 名稱應該代表意圖,而不是型別、實作細節,或作者腦中的臨時縮寫。 當名稱取得好,程式碼本身就會變得可讀,甚至不需要額外註解。 好的命名通常具備幾個特徵: - 能唸出來、能在對話中被使用 - 可以被搜尋,避免過短或過於泛用 - 在同一個概念上,使用同一個詞彙 e.g.(名稱是否能表達意圖) ``` // Bad int d; ``` 漂亮,看到 d,你根本不知道這是什麼東西。 ``` // Good int elapsedTimeInDays; int daysSinceCreation; int daysSinceModification; int fileAgeInDays; ``` 好的名稱,會直接告訴你「它是什麼」,而不是「你自己去猜」。 e.g.(能唸出來、能在對話中被使用) ``` // Bad String genymdhms; ``` 這能唸的出來,也算是種語文天才。 ``` // Good String generationTimestamp; ``` 你在 debug 時就可以跟別人說 這個 bug 發生在 generationTimestamp 計算錯誤時,順順順。 e.g.(是否能被搜尋) ``` // Bad for (int i = 0; i < 34; i++) { ... } ``` 當你在專案裡搜尋 i 或 34,會很難找到你要的那一段。 ``` for (int userIndex = 0; userIndex < MAX_USERS; userIndex++) { ... } ``` 可以被搜尋的名稱,能讓你在大型專案中快速定位程式碼, e.g.(在同一個概念上,使用同一個詞彙) 假設你在很多 class 裡面都有 Add 這個 method,這邊的 Add 都是**相加**或**相連**的概念 這時你要新增一個新的 class,他裡面有一個 method 想要將參數加進一個集合裡 這時就不該用 Add,而是該用 Insert 或是 Append 等新的 method 命名 別說雙關語,一致性非常重要。 e.g.(避免把型別資訊塞進名稱裡) ``` // Bad(有爭議,很多人是使用這種方式) Interface: IShapeFactory Impl: ShapeFactory ``` 這個名稱強調的是「它是一個 interface」, 而不是「它的角色是什麼」。 ``` // Good Interface: ShapeFactory Impl: ShapeFactoryImpl ``` 如果真的要區分,應從實作下手 #### 類別與函式,應該自己說明自己是誰 class 與 Object,通常代表的是「事物」,所以命名應該用名詞。 當你發現自己用上 Manager、Processor、Data、Info 這類模糊的詞,往往意味著設計還不夠清楚。 function 和 method 則相反,它代表的是「行為」,命名應以動詞。 一個好的函式名稱,應該像一句動作描述,讓人一看就知道它會做什麼。 當類別與函式的名稱都站得住腳,程式碼會開始有「結構感」,而不是一堆零散的工具集合。 e.g.(避免使用模糊的 Class 名稱) ``` // Bad class UserManager { void handle(); void process(); } ``` 會很難看懂這個 `class` 到底在管理什麼東西,通常 `Manager` 代表很多事都往這裡丟 ``` // Good class UserAuthenticator { boolean authenticate(User user); } class UserRepository { User findByUserId(UserId id); } ``` 好的 `Class` 名稱,會明確說出「它是什麼角色」, 而不是只告訴你「它在處理一些事情」。 e.g.(Function 名稱應該清楚描述「做了什麼」) ``` // Bad void handle(); void doIt(); void process(); ``` 這類 function 名稱,幾乎沒有提供任何資訊, 讀者只能進去看實作,才能知道發生了什麼事。 ``` // Good void validateOrder(Order order); void calculateTotalPrice(Order order); void sendConfirmationEmail(Order order); ``` 好的 function 名稱,本身就是一行說明文件。 e.g.(當命名正確時,結構會自然浮現) ``` // Bad class DataProcessor { void processUser(); void processOrder(); void processPayment(); } ``` 這是一個典型的「萬能類別」, 什麼都能放,卻什麼都不清楚。 ``` // Good class UserService { void registerUser(); } class OrderService { void placeOrder(); } class PaymentService { void processPayment(); } ``` 當 `class` 與 `method` 的名稱都清楚時, 系統的結構會直接反映在檔案與目錄上,不需要額外說明。 #### 讓 function 小到無法再拆 > Clean Code 對 function 的要求,其實很嚴格: > 短、小、只做一件事。 這裡的「一件事」,不是技術上的分類,而是抽象層級上的一致。 如果一個 function 同時在「做事」又在「描述怎麼做」,那它很可能混合了不同層次的責任。 > 一個實用的自我檢查方式是: > 如果你能從一個 function 中,再抽出另一個有意義、好命名的 function,那通常代表原本的 function 太胖了。 #### 參數越少,理解越快 function 的參數數量,會直接影響閱讀與理解的難度。 理想情況下: - 沒有參數最好 - 一個參數可以接受 - 兩個參數勉強 - 三個以上,除非真的有特殊的理由,否則幾乎都該重新設計 每多一個參數,讀者就必須在腦中多記一個前提條件。 尤其是 boolean 參數,它通常暗示著:這個 function 其實在做不只一件事。 與其用參數切換行為,不如把行為拆成不同的函式,讓名稱直接表達差異。 #### 明確的資料流與責任歸屬 > 如果一個 function 需要改變某個物件的狀態,那就讓那個物件自己負責改變,而不是把它當成輸出參數丟來丟去。 e.g. ``` AppendFooter(s); ``` 這邊的 method 可能有兩種情境 1. s 接在某個東西的後方 2. 這個 method 會在 s 後面加上 footer 請看: ``` public void AppendFooter(StringBuilder report){ ... } ``` 我們需要進去看 method 才能消除疑慮 但如果是這樣命名 ``` report.AppendFooter(); ``` 就能一目瞭然這邊想幹嘛了 同樣地,錯誤處理也應該清楚表達意圖。 使用 exception 來呈現 error handling,不僅能讓錯誤的情況從正常的主程式抽離出來,還能讓程式碼變得更簡化 #### 不要害怕長名字,害怕的是不清楚 在實務上,長而清楚的命名,遠比短而模糊的名稱好,也比長而清楚的註解好。