軟體設計

正確理解 DRY 原則:比程式碼重複更可怕的是錯誤的抽象化

進行程式碼審查時,總會遇到一個特別常見的場景。

閱讀 3 分鐘
正確理解 DRY 原則:比程式碼重複更可怕的是錯誤的抽象化 封面圖

進行程式碼審查時,總會遇到一個特別常見的場景。

為了修正一個功能進行搜尋,結果發現幾乎一模一樣的程式碼出現在三個地方。只修正一處,另外兩處就會繼續以 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 說的。

硬是把兩段看似相似的程式碼合併,會發生以下情況。

  1. 建立共用函式
  2. 其中一方的需求變更,因此新增選項參數
  3. 另一方也變更,因此新增 if 分支
  4. 不知不覺間,變成一個有五個參數、沒有人看得懂的函式

到了這個地步,還不如保留兩份複製的程式碼。

硬湊在一起的函式崩潰的四個階段
硬湊在一起的函式崩潰的四個階段

因此實務上常使用「三次法則(Rule of Three)」。相同模式出現兩次時先觀察,第三次出現時再進行抽象化。重複大約三次後,就能看出它究竟是相同知識,還是純粹的巧合。


我的判斷標準

發現重複時,我會提出一個問題。

這兩段程式碼會因為相同的原因而變更嗎?

如果會因為相同原因變更,就是真正的重複,因此合併。如果會因為不同原因變更,即使外觀再相似,也維持原狀。

合併前,只要問自己這一個問題就夠了
合併前,只要問自己這一個問題就夠了

今天的總結

把 DRY 原則濃縮成重點,就是以下幾點。

  • 重複的單位不是程式碼外觀,而是知識(商業規則)。
  • 只合併會因為相同原因而變更的程式碼。只是碰巧相似的程式碼,就維持原狀。
  • 如果沒有把握,就使用三次法則。等到第三次重複時再抽象化,也不算太晚。

如果 KISS 是「保持簡單」,DRY 就是「只把知識放在一個地方」。兩者最終都是為了降低變更成本。

應該有一段邏輯,你每次搜尋都會在三個地方找到。那就是今天的重構候選。