Clean Code 讀後(一):表達層——命名、函式、註解、編排

《Clean Code》第 2–5 章讀後整理:閱讀寫作的 10:1 比例、命名作為意圖路標、函式的短小原則、註解的雙面刃、編排作為溝通工具——以及為什麼這些對 AI 時代的非工程師也重要。

13 min read 來自《Clean Code》

軟體工程師,顧名思義是撰寫軟體的人,而軟體是由程式碼所構成。 程式碼品質會影響閱讀的人,也會影響修改與維護的人。

什麼叫做品質?《Clean Code》這本書把程式碼整潔當作品質的核心指標:你產出的程式碼越整潔,就越具備軟體工程師的專業品質。

閱讀與撰寫的 10:1 比例

這本書從一個關鍵洞察出發:我們花在閱讀與撰寫程式碼的時間比例約為 10:1。所以提升可讀性,就是在提升整體開發效率。 即便在 AI 時代也是如此——AI 雖然讓撰寫時間變少,但維護、整理、以及任何需花腦力理解的時間,並不會因此減少太多。

對應到實踐,作者借用了童子軍的格言:「離開營地前,都要讓它比你抵達時更乾淨。」 每次修改時,不只要完成當下任務,也該順手清理周邊程式碼。久而久之,系統會越來越乾淨,而不是腐敗。

何謂乾淨的程式碼?Robert C. Martin(Uncle Bob)綜合多位軟體大師的觀點,歸納出六大特質:專注、俐落、精緻、倍受照顧、減少重複、直覺。 這本書有好幾個章節,我會分成四篇來分別討論。第一篇是表達層,也就是我們如何命名程式碼、如何撰寫函式、如何寫下註解,以及如何做編排。

命名:濃縮意義的路標

命名(Naming)非常重要,它影響程式碼的可讀性(readability)與可維護性(maintainability)。 好名稱應能清晰表達該變數、函式、類別的內容跟目的。名字在人類的溝通上,可以說是濃縮一個物件、或一個人所代表的意義,甚至會影響人的性格與發展。 我們也許不知不覺地用名字定義自己;被別人稱呼名字,等於不斷被給予一份身份認同,別人也會用這個名字來判斷你的價值。 就像我們喜歡把狗叫作來福,因為我們希望這隻狗來的時候,福氣、福分也跟著來。

同樣的道理放到程式碼,每個變數、函式、類別都被名稱賦予身份;名稱怎麼取,就決定了未來閱讀的人怎麼看待它、期待它的行為。

在程式碼中,命名的基本規則是讓名稱代表意圖(Use intention-revealing names)。如果名稱還需要註解(comments)補充說明,就代表它已經失敗。 這也是這本書不推薦撰寫註解的原因——如果程式碼夠簡潔、編排完整、責任分離、命名清楚,到底還有什麼情況需要註解?一定有,但那應該是特殊案例。

再來,名稱必須避免誤導。我們都是藉由抽象理解細節,所以抽象就像個路標:你誤導了大家,後面理解細節的方向都會出錯,錯誤理解還會被一路放大。

考量人類的認知習慣,名稱除了要能代表意圖、避免誤導,也該讓人容易朗讀,也就是避免贅字、避免雜訊——最簡潔、最直接地表達自己,並且易於搜尋。 我們應儘可能用常用詞彙溝通,這樣讀的人不會卡,各階層的人也能跟上你在說什麼。 艱深詞彙有它的應用情境,但軟體中艱深詞彙的意涵,也許更適合被拆解成數個簡單詞彙。

這樣的好處是減少雜訊與思維轉換。讀者不用先濾掉雜訊,也不用在腦中再做一道翻譯。 這其實很像服務業,多站在顧客的角度——而這些顧客就是未來看程式碼的人,也包含未來的自己。

而「顧客是誰」還可以再分一層。書中區分了 Solution Domain 與 Problem Domain:給工程師看的程式碼可以用 JobQueueOrderObserver 這些技術術語; 但寫到業務邏輯層,命名就應該回到領域專家熟悉的語言,例如 PremiumPolicyClaimVoucher。命名選用哪層的詞彙,其實就是在選擇你想對話的對象。

另個減少思維轉換的方法是「概念一致性」。同個概念在整個專案裡應用同個詞,不要讓 controllermanagerdriver 同時混雜代表類似的東西——讀者看到 DeviceManagerProtocolController,會不知道兩者本質的差別在哪。 選定一個詞、貫徹到底,這也是一種對未來讀者的善意。

函式:基本動詞的短小原則

函式(function)是系統中的基本動詞——呼叫一個函式,就代表觸發一個動作。 所以它應夠短小,且只把一件事做好。這符合單一職責原則(Single Responsibility Principle, SRP)。

