🎯 什麼情境該想到我

當你正在寫程式,想讓它保持容易理解、容易修改的時候。

章導言引 John Dryden:「任何一个笨蛋都能够让事情变得越来越笨重、越来越复杂、越来越极端。需要天才的指点以及许多的勇气,才能让事情向相反的方向发展。」書的論點是:新專案的程式碼一開始都很好懂,隨著推進逐漸變成龐然大物;最簡單的方法是每天付出一點小努力,避免程式碼腐爛。

習慣良策站內的深入版本
程式碼要清晰表達意圖寫清晰的而不是討巧的程式碼工具-簡單清楚至上
用程式碼溝通用註解說明意圖與約束,註解不能取代好程式碼工具-註解是必要之惡
動態評估取捨性能足夠了,就把注意力放在其他因素上工具-簡單清楚至上
增量式編程在很短的編輯/建置/測試循環中寫程式工具-小步重構
保持簡單開發可以工作的、最簡單的解決方案工具-管理複雜度
編寫內聚的程式碼讓類別的功能盡量集中,讓元件盡量小工具-函式短小且單一職責
告知不要詢問告訴它做什麼,然後管好自己的職責依戀情結
根據契約進行替換多用委派,少用繼承工具-Go介面小而隱式

出處:Subramaniam & Hunt《高效程式設計師的 45 個習慣》第 6 章。書中範例是 C# 與 Java;這裡加註 Go 的對應寫法。引文保留原書簡體字。

⚙️ 怎麼用(逐項)

程式碼要清晰表達意圖

Program Intently and Expressively 什麼時候會冒出來:程式能跑但過幾個月自己看不懂/寫了很聰明但別人讀不懂的寫法/同事看我的程式要問很久/想寫給人看而不只是給機器跑

魔鬼的誘惑:「让人觉得聪明更加重要。别人给你钱是因为你脑子好使。」

良策:「要编写清晰的而不是讨巧的代码。向代码阅读者明确表明你的意图。可读性差的代码一点都不聪明。」

書中重點:

  • 引 C.A.R. Hoare:「设计软件有两种方式。一种是设计得尽量简单,并且明显没有缺陷。另一种方式是设计得尽量复杂,并且没有明显的缺陷。」
  • 程式碼被讀的次數遠多於被寫的次數,「代码清晰程度的优先级应该排在执行效率之前」。
  • 咖啡杯的例子:coffeeshop.PlaceOrder(2) 的 2 是什麼意思?加註解 /* large cup */ 是在替爛程式碼補破洞;定義列舉 CoffeeCupSize.Large 才是正解。
  • 位移的例子:val << 1 是不必要又危險的效能最佳化,val * 2 更清楚,而且對某些編譯器可能更快。
  • 鎖的例子:在 MakeCoffee() 裡 lock(this) 有兩個問題——鎖的範圍太大(如果 GetWater() 也鎖 this,就不能同時煮咖啡和取熱水,看不出這是不是刻意的),而且鎖住了一個外部可見的物件(別處的程式碼也可能鎖它,導致效率變差甚至死結)。改用一個私有的鎖物件,同時解決兩個問題,也表明了意圖。
  • 書中把這叫做 PIE 原則:Program Intently and Expressively(意圖清楚、表達明確地編程)。

Go 的對應:

  • 列舉:type CupSize int 加上 const ( Small CupSize = iota; Medium; Large )。
  • 鎖:Go 的慣例本來就是把 sync.Mutex 放成不匯出的欄位(mu sync.Mutex),外部無法鎖它——書中的建議在 Go 是預設做法。

切身感受:「应该让自己或团队的其他任何人,可以读懂自己一年前写的代码,而且只读一遍就知道它的运行机制。」

平衡的藝術:

  • 把程式碼當成一個不知道何時會被打開的時間膠囊。
  • 有意圖地編程不是建立更多類別或型別的理由,也不是過度抽象的藉口。
  • 耦合方式要符合實際:用雜湊表鬆散地連接本來就鬆散的元件;緊密相關的元件用雜湊表連接,反而隱藏了意圖。

