iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
自我挑戰組

程式碼門診:診斷壞味道、開出重構處方系列 第 2

Write Everything Twice - 為什麼重複寫程式碼反而是好事?

  • 分享至 

  • xImage
  •  

昨天提到關於「DRY 原則」(Don't Repeat Yourself),要避免重複程式碼。但是有一個看似矛盾的觀念叫做「Write Everything Twice」,簡稱 WET 原則,第一次聽到 WET 原則以為是在搞笑😂,殊不知真的有人在談論他,來觀看英文維基百科對於 WET 原則描述如下

The opposing view to DRY is called WET, a backronym commonly taken to stand for write everything twice (alternatively write every time, we enjoy typing or waste everyone's time). WET solutions are common in multi-tiered architectures where a developer may be tasked with, for example, adding a comment field on a form in a web application. The text string "comment" might be repeated in the label, the HTML tag, in a read function name, a private variable, database DDL, queries, and so on. A DRY approach eliminates that redundancy by using frameworks that reduce or eliminate all those editing tasks except the most important ones, leaving the extensibility of adding new knowledge variables in one place.This conceptualization of "WET" as an alternative to "DRY" programming has been around since at least 2002 in the Java world, though it is not known who coined the term.

在中文的 wiki 對於 WET 原則的介紹來自於 The Pragmatic Programmer 書中認為重複可以發生在以下幾種情況

  1. Imposed duplication:開發者認為不得不的重複
  2. Inadvertent duplication:開發者沒有意識到的重複
  3. Impatient duplication:開發者複製自己或他人的程式碼造成的重複
  4. Interdeveloper duplication:不同開發者間共同開發或交接造成的重複

連知名大大 Kent C. Dodds 也闡述了對於 WET 原則,大概的意思不好的抽象比起重複的程式碼更糟糕,他在文章中還提到了 AHA(發音為啊哈),套用 WET 原則取 Write Everything Twice 的每個字母第一個字,AHA 則是"Avoid Hasty Abstractions"

換句話說,這不是鼓勵我們寫出重複的程式碼,而是提醒我們:過早的抽象化比重複程式碼更危險。接下來我們會透過 TypeScript 範例來了解這個概念。

以下用同一個「結帳金額計算」業務邏輯做對照:過度 DRY(不良) vs WET(較佳)。兩段程式皆為 React + TypeScript

過度 DRY(不良):過度抽象,一個萬用元件塞滿 if/else(可讀性差、難維護)

import React from "react";

type Context = "cart" | "order";
type BadProps = {
  subtotal: number;
  context: Context;
  vip?: boolean;
  coupon?: number;
  rush?: boolean;
  showInvoiceFee?: boolean;
};

const BadPriceCard: React.FC<BadProps> = (props) => {
  let shipping = props.subtotal < 1000 ? 60 : 0;
  let discount = 0;
  if (props.context === "cart") {
    if (props.coupon) discount += props.coupon;
    if (props.vip) discount += Math.min(100, props.subtotal * 0.05);
    if (props.rush) shipping += 120; // 其實購物車不該出現加急邏輯
  } else {
    // order
    if (props.vip) discount += 50; // 與 cart 不同規則糾纏在一起
    if (props.subtotal > 5000) shipping = 0;
    if (props.rush) shipping += 120;
  }
  const invoiceFee = props.showInvoiceFee && props.context === "order" ? 20 : 0;
  const total = Math.max(0, props.subtotal + shipping + invoiceFee - discount);

  return (
    <div style={{ border: "1px solid #ccc", padding: 12 }}>
      <h3>{props.context === "cart" ? "購物車摘要" : "訂單明細"}</h3>
      <div>小計:{props.subtotal}</div>
      {props.coupon ? <div>折價券:-{props.coupon}</div> : null}
      {props.vip ? <div>VIP 折抵:已套用</div> : null}
      {props.showInvoiceFee && props.context === "order" ? <div>發票處理費:{invoiceFee}</div> : null}
      {props.rush ? <div>急件處理:已加收</div> : null}
      <div>運費:{shipping}</div>
      <strong>應付:{total}</strong>
    </div>
  );
};

export const BadCart = () => (
  <BadPriceCard subtotal={1200} context="cart" vip coupon={100} rush />
);
export const BadOrder = () => (
  <BadPriceCard subtotal={700} context="order" rush showInvoiceFee />
);

WET(較佳):先各寫各的,重複一點沒關係,邏輯清楚易讀,等穩定再抽象

import React from "react";

type CartProps = { subtotal: number; vip?: boolean; coupon?: number };
type OrderProps = { subtotal: number; vip?: boolean; rush?: boolean };

export const CartSummary: React.FC<CartProps> = ({ subtotal, vip, coupon }) => {
  const shipping = subtotal < 1000 ? 60 : 0;
  const discount = (coupon ?? 0) + (vip ? Math.min(100, subtotal * 0.05) : 0);
  const total = Math.max(0, subtotal + shipping - discount);
  return (
    <div style={{ border: "1px solid #8bc", padding: 12 }}>
      <h3>購物車摘要</h3>
      <div>小計:{subtotal}</div>
      {coupon ? <div>折價券:-{coupon}</div> : null}
      {vip ? <div>VIP 折抵:已套用</div> : null}
      <div>運費:{shipping}</div>
      <strong>應付:{total}</strong>
    </div>
  );
};

export const OrderSummary: React.FC<OrderProps> = ({ subtotal, vip, rush }) => {
  const baseShipping = subtotal > 5000 ? 0 : 60;
  const shipping = rush ? baseShipping + 120 : baseShipping;
  const discount = vip ? 50 : 0;
  const invoiceFee = 20;
  const total = Math.max(0, subtotal + shipping + invoiceFee - discount);
  return (
    <div style={{ border: "1px solid #8c8", padding: 12 }}>
      <h3>訂單明細</h3>
      <div>小計:{subtotal}</div>
      {vip ? <div>VIP 折抵:-{discount}</div> : null}
      {rush ? <div>急件處理:+120</div> : null}
      <div>發票處理費:{invoiceFee}</div>
      <div>運費:{shipping}</div>
      <strong>應付:{total}</strong>
    </div>
  );
};

export const GoodDemo = () => (
  <>
    <CartSummary subtotal={1200} vip coupon={100} />
    <OrderSummary subtotal={700} rush />
  </>
);

說明(重點)

  • 過度 DRY 範例:用一個「萬用元件」硬塞兩種情境,充滿 if/else 與旗標(contextrushshowInvoiceFee…),抽象錯誤、耦合緊、難讀難改
  • WET 範例:把「購物車」與「訂單」拆成兩個小元件,雖然有些計算片段相似且重複,但每個元件的規則一眼看懂。等需求穩定,再決定是否抽出共用小函式(例如計算運費或 VIP 折扣),避免過早抽象造成維護負擔。

總結

「Write Everything Twice」的核心概念提醒我們:

  1. 先寫具體實作,了解真正的需求
  2. 在第二次遇到相似問題時,再考慮是否需要抽象化
  3. 好的抽象化來自於理解,而不是預測

這不是說永遠不要抽象化,而是要在正確的時機進行抽象化。當我們真正理解問題域,並且看到明確的重複模式時,才是提取共用邏輯的最佳時機。

在現代開發中,WET 原則特別適用於快速變化的業務需求階段。這時候我們可以發現,讓程式碼先「活著」比讓它「完美」更重要。等需求穩定後,再進行適當的重構和抽象化,往往能得到更好的架構設計。

參考資料

上一篇
Don't Repeat Yourself:讓程式碼更乾淨優雅的第一步
下一篇
Rule of Three - 什麼時候該開始重構?
系列文
程式碼門診:診斷壞味道、開出重構處方7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言