再來,函式應維持單一層次的抽象,不該把高階邏輯與低階細節混在同段,細節應委託給其他函式完成。 函式的首則是短小,其次還是更短小——讓每層都單純,把細節留在更下層的函式裡。 這樣讀函式會非常放心:不用記住整體輪廓,可以層層深入,忘掉前面情境(context),只專心檢驗眼前這函式有沒有如預期動作。

第二個是引數(arguments),也就是執行這動作所需傳入的資訊。要傳入越多資訊才能完成的動作,就代表這動作越複雜。 而當我們要寫單元測試、驗證所有情況都符合預期時,這些引數組合會以二、三、甚至四的冪次方展開。

所以作者提到,如果一個函式需要三個以上的引數,那這些引數通常應該被包成一個類別(class)——把零散的參數歸成同一組概念。 另外,我們也應該避免輸出型引數。讀者預期引數是用來「傳入」資訊的,若引數被偷偷拿來「帶回」結果,會破壞這份預期,造成讀者的認知斷層。

再來是命令與查詢的分離(Command and Query Separation, CQS)。一個動作要麼做事、要麼回答問題,但一個函式不應同時做這兩件事。 這道理也跟與人互動一樣:如果一個人嘴上回了一串要你判斷的話、手上也把事情做了,你就得分頭檢驗他的「言」與「行」是否一致——而這種檢驗,本身就是筆不必要的腦力支出。

同樣地,我們也要警覺函式的副作用(side effects)。所謂副作用,就是函式表面上承諾只做一件事,背地裡卻偷偷改了全域狀態、初始化了不該初始化的東西。 這跟前面言行合一的類比是一致的:當一個人嘴上說 A、私下做 B,下次別人就不敢再單純信任他的話。函式也是一樣,一旦被發現有副作用,後續每次呼叫都得多花腦力去防範它。

三階段撰寫法:從草稿到定稿

要寫出高品質的基礎程式碼——也就是乾淨函式,很難一步到位。 寫程式碼的過程,本來就是把腦中想法持續鋪陳到程式碼裡,而腦中的想法往往複雜、互相關聯、甚至有疏漏,不可能一次到位。

我們可以把它分成三階段: 第一是草稿(draft):邏輯混亂、命名隨意、引數過多都很正常,目標就是先把功能快速兜出來。 第二是修飾(refining):開始調整結構,讓它越來越接近完美——分解長函式、優化命名、消除重複。 最後是定稿(final):交付一份可被信任的品質成果。這時程式碼應已經過同儕審查,夠格被合進主要的 Git 分支。

在這個過程中,單元測試就變得格外重要。它的職責是確保程式碼的結果符合預期——它不管你中間怎麼改,只在乎結果是否一致。 我們因此可以放手把程式碼修整成乾淨的樣子,不用擔心破壞它的邏輯。

註解:雙面刃的使用時機

前面提到寫乾淨程式碼,要有好命名與好函式。也因此,註解這種對機器完全沒有意義、只對人有意義的東西,本身其實是把雙面刃。 為什麼說雙面刃?一面是它能補充程式碼表達不足之處——它的存在往往就是彌補程式碼沒有辦法充分表達意圖;另一面是它無法跟著程式碼一起更新,過時註解反而會誤導讀者。

雖然如此,有些情況下註解仍然是必要的。例如附上法律資訊,可能是給非工程師看的;或者要傳達特定情境,因為程式碼通常只寫給機器看,跟對照文件之間會有隔閡,這時候把需要的資訊放進註解就有其必要。 再來,程式碼追求的是完美世界,但我們其實活在不完美的世界——常常需要做些暫時處理、或者次佳方案( workaround)。這時候,寫註解就能讓後來的人知道:這邊看起來不完美,但那是有原因脈落的。

我們也可以用註解告誡後果——有些程式碼可能是效能瓶頸(bottleneck)、可能容易出錯,標一句註解提醒後人額外小心。最後,如果我們沒辦法一次完成所有的事,可以先用 TODO 把待辦事項記錄下來。

那什麼是糟糕的註解呢?如前面所說,雜訊與多餘訊息對乾淨程式碼來說都是糟糕的。再來,程式碼會不斷被修改,但註解往往不會跟著動——大家都怕改註解、不確定要怎麼改,於是註解很容易過時、進而誤導讀者(misleading)。

還有一種情況,是很多被當成「歷史」放進註解的資訊,其實該交給 Git 這類版本控制系統。 每次 commit 都可以附上訊息(message),每個開發都可以用子分支(branch)來繳交,這些都能讓歷史資訊有更合適的容身之處。