用程式碼溝通

Communicate in Code 什麼時候會冒出來:想寫一堆註解解釋難懂的程式/註解跟程式碼已經對不上了/在猶豫這裡要不要加說明/想靠命名讓程式自己說話

魔鬼的誘惑:「精确地解释代码做了什么,每行代码都要加注释。不用管为什么要这样编码。」

良策:「用注释沟通。使用细心选择的、有意义的命名。用注释描述代码意图和约束。注释不能替代优秀的代码。」(這一節標題是「用程式碼溝通」,良策的第一句原文卻是「用註解溝通」;旁註:不要用註解來掩蓋你的程式碼。)

書中重點:

  • 大部分的文件都和程式碼脫節,違反 DRY,而且會誤導人。
  • 命名很重要:好名字傳遞大量正確的資訊,爛名字傳遞錯誤的資訊——例如一個叫 readAccount() 的方法,實際上在往硬碟寫入地址資料。
  • 短不等於神秘:i 當迴圈索引、s 當字串是慣用法;但拿 s 當迴圈索引、或叫它 indexvar,都不好。
  • //Constructor 這種 IDE 自動插入的註解是雜訊,而且會過時。
  • 每個方法的註解應該說明四件事:
    1. 目的:為什麼需要這個方法?
    2. 需求(前置條件):呼叫前必須滿足什麼?
    3. 承諾(後置條件):呼叫後保證什麼?
    4. 例外:會出什麼錯?
  • 「如果方法只在日全食時才能正常工作,先知道比较好。」

切身感受:「注释就像是可以帮助你的好朋友,可以先阅读注释,然后快速浏览代码,从而完全理解它做了什么,以及为什么这样做。」

平衡的藝術:

  • 巴斯卡說他沒時間寫短信,只好寫長信——請花時間寫簡明的註解。
  • 程式碼能傳達意圖的地方,不要用註解。
  • 註解要說明為什麼,不是做了什麼。
  • 重寫方法時,保留描述原方法意圖與約束的註解。

在今天的樣子:這一條和《程式碼整潔之道》的觀點一致,完整的判準見 工具-註解是必要之惡;Go 的文件註解慣例見 工具-Go命名慣例。書中的「四件事」在 Go 裡通常寫成一段 doc comment,錯誤用哨兵錯誤或型別明確表示。

動態評估取捨

Actively Evaluate Trade-offs 什麼時候會冒出來:一心想追求某個最完美的做法/在效能跟開發速度之間拉扯/團隊為了選哪個方案僵在那/說不出取捨的判斷依據是什麼

魔鬼的誘惑:「性能、生产力、优雅、成本以及上市时间……每一项都必须达到最理想状态。」

良策:「动态评估权衡。考虑性能、便利性、生产力、成本和上市时间。如果性能表现足够了,就将注意力放在其他因素上。不要为了感觉上的性能提升或者设计的优雅,而将设计复杂化。」(旁註:沒有適用所有狀況的最佳方案)

書中重點:

  • 量過才知道:有人說 Web Service 比另一種遠端呼叫方式慢,但實際效能測試顯示,XML 文件很小,建立和解析的時間相對於整體回應時間可以忽略;而 Web Service 更容易開發,日後接第三方服務也方便。
  • 側欄〈過猶不及〉:一位客戶非常相信可設定性,應用程式有大約 10,000 個可設定的變數,新增程式碼變得異常困難——而實際上只有 19 個客戶,預計不會超過 50 個。
  • 不照書上的 OO 方式:從資料庫取資料、建立物件、傳給 UI、再從物件拆出資料顯示——不如讓資料層直接回傳資料集給 UI 顯示。「不按书上说的OO方式来做,可以减少投入」,關鍵是多留個心眼。
  • 效能何時「足夠好」,由客戶決定(見 讓客戶做決定)。

