第 1 章說過:軟體開發是迭代且增量的。系統的設計不斷在演化——成熟系統的設計,多半由「演化過程中的變更」決定,而非當初的構思。
前面章節談的是怎麼在初始設計階段擠出複雜性;本章談修改既有程式時,如何阻止複雜性悄悄爬回來。
保持戰略型思維#
第 3 章區分過:
- 戰術型:只求東西能動
- 戰略型:以好設計為主要目標
戰術型很快讓系統設計變糟。
想要好維護、好擴充的系統,「能跑」不是夠高的標準——必須優先考慮設計、思考戰略。
修改既有程式時的常見錯誤#
開發者改既有程式時的典型心態:
「能讓我達成目的的最小修改是什麼?」
理由可能是:對程式碼不熟,怕大改引入 bug。但結果是:
- 每次最小修改都引入幾個特殊情況、依賴或其他複雜性
- 系統設計每改一次就壞一點,問題逐步累積
戰略型修改的目標#
修改完之後,系統的結構應該與「一開始就帶著這個變更去設計」時的樣子一樣。
要做到這一點:
- 抗拒「快速修補」的誘惑
- 思考:在這個新需求下,目前的設計仍然是最佳的嗎?
- 不是 → 重構至最佳設計
- 這樣每次修改後,系統設計都變得更好
這也是投資心態:投入一點時間重構 → 系統更乾淨 → 開發提速 → 收回重構投入。
即使這次不需重構#
即使你的這次變更不需要重構,仍要主動尋找可以順手改善的設計缺陷。
任何時候改程式,都試著至少讓設計好一點——若沒讓設計變好,多半就是讓它變糟。
現實中的取捨#
商業開發中投資心態與現實有時衝突:
- 「正確」重構要 3 個月,「快補」只需 2 小時 → 截止日臨頭時可能不得不快補
- 重構會破壞與其他人 / 其他團隊的相容性 → 可能不可行
但盡量抗拒這些妥協。問自己:
「在現有限制下,我能做到的最乾淨的設計是什麼?」
- 也許有個「介於 3 個月和 2 小時之間」、幾天就能完成、且效果接近 3 個月版本的方案
- 真的當下來不及,請主管在這個截止日後排時間回頭做
每個開發組織都應該規劃花一小部分總工時做清理與重構——它長期一定回本。
維護註解:放近程式碼#
確保註解被更新的最佳方式:把註解放在它所描述的程式碼旁邊——這樣修程式時自然會看到。
距離越遠 → 越容易遺漏更新。
方法的介面註解該放哪?#
- 最佳位置:放在
.c/.cpp等實作檔案中、緊鄰方法主體上方 - 任何修改都會經過這段,註解就在眼前
C / C++ 的另一選擇:放在 .h#
可以放在 header 的方法宣告旁邊,但:
- 與實作距離遠 → 改方法時看不到 → 易過時
- 開啟另一個檔案找註解又要多步驟
有人主張介面註解該放 header 讓使用者不用看實作就能學 API。但其實使用者不該靠讀程式碼或 header——他們應該透過 Doxygen / Javadoc 等工具產生的文件、或 IDE 自動顯示的 tooltip 來學 API。
既然如此,註解應該放在「對改程式的開發者最方便」的地方。
實作註解的擺放#
不要把整個方法的註解全堆在方法頂端。
把每段註解推到「包住所有它描述的程式碼的最小範圍」內:
- 例如方法有三個主要階段 → 別在頂端寫一大段
- 在每個階段第一行程式碼上方各放一段註解
但頂端可以放一段「整體策略」說明:
// We proceed in three phases:
// Phase 1: Find feasible candidates
// Phase 2: Assign each candidate a score
// Phase 3: Choose the best, and remove it一般原則:註解離程式碼越遠,就應該越抽象——這樣它較不會被局部修改使其失效。
註解屬於程式碼,不屬於 commit log#
常見錯誤:把變更的詳細資訊寫進 commit message,但程式碼裡沒記錄。
問題:
- commit log 雖然能搜尋,但需要它的開發者通常不會想到去搜
- 即使搜了,要找到對的 commit 也很費力
寫 commit message 時問自己:
「未來的開發者會需要這份資訊嗎?」
會 → 把它寫進程式碼。
例如:commit 訊息描述了一個微妙問題(promoted 此次變更的原因)。如果沒寫進程式碼,後人可能不知不覺把改動還原,把 bug 重新引入。
commit message 中也可以放一份副本,但最重要的是寫進程式碼裡——因為這是開發者最可能看到的地方。
維護註解:避免重複#
若文件被重複,就更難找出與更新所有相關副本。
每個設計決定,恰好記錄一次。
自然中央位置#
例如:與某變數相關的 tricky 行為影響多處 → 把行為描述寫在該變數的宣告旁註解——這是讀者搜尋的自然位置。
沒有自然位置時#
- 建立
designNotes檔(13.7 節) - 或在最佳位置放主註解,其他相關位置寫短註解指向那裡:
// See the comment in xyz for an explanation of the code below.
引用過時時 → 症狀自我顯現(讀者去查找不到,可用 revision history 追溯)。
而若是文件被重複、且部分副本未更新,沒有任何訊號告訴開發者他正在用舊資訊。
不要在 A 模組重複描述 B 模組的設計#
例如:呼叫某方法之前,不要在呼叫旁註解「該方法做了什麼」。讀者要知道,去看那個方法的介面註解就好;好工具會自動顯示。
已存在於外部的文件,不要重複#
- 寫 HTTP 協定的類別 → 不要在程式碼內重述 HTTP 協定,加一行 URL 即可
- 已寫在 user manual 的指令 → 在介面註解寫:
// Implements the Foo command; see the user manual for details.
重點:讓讀者容易找到所有需要的文件——但不代表你得親手寫所有文件。
維護註解:審 diff#
提交前花幾分鐘掃一遍所有 diff,確認每個變更都已對應在文件中。
順便也能抓到:忘了移除的 debug 程式碼、未處理的 TODO 等。
高層註解最容易維護#
越高層、越抽象的註解,越容易維護。
它們不反映程式碼細節,所以小修改不會讓它們失效——只有整體行為變才會牽動它們。
當然,第 13 章提過:有些註解必須詳盡精確。但最有用的註解(不只是重複程式碼)通常也最易維護。