🎯 什麼情境該想到我
當你發現「同一件事在多個地方重複」、改一處就得同步改很多地方(還常漏改)時。
⚙️ 怎麼用
系統中每一項知識,都應該有單一、明確、權威的表述。
- 辨識重複的「知識」(不只是程式碼,還有邏輯、設定、文件、schema)。
- 抽取到單一來源:共用函式/模組/常數/設定;讓其他地方引用它。
- 改動只需改一處 → 消除「漏改造成不一致」的整類 bug。
- 注意「假重複」:長得像但會各自獨立演變的,別強行合併(否則耦合出問題)。
★ 「重複的知識」vs「長得像的程式碼」:一個判斷法
問:如果其中一處因為需求改變而必須改,另一處也必須跟著改嗎?
| 答案 | 意義 | 動作 |
|---|---|---|
| 必須,否則就是 bug | 真重複:同一份知識有兩個副本 | 合併成單一來源 |
| 不一定,它們可能各自演變 | 假重複:只是現在碰巧長得像 | 保持分開 |
// 看起來重複,其實是兩份不同的知識
func validateSignup(u User) error { if len(u.Name) > 50 { … } }
func validateShipping(a Addr) error { if len(a.Name) > 50 { … } }使用者名稱上限和收件人姓名上限碰巧都是 50。合併成 validateName(s) 之後,哪天物流商把收件人欄位改成 35 字——你要嘛改壞註冊流程,要嘛再拆回來。錯誤的抽象比重複更貴:拆開一個被多處依賴的抽象,比合併兩份重複難得多。
★ 三次法則(Rule of Three)
- 第一次:寫。
- 第二次:複製,但記下來。
- 第三次:這時你已經看到三個實例,知道哪些部分真的一樣、哪些會變——這才是提煉抽象的時機。
只看兩個例子就抽象,抽出來的參數與分支常常猜錯方向。
★ DRY 不只管程式碼:最有價值的是「跨格式」的重複
最常出事的重複,不是兩個函式長得像,而是同一份知識以不同格式存在好幾處:
| 重複的知識 | 典型的多個副本 | 單一來源的做法 |
|---|---|---|
| 資料結構 | DB schema、Go struct、API 文件、前端 TS 型別 | 從一處產生其他:sqlc(SQL → Go)、OpenAPI/protobuf → Go 與 TS |
| 列舉值 | Go 常數、DB CHECK 約束、前端下拉選單 | 常數定義一處,go generate + stringer 產生字串方法 |
| 設定預設值 | 程式碼、README、Helm chart | 程式碼是權威,文件從程式碼產生或只寫「見 X」 |
| 業務規則 | 前端驗證、後端驗證 | 後端是權威;前端驗證只是 UX,不重複定義規則的細節(或共用 JSON Schema) |
產生出來的程式碼要 commit 並在 CI 檢查是否最新(產生後 git diff --exit-code),不然「單一來源」只是理論。
★ 什麼地方刻意不 DRY
- 測試程式碼:測試偏好 DAMP(Descriptive And Meaningful Phrases)——每個測試案例讀起來完整、自足,比共用一堆 helper 更重要。測試失敗時,讀者應該不用跳三個檔案才知道準備了什麼資料。
- 微服務之間:兩個服務各自定義同樣的 DTO,比共用一個「common」套件讓它們綁在一起好。共用程式庫會變成部署耦合(改一個欄位,所有服務要一起升級)。
- Go 慣用語:Go 社群有句話「一點複製勝過一點依賴」(a little copying is better than a little dependency)——為了一個 10 行的函式引入一個外部模組,通常不值得。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
-
DRY 是消除「知識重複」,不是消除「所有相似的字元」;過度 DRY 會造成錯誤耦合。
-
過度 DRY 的症狀:一個共用函式有 4 個 bool 參數、裡面一堆
if caller == X。這代表它原本是好幾個不同的東西,被硬湊在一起——拆回去(行內函式 之後再各自整理)。