進行程式碼審查時,總會遇到一個特別常見的場景。
為了修正一個功能進行搜尋,結果發現幾乎一模一樣的程式碼出現在三個地方。只修正一處,另外兩處就會繼續以 Bug 的形式留下來。
今天的主角是正面針對這個問題的原則:DRY。它就像上次介紹的 KISS 原則一樣,總是成對出現。
DRY 是「Don’t Repeat Yourself」的縮寫,意思是不要在系統中重複相同的知識。
這個概念由 Andy Hunt 和 Dave Thomas 整理於 1999 年出版的書籍 <The Pragmatic Programmer> 中,原始定義相當精準。
所有知識在系統中都應該只有一個、明確且具權威性的表示方式。
這裡值得注意的不是「程式碼」,而是「知識」這個詞。這就是今天主題的核心。
為什麼複製貼上程式碼會造成問題?
複製貼上本身並沒有錯,問題會在之後發生。
如果相同的邏輯存在於三個地方,需求變更時就必須找到並修改全部三處。只要漏掉一處,就會變成 Bug。
// 運費計算邏輯分散在三個檔案中
// Cart.swift
let fee = total >= 50000 ? 0 : 3000
// Checkout.swift
let shippingFee = totalPrice >= 50000 ? 0 : 3000
// OrderSummary.swift
let delivery = price >= 50000 ? 0 : 3000
如果某天收到「把免運門檻降到 3 萬韓元」的需求呢?你必須找遍三個檔案。搜尋關鍵字也各不相同,因此一定會漏掉其中一處。
// 將知識集中在一個地方
// Shipping.swift
enum ShippingPolicy {
static let freeShippingThreshold = 50000
static let fee = 3000
static func shippingFee(for total: Int) -> Int {
total >= freeShippingThreshold ? 0 : fee
}
}
現在政策變更時,只要修改一個地方即可。這就是 DRY。
重複的不是程式碼,而是「知識」
不過,很多人會在這裡失足。他們把 DRY 解讀成「把所有看起來相似的程式碼合併」。
DRY 所說的重複不是程式碼外觀的重複,而是知識,也就是商業規則的重複。
這個區分很重要,因為世界上存在只是碰巧相似的程式碼。
| 情況 | 算重複嗎? |
|---|---|
| 運費計算邏輯出現在三個地方 | 真正的重複(相同知識) |
| 會員註冊驗證與活動報名驗證碰巧相似 | 表面的重複(不同知識) |
| 常數 3000 分別出現在運費與點數累積門檻中 | 表面的重複(不同意義) |
會員註冊驗證與活動報名驗證目前都要求「姓名必填、電話號碼 11 碼」,所以看起來像是相同程式碼。但兩項規則會因不同原因而變更。若把它們合併,修改活動規則時就可能讓會員註冊故障。
草率的抽象化比重複更昂貴
因此在開發者社群中,常會聽到「寧可重複,也不要錯誤的抽象化(prefer duplication over the wrong abstraction)」這句話。這是知名 Ruby 開發者 Sandy Metz 說的。
硬是把兩段看似相似的程式碼合併,會發生以下情況。
- 建立共用函式
- 其中一方的需求變更,因此新增選項參數
- 另一方也變更,因此新增 if 分支
- 不知不覺間,變成一個有五個參數、沒有人看得懂的函式
到了這個地步,還不如保留兩份複製的程式碼。
因此實務上常使用「三次法則(Rule of Three)」。相同模式出現兩次時先觀察,第三次出現時再進行抽象化。重複大約三次後,就能看出它究竟是相同知識,還是純粹的巧合。
我的判斷標準
發現重複時,我會提出一個問題。
這兩段程式碼會因為相同的原因而變更嗎?
如果會因為相同原因變更,就是真正的重複,因此合併。如果會因為不同原因變更,即使外觀再相似,也維持原狀。
今天的總結
把 DRY 原則濃縮成重點,就是以下幾點。
- 重複的單位不是程式碼外觀,而是知識(商業規則)。
- 只合併會因為相同原因而變更的程式碼。只是碰巧相似的程式碼,就維持原狀。
- 如果沒有把握,就使用三次法則。等到第三次重複時再抽象化,也不算太晚。
如果 KISS 是「保持簡單」,DRY 就是「只把知識放在一個地方」。兩者最終都是為了降低變更成本。
應該有一段邏輯,你每次搜尋都會在三個地方找到。那就是今天的重構候選。