好命名跟重構應該能取代註解需求。當想寫註解時,要停下來思考:我能不能透過修改變數名稱、或重構邏輯,讓程式碼自己說話?

編排:一致性的溝通工具

表達層的最後一塊,是編排。編排是程式設計師最重要的溝通工具——它追求一致性與流暢度,讓讀者看到同樣的風格,就能直覺知道它們承載著相同的意義。

在垂直編排上,我們可以學習報紙文章——由上而下展開內容,並用空白行距來隔離思緒。 報紙用版面框線、粗體標題、欄位區隔來分塊,但程式碼裡沒有這些視覺元素,能依靠的只有空白行距。 原則是:越靠近的應該越相關,離越遠的應該越不相關。

那要怎麼讓程式碼讀起來像順暢文章?答案是由上而下的敘事:先把高層抽象放在頂部,中間是主要邏輯,底層才是各種細節。 這就像我們讀文章或聽演講,先知道大綱,就能快速掌握方向、預期接下來會收到什麼。

這個由上而下的順序也有具體的實踐方式:呼叫者(caller)放在被呼叫者(callee)上方,讓讀者順著呼叫鏈往下讀,就能逐步看到細節。 同樣地,變數宣告應該緊跟在它被使用的地方,這樣讀者不需要往回翻找定義——這也呼應了垂直編排的另一個原則:相關的東西要靠近。

水平編排則要考慮人的眼睛移動範圍,所以一般建議 80 到 120 個字元就換行。 當然,最重要的還是大家要有同一套文化——畢竟各地的報紙、閱讀慣例(convention)都不同。一份程式碼要怎麼定義它的編排,就是要與團隊規則高度統一;這樣團隊成員看到某種格式時,就能直覺反應出它代表的意義。

寫作藝術與人文學科

講到這裡,對於不是程式設計師的人,我們可以從中得到什麼?我們會發現,撰寫程式碼其實是一門寫作藝術,也是一門人文學科。

所謂寫作藝術,就是清楚明確地表達、不讓讀者混亂。我們並不是在寫小說,也不是要在文字裡塞進一部電影的份量;只能單靠文字傳遞資訊的情境下,我們的目標就是像寫作一樣清楚、直接。

同時,它也是一門人文學科。人文學科談的就是「關於人的一切」。一份程式碼之所以能跑、能動,就代表它已經被機器理解;剩下的關卡就是「人類也要理解」。 而要被人理解,就得顧慮人的需要、人的歷史、以及人慣用的詞彙。也因此,越懂人文學科的人,越能理解軟體工程所追求的品質成果。

AI 時代的普遍價值

到了 AI 時代,任何領域的「交付成果」邊界都越來越模糊。我們要完成一件事,只需要知道目標是什麼、大致該怎麼做、什麼樣的成果算是可交付的品質。 所以理解如何寫出乾淨的程式碼,對使用 AI 的非軟體工程師也越來越重要。 因為背後的概念是相通的:當你的內容更清楚、能獨立存在,就代表它可以被檢驗、可以被重用;當你的邏輯順序是從抽象到具體,你就更能判斷哪些具體層面可以交給 AI 處理,並用抽象來檢驗 AI 所交出的成果。

人類的科技不斷在演進,但這些演進都需要很多基礎設施——程式碼要跑,背後當然要有晶片、CPU、記憶體等硬體的發展。 而我們要繼續追求人之為人的價值,就必須在「溝通、建構對世界的理解」這件事上反覆琢磨。讓自己的思緒清楚、讓傳遞出去的思緒清楚——我們才更容易跟其他人、跟 AI 溝通,產出更高品質的成果。 而這,也就是我們作為知識工作者該有的貢獻。

最後想引一句書中的話作結:

“Master programmers think of systems as stories to be told rather than programs to be written.”

厲害的程式設計師,把系統當作要被講述的故事,而不只是要被撰寫的程式。

程式碼是這樣,而我們做的所有溝通工作——寫文章、做簡報、跟 AI 對話——本質上也都是。

接下來的三篇

這篇談的是表達層(命名、函式、註解、編排),對應《Clean Code》的第 2–5 章。後續三篇預計會走:

  • 第二篇:結構與處理層——物件與資料結構、錯誤處理、邊界(章節 6–8)
  • 第三篇:測試與系統層——單元測試、類別、系統設計(章節 9–11)
  • 第四篇:重構實戰層——逐步精煉、壞味道、重構案例(章節 14–17)

Newsletter

訂閱「恩普拉氏」電子報

撰寫那些關於成長、突破、跨界思考的事。

前往 Substack 訂閱 →