🎯 什麼情境該想到我

當你發現「同一件事在多個地方重複」、改一處就得同步改很多地方(還常漏改)時。

⚙️ 怎麼用

系統中每一項知識,都應該有單一、明確、權威的表述。

  1. 辨識重複的「知識」(不只是程式碼,還有邏輯、設定、文件、schema)。
  2. 抽取到單一來源:共用函式/模組/常數/設定;讓其他地方引用它。
  3. 改動只需改一處 → 消除「漏改造成不一致」的整類 bug。
  4. 注意「假重複」:長得像但會各自獨立演變的,別強行合併(否則耦合出問題)。

★ 「重複的知識」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。這代表它原本是好幾個不同的東西,被硬湊在一起——拆回去(行內函式 之後再各自整理)。

🔗 相關工具

  • 工具-提煉函式 —— 發現重複之後最常用的消除手法,DRY 說「該做」,這張說「怎麼做」
  • 工具-程式碼壞味道 —— 反過來用:從壞味道清單(尤其 Duplicated Code)反推哪裡違反了 DRY
  • 工具-封裝變化點 —— 互補視角:DRY 消除的是「重複」,這張隔離的是「會變的部分」,兩者常一起出手