一段程式今天能正常執行,不代表它就是好程式碼。
真正讓團隊痛苦的,往往不是「完全不能跑」,而是另一種狀態:功能還能運作,但每次要改一點東西,都要先花很久猜原作者到底在做什麼。
變數名稱看不懂、函式幾百行、同一段邏輯複製四次、錯誤處理散落各處、註解和實際程式已經對不上。這些問題不一定今天就造成故障,卻會一點一點提高每次修改的成本。
Robert C. Martin 在《Clean Code》中,把這類問題整理成一套可讀性與可維護性的實務原則。這本書影響很大,也引起不少討論。最值得學的不是把每一條建議當成硬性規定,而是理解它背後一直在問的同一件事:
這段程式,下一個人能不能快速看懂,而且安全地改?
Clean Code 為什麼會成為工程團隊常談的主題?
寫程式時,最容易把注意力放在「功能完成了沒有」。
但一個會持續維護的系統,大部分時間不是第一次寫,而是在讀舊程式、找問題、修改需求、修 Bug、增加測試與做重構。
只要專案活得夠久,就會開始出現理解成本。
今天為了趕功能,變數叫做 data1、temp、result;明天另一個人接手,就要重新推理這些名字代表什麼。今天把所有判斷塞在一個大函式裡很快,三個月後有人要加一個例外條件,卻不敢動,因為不知道會不會破壞其他流程。
Clean Code 處理的,就是這種「短期省幾分鐘,長期多花幾小時」的問題。
第一個原則:名字要讓人知道這個東西為什麼存在
命名是最便宜,也最容易低估的設計工具。
如果一個變數叫 d,你必須往前往後找,才知道它代表「經過的天數」。如果名稱直接寫成 elapsedDays,理解成本立刻下降。
一個好名稱通常要回答幾件事:它代表什麼、為什麼存在、會被怎麼使用。
例如布林值如果叫 flag,你很難知道 true 到底代表什麼;改成 isActive、hasPermission、canEdit,程式讀起來就更接近自然語言。
函式名稱也是一樣。
calculateMonthlyRevenue() 比 calc() 更容易理解;sendPaymentReminder() 比 process() 更具體。
Clean Code 很重視「讓名稱揭露意圖」。這不是為了漂亮,而是因為讀程式的人不應該一直靠猜。
第二個原則:函式應該聚焦在一個清楚的責任
長函式最大的問題,不只是行數多,而是它通常混了很多不同層次的工作。
例如一個 checkout() 同時做資料驗證、計算金額、建立訂單、扣款、寄 Email、更新會員資格、寫 Log。當任何一個流程要改時,整段都可能受到影響。
比較容易理解的方式,是把不同意圖拆成能被命名的步驟。
validateOrder()
calculateTotal()
chargePayment()
activateMembership()
sendReceipt()
這些名稱本身就開始形成一段可閱讀的流程。
但「函式要小」不能被誤解成每個函式都必須四行、五行。真正重要的是一個函式裡不要混入太多責任與不同抽象層次。
如果為了追求短函式,反而把一段很清楚的邏輯切成十幾個必須跳來跳去才能理解的小方法,可讀性也可能變差。
第三個原則:不要讓參數偷偷改變函式的意思
函式參數愈多,使用者愈難一次理解。
特別是布林參數,很容易變成警訊。
例如 renderPage(true) 到底代表什麼?如果 true 代表列印版,false 代表螢幕版,那呼叫端光看程式並不清楚。
拆成 renderForPrint() 和 renderForScreen(),通常更容易理解。
很多參數也可能代表這些資料其實應該組成一個有意義的物件。例如 startDate、endDate、timezone 如果總是一起出現,可以考慮用 DateRange 或 Period 之類的物件表達這個概念。
重點不是「三個參數就一定錯」,而是參數數量開始增加時,要檢查函式是不是正在承擔太多事情。
第四個原則:重複程式碼會讓同一個規則慢慢長成不同版本
複製貼上很方便,所以重複程式碼特別容易出現。
真正的問題不是多了幾行,而是「同一份知識」開始散落。
假設會員期限的計算邏輯,在結帳、續約、後台手動開通三個地方各寫一份。半年後規則改了,如果只改其中兩處,就會出現同一個商業規則三種答案。
這才是重複最危險的地方。
可以共用的規則應該集中到一個清楚的位置,讓未來修改只需要改一次。
但也不能看到兩段長得像,就急著抽成共用函式。真正值得消除的是「相同知識的重複」,不是剛好長得相似的程式。
第五個原則:註解應該解釋原因,不要替糟糕命名補漏洞
Clean Code 對註解的態度一直很嚴格。
如果程式旁邊寫:
// 檢查使用者是否符合資格
下一行卻是一大串看不懂的條件,那比較好的做法通常不是增加更多註解,而是把條件抽成 isEligible()。
因為程式會被修改,註解卻很容易忘記一起更新。當註解和程式碼不一致時,錯誤的註解比沒有註解更危險。
但這不代表「所有註解都不好」。
有些資訊無法單靠程式本身說清楚,例如為什麼必須配合某個外部協定、為什麼看似多餘的等待不能移除、為什麼某個商業例外存在。這些背景屬於「為什麼」,而不是「程式正在做什麼」。
Robert C. Martin 後來也公開寫過,有些複雜概念確實需要註解輔助。比較實際的判斷不是「有沒有註解」,而是這段註解是否提供程式本身無法清楚表達的重要背景。
第六個原則:錯誤處理不要把主要流程淹沒
錯誤處理很容易讓真正的商業邏輯消失。
一個函式如果每走一步就塞入一層 if error、null check、return code,最後最重要的正常流程反而看不見。
Clean Code 主張把錯誤處理視為獨立責任,讓主要路徑保持清楚。
不同程式語言有不同工具。有些語言大量使用 Exception,有些語言偏好 Result、Option 或明確的錯誤值。真正值得保留的原則,是讓呼叫者清楚知道失敗可能怎麼發生,而且不要偷偷吞掉錯誤。
錯誤訊息也應該有足夠上下文。
「Save failed」幾乎沒有幫助;如果能知道哪一個操作、哪一個物件、哪個階段失敗,排查速度會差很多。
不要把「不能回傳 null」當成所有語言的絕對規則
《Clean Code》很反對到處回傳 null,因為這會迫使每個呼叫者持續做 null check。
這個問題確實存在,但今天不同語言已經有不同解法。
有些語言使用 Optional 或 Option,有些使用型別系統區分 nullable 與 non-null,有些情況回傳空集合比 null 更自然。
所以真正值得保留的不是「任何情況都禁止 null」,而是不要讓「沒有值」變成一個模糊、容易被忘記處理的狀態。
可讀性與安全性才是目的。
類別的責任要清楚,不要什麼都放進 Manager
當一個類別愈長愈大,名稱常會開始變成 Manager、Helper、Utils、Service。
這些名字不一定有問題,但有時代表這個類別已經沒有清楚責任。
例如 CourseManager 同時處理課程建立、付款、Email、權限、報表、圖片上傳。當任何人碰到課程功能,就只能繼續把程式塞進同一個類別。
最後它變成所有功能的交叉路口。
比較好的判斷方式,是問:這個類別會因為幾種不同理由而被修改?
如果行銷規則改了要改它、付款規則改了也要改它、Email 格式改了還是要改它,通常就代表責任混在一起。
Code Smell 不是錯誤,而是提醒你「這裡值得看一下」
程式異味(code smell)不是 Bug。
它比較像警示訊號:這段程式可能正在累積設計問題。
常見例子包括:
函式過長、參數過多、重複邏輯、Magic Number、死程式、Feature Envy、巨大類別、過多旗標參數、同一個修改要散落改很多地方。
看到 smell 並不代表一定要立刻重構。
例如一段穩定、很少變動、也沒有造成理解困難的舊程式,重構它的優先級可能比一個每天都在改的核心模組低很多。
Clean Code 最適合拿來提高判斷力,不適合變成機械式打勾表。
測試程式也需要可讀性
很多團隊很在意正式程式,卻把測試寫得很亂。
結果幾個月後,測試本身反而變成修改障礙。
好的測試應該讓人一眼看懂情境、執行動作與預期結果。
測試名稱如果只是 test1、test2,失敗時幾乎沒有資訊;像 shouldRejectExpiredToken 這種名稱,會直接告訴你哪一個行為壞了。
測試也不應高度依賴時間、網路、外部 API 或不穩定資料。否則測試一下通過、一下失敗,團隊最後只會學會忽略它。
乾淨的測試,本身就是最接近實際行為的文件之一。
Boy Scout Rule:不要等到「大重構」才整理
Clean Code 很常提到童子軍原則(Boy Scout Rule):離開營地時,讓它比你來的時候更乾淨。
放到程式開發,就是每次碰到一小段程式時,如果能安全地改善一點,就順手改善。
可能只是改一個名稱、刪掉一段死程式、補一個測試、抽出重複邏輯。
這種做法的好處,是不用等到程式爛到完全不能動時,再啟動一次昂貴的大整理。
但前提仍然是要有安全網。沒有測試保護時,所謂「順手整理」也可能順手改壞。
如果你面對的是已經很慢、很亂、很難動的既有系統,可以先延伸閱讀 系統愈做愈慢、愈改愈亂?架構最佳化應該先從哪裡開始。
Clean Code 與 Clean Architecture 解決的不是同一層問題
Clean Code 比較接近函式、類別、命名、錯誤處理與測試的程式碼層級。
Clean Architecture 則處理模組、依賴方向、商業規則與外部技術邊界。
你可以有一段命名非常漂亮、函式非常短的程式,卻仍然把商業邏輯和資料庫綁死;也可以有架構邊界很清楚,但每個模組內部仍然難讀。
兩者需要一起看,但不能互相取代。
如果要理解依賴方向與框架邊界,可以看 Clean Architecture:為什麼商業邏輯不該被框架與資料庫綁死。
Clean Code 最有爭議的地方:規則不能脫離情境
Clean Code 影響很大,但它不是沒有爭議。
John Ousterhout 在《A Philosophy of Software Design》第二版中特別加入與 Clean Code 的比較,明確指出雙方在函式長度與註解角色上有不同看法。
其中一個爭議,就是「函式愈短愈好」如果用得太機械,可能造成過度切分。
當一段邏輯本來可以順著讀完,卻被拆成大量很小的函式,讀者反而必須不停跳轉檔案與方法,才能拼出全貌。
另一個爭議是註解。
Clean Code 傾向認為程式本身應盡量自我說明,但另一派會強調:好的註解可以描述設計意圖、取捨、限制與外部背景,這些資訊不是靠漂亮命名就能完整表達。
這些爭論反而提醒我們一件重要的事:程式品質不是靠行數、註解數或函式數量評分。
真正的問題始終是,這樣的設計是否讓人更容易理解與修改。
把 Clean Code 用在 AI 寫程式,反而更重要
現在使用 AI 產生程式碼,速度比以前快很多。
但「生成得快」不等於「未來好維護」。
AI 很容易產出可以執行、但存在重複、命名普通、錯誤處理不一致、抽象層級混雜的程式。
如果只是一次性原型,這不一定是大問題;但如果程式要進正式環境、持續修改,就不能只看它今天能不能跑。
至少要再檢查:名稱有沒有表達意圖、函式是否混太多責任、商業規則是否重複、錯誤有沒有被吞掉、重要行為是否有測試。
AI 可以降低「寫出第一版」的成本,但閱讀、判斷與維護的責任沒有消失。
什麼才算真正乾淨的程式碼?
可以先不要追求抽象的「優雅」,改問幾個實際問題。
一個新加入的人,能不能快速理解主要流程?
名稱是否足以表達用途,不需要一直猜?
一個需求變更,通常能不能集中在少數相關位置修改?
錯誤發生時,是否容易知道哪裡出了問題?
重要商業行為有沒有測試保護?
如果明天要改這段程式,團隊是有信心,還是大家都說「不要碰它」?
最後這個問題往往最誠實。
Clean Code 真正的價值,不是讓程式碼看起來漂亮,而是降低下一次改動需要付出的理解成本與風險。
常見問題
Clean Code 是不是代表函式一定要很短?
不是。短函式通常比較容易聚焦,但真正目標是單一責任與清楚的抽象層次。為了追求行數而過度切分,反而可能降低可讀性。
註解愈少就代表程式碼愈乾淨嗎?
不一定。重複程式本身意思的註解通常價值低,但用來說明設計原因、外部限制或不直覺取捨的註解仍然很重要。
Code Smell 出現就一定要立刻重構嗎?
不用。Smell 是提醒你檢查,不是自動判定錯誤。要看這段程式的變動頻率、風險、測試狀態與實際維護成本,再決定優先級。
Clean Code 適合所有程式語言嗎?
很多核心原則可以跨語言使用,例如命名清楚、責任聚焦、避免重複與保持測試可讀;但 Exception、null、型別與函式設計等具體做法,仍要配合語言特性。
AI 產生的程式能跑,還需要做 Clean Code 檢查嗎?
如果只是短期原型,可以先求快速驗證;如果要進正式環境或長期維護,就應該檢查可讀性、重複、錯誤處理、測試與責任邊界。能跑只是第一關。
參考資料
Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship.
Robert C. Martin. The Clean Coder: A Code of Conduct for Professional Programmers.
Robert C. Martin. “Necessary Comments.” The Clean Code Blog.
John Ousterhout. A Philosophy of Software Design, Second Edition.
Martin Fowler. Refactoring: Improving the Design of Existing Code.
