iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

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

Day 28|問個顏料存量,要問過五個學徒才有答案:訊息鏈 (Message Chains)

  • 分享至 

  • xImage
  •  

畫室主人想知道,紅色顏料還剩多少
合理的做法,是直接問管理顏料的那位學徒

不合理的做法,是主人得先問大學徒「顏料放在哪個房間」,再問那個房間的學徒「放在哪個櫃子」
再問管櫃子的人「哪一格抽屜」,最後才問到真正管顏料的人

問一件小事,繞了五個人
不是因為顏料很難找,是因為沒有人幫主人把這條路,事先鋪好

為了問一個城市,穿越了三層物件

系統裡有這樣的巢狀結構:

public class Address
{
    public string Street { get; set; }
    public string Zone { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public Address ShippingAddress { get; set; }
}

public class Customer
{
    public string Name { get; set; }
    public Order LastOrder { get; set; }
}

有個功能,要判斷客戶最後一筆訂單的收貨地區,是不是能享有當日速達:

public class ExpressEligibilityChecker
{
    public bool IsEligibleForExpress(Customer customer)
    {
        string zone = customer.LastOrder.ShippingAddress.Zone;

        return zone == "北" || zone == "中";
    }
}

customer.LastOrder.ShippingAddress.Zone一行程式碼,串起了三個物件

這條鏈,串起的不只是資料

ExpressEligibilityChecker 為了問到 Zone,被迫連帶知道了:

  • Customer 有一個 LastOrder
  • Order 有一個 ShippingAddress
  • Address 有一個 Zone

它本來只該認識 Customer 這位「密友」,卻被迫也認識了 OrderAddress 的內部長相

這條鏈,只要中間任何一環改變
LastOrder 改名成 RecentOrder,或者 Address 換了一種結構

所有沿用這條鏈的程式碼,都會一起壞掉

這正是物件導向裡「迪米特法則」(最少知識原則)要提醒我們的事
只跟你的密友說話,不要繞過密友,直接向密友的密友問問題

要幫這樣的程式碼寫測試,也會變得很痛苦
得先組出一個完整的 CustomerOrderAddress 巢狀假物件,才能測一個簡單的判斷

讓每一站,自己負責轉交下一站

解法是隱藏委派 (Hide Delegate)
在鏈條的每一站,都建立一個方法,把「怎麼找到下一站」的細節,藏在物件內部

先讓 Order 自己知道怎麼回答「收貨地區」:

public class Order
{
    public int OrderId { get; set; }
    public Address ShippingAddress { get; set; }

    public string GetShippingZone() => ShippingAddress.Zone;
}

再讓 Customer 把請求,轉交給 LastOrder

public class Customer
{
    public string Name { get; set; }
    public Order LastOrder { get; set; }

    public string GetLastOrderShippingZone() => LastOrder.GetShippingZone();
}

呼叫端,現在只需要跟它的密友 Customer 對話:

public class ExpressEligibilityChecker
{
    public bool IsEligibleForExpress(Customer customer)
    {
        string zone = customer.GetLastOrderShippingZone();

        return zone == "北" || zone == "中";
    }
}

ExpressEligibilityChecker 現在只認識 Customer 一個類別
不再需要知道 OrderAddress 的存在

未來 Customer 內部要換一種方式儲存訂單歷史
只要 GetLastOrderShippingZone() 這個對外的契約沒變,呼叫端完全不用跟著修改

不是所有鏈,都值得隱藏

如果是 「短而穩定的鏈」
例如只串兩層,而且結構幾乎不會變動,通常是可以接受的,不需要為了原則而過度重構

真正該處理的,是那種跨越三層以上、或者串接的結構本身就容易變動的鏈

判斷的關鍵不是「有沒有連續呼叫」
是這條鏈,有沒有讓呼叫端知道了太多它不該知道的內部結構

還有一個更重要的提醒,如果把鏈條上每一站的委派都無腦隱藏,鏈條起點的物件
可能會被塞滿一堆 「只是轉發」 的方法,變成明天要談的下一種壞味道

自我檢查清單

  1. 這一行程式碼裡,出現了幾層連續的方法呼叫或屬性存取?
  2. 呼叫端,是不是因此連帶知道了好幾個物件的內部結構?
  3. 如果鏈條中間任何一環改變名稱或結構,會不會有一堆看似無關的程式碼跟著壞掉?
  4. 要幫這段程式碼寫測試,是不是得建立一整串巢狀的假物件?
  5. 這條鏈,是短而穩定的例外,還是真的該用隱藏委派來縮短?

明日預告

明天是模組五的最後一站,也是這 23 種壞味道裡的最後一種
一個角色,只負責把話轉過去,自己不做任何判斷

中間人(Middle Man)


上一篇
Day 27|兩間畫室共用一把鑰匙:不適當的親密關係 (Inappropriate Intimacy)
下一篇
Day 29|只負責轉達話語,自己不畫一筆的傳令:中間人 (Middle Man)
系列文
文藝復興:這段程式碼,好像有點味道29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言