iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 7

Day 07|總是結伴出現卻沒有名字的三兄弟:資料泥團 (Data Clumps)

  • 分享至 

  • xImage
  •  

工匠採買磚頭,從來不是單獨算一塊一塊的價錢。
磚的種類、數量、要送去哪一圈。 這三件事,永遠是一起被記在同一張採購單上

沒有人會把「數量」單獨記在一本帳,「種類」記在另一本帳,用的時候再想辦法對起來

因為這三個資訊,早就是同一件事的三個面向

昨天留下的三兄弟

Day 06 把七個參數包進了 OrderRequest

public record OrderRequest
{
    public int CustomerId { get; init; }
    public int ProductId { get; init; }
    public int Qty { get; init; }
    public CustomerTier Tier { get; init; }
    public string CouponCode { get; init; }
    public bool SendEmail { get; init; }
    public bool SendSms { get; init; }
}

問題解決了嗎? 只解決了一半

去翻 Process 內部呼叫的每一個方法:

private (Customer, Product) LoadAndValidate(int customerId, int productId, int qty) { ... }
private decimal CalculatePrice(Product product, int qty, CustomerTier tier, string couponCode) { ... }
private void DeductStock(Product product, int qty) { ... }
private void LogOrder(int customerId, int productId, int qty, decimal price) { ... }

productIdqty,在四個方法裡,出現了四次

拆掉其中一個,會發生什麼事

有一個簡單的試驗方法:試著把 qty 從某一個方法簽名裡拿掉

private void DeductStock(Product product) { ... }

DeductStock 立刻失去意義。 扣庫存扣多少?沒有 qty,這個方法根本做不了事

這正是資料泥團的辨識訣竅:拆掉其中一個變數,如果剩下的變數瞬間變得不完整,就代表它們從一開始就該被綁在一起

productIdqty,其實一直在描述同一件事:「這張訂單,要買哪個商品、買幾件」

只是這件事,一直沒有一個名字

沒有名字的代價

沒有名字的資料泥團,代價不只是「參數看起來很像」:

  • 驗證邏輯散落各處。如果哪天要規定「單次訂購不能超過 99 件」,這條規則該加在 LoadAndValidate?還是 CalculatePrice?還是兩個都要加?
  • 改一次,要動好幾個地方。假設要新增「批次編號」跟著商品走,四個方法的簽名都要跟著改一輪。
  • 它會助長其他壞味道。這正是 Day 06 過長參數列表的部分成因。
    泥團沒被封裝,才會不斷以零散參數的形式,塞進一個又一個方法。

提煉出「這一單要買什麼」

productIdqty,正式封裝成一個屬於自己的類別:

public record OrderLine
{
    public int ProductId { get; }
    public int Qty { get; }

    public OrderLine(int productId, int qty)
    {
        if (qty <= 0)
            throw new ArgumentException("數量必須大於零");

        ProductId = productId;
        Qty = qty;
    }
}

「數量必須大於零」這條規則,現在只活在一個地方。

四個原本各自散落的方法,跟著換成接收 OrderLine

private (Customer, Product) LoadAndValidate(int customerId, OrderLine line) { ... }
private decimal CalculatePrice(Product product, OrderLine line, CustomerTier tier, string couponCode) { ... }
private void DeductStock(Product product, OrderLine line) { ... }
private void LogOrder(int customerId, OrderLine line, decimal price) { ... }

OrderRequest 也跟著整理,直接把 OrderLine 收進去,而不是繼續攤平成兩個獨立欄位:

public record OrderRequest
{
    public int CustomerId { get; init; }
    public OrderLine Line { get; init; }
    public CustomerTier Tier { get; init; }
    public string CouponCode { get; init; }
    public bool SendEmail { get; init; }
    public bool SendSms { get; init; }
}

四個方法簽名同時變短,而且「一次訂購要買什麼」這個概念,終於有了自己的名字和家

模組一,蓋完了

從 Day 03 到 Day 07,我們一路把 OrderProcessor 從一個 60 行、七參數、字串硬撐規則的方法,拆成:

  • 職責分明的私有方法(Long Method)
  • 各自獨立的協作物件(Large Class 的教訓,用在 ShopManager 上)
  • 有名字、有規則的型別,取代裸的 string(Primitive Obsession)
  • 一張完整的委託單,取代七個位置咒語(Long Parameter List)
  • 結伴出現的資料,終於有了自己的類別(Data Clumps)

這五天處理的,都是同一件事:體積本身,不是重點;有沒有比例,才是重點

自我檢查清單

  1. 有沒有一組變數,總是同時出現在好幾個方法的簽名裡?
  2. 試著拿掉其中一個變數,剩下的還有意義嗎?
  3. 這組資料相關的驗證邏輯,是不是散落在好幾個地方,各自重複?
  4. 如果要幫這組資料多加一個欄位,需要同時修改幾個方法?
  5. 這組資料,是不是其實一直在描述同一個業務概念,只是沒有名字?

明日預告

明天先不急著離開工地
Day 08 會退到廣場外,替模組一的五個警訊做一次驗收:當它們同時出現,該先修哪一個?

完成比例檢視後,Day 09 再進入模組二,拜訪達文西與物件導向的濫用者


上一篇
Day 06|召喚一個函式要念十句咒語:過長參數列表 (Long Parameter List)
下一篇
Day 08|退到工地外,看圓頂的比例:五個警訊,同時出現先修哪一個?
系列文
文藝復興:這段程式碼,好像有點味道9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言