DRY 原則與重複程式碼實務 從生活備品看重構中的重複問題,避開「假重複」的架構陷阱

me
林彥成
2023-09-18 | 4 min.
文章目錄
  1. 1. 什麼是 DRY 原則與重複程式碼?
  2. 2. 重複的物件:囤積者的焦慮
    1. 2.1. 實戰重構:拆分 vs 合併的選擇
  3. 3. 深度解析:真重複 vs 假重複 (Accidental Duplication)
  4. 4. 結語:依照「本質」進行分類
  5. 5. FAQ:DRY 原則常見問題
    1. 5.1. Q1:什麼時候應該打破 DRY 原則?
    2. 5.2. Q2:強行執行 DRY 會帶來什麼風險?
    3. 5.3. Q3:React 元件重複很多 HTML 結構,一定要抽成元件嗎?

什麼是 DRY 原則與重複程式碼?

DRY 原則 (Don’t Repeat Yourself, 不要重複你自己) 是軟體開發中旨在減少「知識重複」的設計核心。其目標在於確保系統中的每一項邏輯或規格,都具備唯一的、明確的權威表示。然而,並非所有看起來相似的程式碼都需要被合併。重複程式碼 需細分為「真重複」(邏輯與未來演化方向完全一致)與「假重複 (Accidental Duplication)」(目前相似但代表不同業務職責)。過早或強行的 DRY 往往會導致模組間產生不必要的強耦合,反而增加維護難度。正確的重構策略應是以「本質職責」為導向,在程式碼精簡與系統解耦之間找到平衡。


延續上一篇文章 該好好重構和斷捨離的是程式碼還是人生 中可怕的例子,這篇文章我們來看看在 軟體開發原則 中,當同類型的東西一再出現該怎麼辦?

重複的物件:囤積者的焦慮

在生活中擁有什麼東西,等同於自己的價值觀。有時候總是害怕未來缺少什麼,所以我們習慣提早購買備品。仔細想想大家的家裡是不是都有非常多的牙刷、牙膏、衛生紙呢?而實際上我們只需要一條牙膏。

在寫程式中,我們常被教導要遵循 DRY 原則 (Don’t Repeat Yourself),即「不要重複你自己」。不過,重複真的絕對不好嗎?

實戰重構:拆分 vs 合併的選擇

假設今天一個專案總共用五個階段、五個角色,每個角色在不同階段需要填的欄位並不相同。在開發初期為了快速測試,我們可以選擇:

  1. 拆開元件 (WET - Write Everything Twice):雖然同類型的東西一再出現,但修改特定元件時不會影響其他流程,容錯性高,開發快速。
  2. 合在一起 (DRY):雖然減少了重複,但會讓傳入的 Props 與內部的條件判斷(if-else)變得極其複雜,形成難以維護的 上帝物件

深度解析:真重複 vs 假重複 (Accidental Duplication)

在進行 程式碼重構 時,最困難的決定莫過於判斷這段重複是「真的」還是「剛好長得像」。

  • 真重複: 兩段程式碼邏輯完全相同,且未來修改的原因也一定相同(例如:稅金計算公式)。這時應果斷抽離成通用函式。
  • 假重複: 兩段程式碼目前看起來一模一樣,但它們服務於不同的業務領域。例如「訂單收件人」與「帳單聯絡人」目前格式相同,但未來可能因為不同的物流或財稅法規而各自分化。這時若強行 DRY,會導致未來的修改變得極度困難。
1
2
3
4
5
6
7
8
9
10
11
12
// 初始版本只有基本角色屬性
const player1 = { name: "玩家1", level: 1, health: 100, damage: 10 };

// 一段時間後,增加需求:敏捷度與背包
const player2 = { name: "玩家2", level: 5, health: 150, damage: 15, agility: 20, inventory: ["劍", "盾"] };

// 如果強行合在一起,函數內容會變得非常長且充滿 if-else
function processPlayerData(name, level, health, damage, agility, inventory, statusCode) {
if (health > 10 && (name === "foo" || damage < 5) && (name !== "bar" || agility > 20)) {
// ...
}
}

在這樣的情境下,不管是拆開或是合起來都會產生些許問題,大大們怎麼看?! 剛才的例子來說也許我會至少先分類,參數分成必要跟選填,接著把邏輯抽出來變成好懂的敘述:

1
2
3
4
5
function processPlayerData({ name, level, health, damage }, { agility, inventory, statusCode }) {
if (checkNameAndHealth() && checkAgility()) {
// ...
}
}

重複最大的問題是同類型的東西需要修改時,會需要在很多地方修改。但如果程式很少要改,好像也沒關係嗎?如果很常需要更改,是不是就容易發生忘記改到的情況?

重複的東西,是真的相同嗎? 還是只是剛好看起來很像,但滿足的需求不同?

舉個例子像我就有好多耳機跟運動鞋,學校宿舍用、工作租屋用、辦公室用、運動用、浴室用,但搬回家之後就多了許多沒用到的。如果依照場合去分類就會擁有好幾同類型的物品。整理東西的時候,我們就要先把同一類的東西先集中,這樣才會知道實際上自己擁有了多少。

有的時候的確是真的相同,牙刷、牙膏來說就是這樣。但以運動用的耳機和浴室用的藍牙喇叭就需要簡易的防水,這就是處理的問題不太相同,但我們是真的需要嗎?


結語:依照「本質」進行分類

就像我有好多耳機與運動鞋,有的在租屋處、有的在辦公室。雖然都是「鞋子」,但它們滿足的需求不同(運動鞋 vs 居家拖鞋)。

整理東西應該是依照物品的「本質」來做分類和整理,而非依照場所。在程式設計中也是如此,重要的不是兩段程式碼現在長得有多像,而是它們背後代表的「職責」是否一致。

整理,是在告訴自己,重要的不是過去的回憶,而是經歷過往後所成就現在的自己。

透過理解 DRY 原則 的真諦,我們能更理智地看待重複。不盲目追求程式碼的精簡,而是追求架構的穩定與靈活,這才是高效開發的斷捨離之道。


FAQ:DRY 原則常見問題

Q1:什麼時候應該打破 DRY 原則?

A:當兩段程式碼只是「巧合地相似 (Coincidental Duplication)」時。如果合併這兩段程式碼需要引入複雜的參數(如:if (type === 'A') doX() else doY()),這通常表示它們的職責不同。此時 WET (Write Everything Twice) 比錯誤的 DRY 更好,因為解耦的成本通常低於維護一個錯誤的抽象。

Q2:強行執行 DRY 會帶來什麼風險?

A:最嚴重的風險是「強耦合」。當您為了節省幾行程式碼而將兩個互不相干的功能強行共用同一個函式時,未來針對功能 A 的修改可能會意外破壞功能 B。這種「改一處壞全站」的現象通常源於過度的 DRY。

Q3:React 元件重複很多 HTML 結構,一定要抽成元件嗎?

A:不一定。如果該結構只是單純的樣式重複且不包含邏輯,使用單純的 CSS Class 可能就足夠。只有當結構伴隨著「行為」(如:點擊事件、內部狀態)且該行為在多處完全一致時,才是提取元件的黃金時機。



喜歡這篇文章,請幫忙拍拍手喔 🤣