切身感受:「即使不能面面俱到,你也应该觉得已经得到了最重要的东西——客户认为有价值的特性。」

平衡的藝術:

  • 現在投入額外資源是為了將來可能的好處,要確認一定有回報(大部分情況不會有)。
  • 真正的高效能系統,從設計一開始就要朝這個方向。
  • 「过早的优化是万恶之源。」(Knuth)
  • 過去用過的解法不一定適用於目前的問題。

在今天的樣子:「量過才知道」在 Go 裡幾乎零成本:go test -bench 加 pprof(見 工具-簡單清楚至上、工具-一題多解與最佳化思維)。「10,000 個可設定變數」是 誇誇其談通用性 的極端例子。

增量式編程

Program Incrementally 什麼時候會冒出來:想一口氣寫完一大塊再開始測/已經好幾小時沒編譯過了/一次改太多壞掉不知道是哪一步/想養成小步前進的開發節奏

魔鬼的誘惑:「真正的程序员写起代码来,一干就是几个小时……不要停下来去编译你的代码,只要一直往下写就好了!」

良策:「在很短的编辑/构建/测试循环中编写代码。这要比花费长时间仅仅做编写代码的工作好得多。」

書中重點:

  • 開車的比喻(書中註明出自 Kent Beck):長途開車不能雙手固定方向盤、油門踩到底開好幾個小時,要不斷修正方向、看路況。
  • 沒有測試確認沒問題,就不要連續寫好幾個小時——甚至好幾分鐘。
  • 增量式編程會傾向產生更小的方法、更內聚的類別。「关键在于持续做一些细小而有用的事情,而不是做一段长时间的编程或重构。」

切身感受:「在写了几行代码之后,你会迫切地希望进行一次构建/测试循环。在没有得到反馈时,你不想走得太远。」

平衡的藝術:

  • 建置與測試循環太久,你就不會常跑——要保證測試能快速執行。
  • 在編譯和測試執行時,停下來想一想、暫時離開細節,是確保方向正確的好方法。
  • 要休息就好好休息,離開鍵盤。
  • 像重構程式碼一樣重構測試。

在今天的樣子:Go 的編譯與測試速度讓這個循環可以非常短:go test ./pkg/ -run TestX 通常在一秒內回饋。具體的節奏見 工具-小步重構 與 工具-紅綠重構循環。

保持簡單

Keep It Simple 什麼時候會冒出來:想預留一堆以後可能用到的彈性/為想像中的需求先做通用框架/設計越弄越複雜結果都用不到/在猶豫要不要先加設定檔跟開關

魔鬼的誘惑:「随便哪个笨蛋都可以编写出简单、优雅的软件。通过编写史上最复杂的程序,你将会得到美誉和认可。」

良策:「开发可以工作的、最简单的解决方案。除非有不可辩驳的原因,否则不要使用模式、原则和高难度技术之类的东西。」(旁註:簡單不是簡陋)

書中重點:

  • Andy 認識一個迷戀設計模式的人:寫一個只有幾百行的程式,在被發現之前,已經把 GoF 書中的 17 個模式都用上了。
  • 許多開發者把投入的努力與程式的複雜度混為一談,以複雜為榮;其實應該以簡單可用的設計為榮。
  • 簡單不是簡陋:簡單的方案通常更難得到。像廚師收汁——從大量的酒和食材開始,最後得到最濃縮的精華醬汁。
  • 側欄〈怎樣才算優雅〉:優雅的程式碼一眼就看得出用途而且簡潔,容易理解,但很難創造。

切身感受:「当你觉得所编写的代码中没有一行是多余的,并且仍能交付全部的功能时,这种感觉就对了。」

平衡的藝術:

  • 程式碼幾乎總能再精煉,但到某個程度之後,改進不再帶來實質的好處,就該停下來。
  • 強行讓程式碼優雅,和過早最佳化一樣有害。
  • 為了簡單而在功能上妥協,就是過度簡化。
  • 太過簡潔不等於簡單,那樣無法達到溝通的目的。
  • 一個人認為簡單的東西,對另一個人可能很複雜。

在今天的樣子:Go 的設計哲學本身就是這一條(見 工具-簡單清楚至上、工具-Go可讀性五原則)。17 個模式的故事,也是 工具-管理複雜度 所說「附屬複雜度」的典型來源。

編寫內聚的程式碼

Write Cohesive Code 什麼時候會冒出來:一個類別什麼事情都在管/檔案越長越大什麼都往裡塞/改一個小功能要動好幾個模組/想讓每個模組只負責一件事

魔鬼的誘惑:「看看IDE中现在打开的是哪个类,直接加进去就是了。如果所有的代码都在一个类或组件里面,要找起来是很方便的。」

良策:「让类的功能尽量集中,让组件尽量小。要避免创建很大的类或组件,也不要创建无所不包的大杂烩类。」

書中重點:

  • 抽屜的比喻:所有衣服丟進同一個抽屜,找襪子要翻遍;分門別類,一打開就找到。
  • 1866 年的美國專利:「可變換的鋼琴、睡椅和五斗櫃」——用睡椅和五斗櫃「填滿鋼琴下未被使用的空間」。毫無內聚,難以維護。
  • Venkat 看過一個 20 頁的網頁應用程式,每一頁的 HTML 裡夾雜大量腳本並內嵌 SQL——資料表結構稍微改一下,所有頁面都要改好幾處。
  • 單一職責原則:一個模組應該只有一個改變的理由。
  • 重用與發布等價原則:重用的粒度就是發布的粒度;套件越大,可重用性越差。

切身感受:「每个类或组件只做一件事,而且做得很好。bug很容易跟踪,代码也易于修改。」

平衡的藝術:

  • 可能拆得太碎而失去實用價值——你需要一隻襪子時,一盒棉線幫不上忙(書中稱為「義大利麵 OO」)。
  • 內聚良好的程式碼,改動的量會和需求變化成比例。書中註記:一位審閱者說他見過一個系統,在表單上加一個欄位需要 16 位成員和 6 位經理同意——這是內聚性很差的清楚訊號。

在今天的樣子:「加一個欄位要改很多地方」就是 霰彈式修改;什麼都塞進一個類別是 過大類別。Go 用套件作為內聚的主要邊界:一個套件一個清楚的職責,internal/ 限制可見範圍(見 工具-介面設計原則)。

告知不要詢問

Tell, Don’t Ask 什麼時候會冒出來:到處把別人的欄位撈出來自己判斷/一長串取值串接才拿到要的東西/邏輯散在呼叫端而不在資料擁有者身上/想把行為搬回資料所在的物件

魔鬼的誘惑:「不要相信其他的对象……从别人那里去拿你需要的信息,然后自己处理,自己决策。」

良策:「告知,不要询问。不要抢别的对象或是组件的工作。告诉它做什么,然后盯着你自己的职责就好了。」(旁註:將命令與查詢分開)

書中重點:

  • 引 Alec Sharp:「面向过程的代码取得信息,然后做出决策。面向对象的代码让别的对象去做事情。」
  • 送報童與錢包(David Bock 的故事):你讓送報童從你的後口袋掏出錢包、拿兩美元、再放回去——結果他開著一輛全新的捷豹揚長而去。送報童應該只告訴你「請付兩美元」,而不是去翻你的錢包、替你做決定。
  • 命令與查詢分離:「命令」可以改變物件的狀態;「查詢」只提供狀態,不修改外部可見的狀態(可以在內部計算或快取,但取得 X 不應該改變 Y)。沒有副作用的查詢,可以在測試、斷言、除錯器中自由呼叫。
  • 側欄〈小心副作用〉:「我們呼叫那個方法,是因為它的副作用」——這代表設計很脆弱,需要馬上重新設計。

切身感受:「告知,不要询问感觉起来就像你在发送消息,而不是调用函数。」

平衡的藝術:

  • 物件如果只是用來裝大量資料,這個做法值得懷疑;有時需要,但不如想像中頻繁。
  • 「命令」回傳資料以方便使用沒有問題。
  • 絕不允許看似無辜的「查詢」修改物件狀態。

在今天的樣子:

  • Go 的例子:與其 if acct.Balance() >= amt { acct.SetBalance(acct.Balance() - amt) }(取得資訊後替它做決定,而且有競爭條件),不如 err := acct.Withdraw(amt)——判斷與修改都在 Account 內部,也能用同一把鎖保護。
  • 命令與查詢分離在函式層級的討論見 工具-函式短小且單一職責;到處取別人資料的壞味道是 依戀情結。

根據契約進行替換

Substitute by Contract 什麼時候會冒出來:用繼承硬塞結果破壞了父類行為/子類別偷偷改掉原本的語意/在猶豫要用繼承還是組合/想確保換掉實作不會出事

魔鬼的誘惑:「深层次的继承是很棒的……不要担心你创建的新类会造成破坏,你的调用者可以改变他们的代码。」

良策:「通过替换代码来扩展系统。通过替换遵循接口契约的类,来添加并改进功能特性。要多使用委托而不是继承。」(旁註:is-a 關係用繼承;has-a 或 uses-a 關係用委派)

書中重點:

  • Liskov 替換原則:任何衍生類別的物件,都必須能替換基底類別的物件,而且使用者不需要知道差別。例如把 BasicUtils 換成 FasterUtils,呼叫方式完全一樣——完美的替換;但如果子類別把排序改成反序,就嚴重違反了原則。
  • 衍生類別的方法相對於基底類別,「应该不要求更多,不承诺更少」。
  • Java 自己也違反了這個原則:java.util.Stack 繼承自 java.util.Vector,把 Stack 傳給期待 Vector 的方法,元素可能以不符合預期的順序插入或刪除。
  • 如果衍生類別不能替換基底類別,就要問:為什麼要用繼承?如果只是為了重用程式碼,應該考慮委派。

切身感受:「你可以偷偷地替换组件代码到代码库中,而且其他代码对此一无所知,它们还获得了新的或改进后的功能。」

平衡的藝術:

  • 相對於繼承,委派更靈活、適應力更強。
  • 繼承不是魔鬼,只是長久以來被誤解了。
  • 不確定一個介面承諾了什麼、要求了什麼,就很難提供有意義的實作。

在今天的樣子——Go 本來就這樣設計:

  • Go 沒有繼承,只有嵌入(一種自動的委派)與介面。書中「多用委派」的建議,在 Go 是唯一的選項。
  • 「不要求更多,不承諾更少」在 Go 裡指的是介面的行為契約:例如 io.Reader 的文件規定:Read 可以回傳少於要求的位元組數;讀到部分資料後遇到錯誤時,可以在同一次呼叫同時回傳 n > 0 與錯誤;len(p) > 0 時不鼓勵回傳「0 位元組、沒有錯誤」。呼叫端則必須先處理 n 個位元組,再看錯誤(依 Go 1.26.4 io.Reader 文件,2026-09-25)。違反契約的實作,型別系統檢查不出來,只能靠文件與測試。
  • 隱式介面讓替換幾乎零成本(見 工具-Go介面小而隱式、工具-針對介面編程)。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 這一章的建議在 2006 年主要針對 Java 和 C#:在 Go 裡,有幾條(私有鎖、委派優於繼承)已經是語言的預設;有幾條(每個方法都寫前置/後置條件的 XML 註解)在 Go 社群不常見。帶走的是原則,不是語法。
  • 「保持簡單」和「清晰表達意圖」有時會衝突:多定義一個型別讓意圖更清楚,但也多了一個概念。判準是:讀者是否因此少了需要記在腦中的東西。

🔗 相關工具