圓頂的曲線,不是每一位工匠憑感覺砌出來的
布魯內雷斯基替不同高度的每一圈磚,都準備了專屬的木製模具(centina)
工匠不需要自己判斷「這一圈該彎多少」,只要照著模具的弧度砌,整座圓頂的曲線就會維持一致
模具,把「正確的形狀」這件事,變成一個看得到、摸得到、不會被誤解的標準
回到 Day 03 拆出來的 CalculatePrice:
private decimal CalculatePrice(Product product, int qty, string customerType, string couponCode)
{
decimal price = product.Price * qty;
if (customerType == "VIP")
price *= 0.9m;
else if (customerType == "REGULAR" && couponCode == "WELCOME10")
price *= 0.95m;
return price;
}
customerType 是一個 string
它看起來只是資料,其實藏著一整套業務規則:客戶只能是「VIP」或「REGULAR」,不能是別的
但僅靠 string 沒辦法替你守住這條規則
想像三個地方,各自要判斷客戶是不是 VIP:
if (customerType == "VIP") { ... }
if (customerType == "Vip") { ... }
if (customerType == "vip") { ... }
三行看起來都合理,但只有第一行會生效
每個工匠憑感覺砌出來的弧度,看起來都「差不多對」——直到某一圈的誤差被下一圈放大,整座圓頂開始歪斜
這就是原始型別執念(Primitive Obsession)的代價:
規則沒有一個固定的形狀,只能靠每個人各自記住、各自猜
解法的核心思想很直接:把「客戶只能是 VIP 或 REGULAR」這條規則,做成一個真正的型別,而不是繼續用字串硬撐。
public enum CustomerTier
{
Regular,
Vip
}
方法簽名跟著換掉:
private decimal CalculatePrice(Product product, int qty, CustomerTier tier, string couponCode)
{
decimal price = product.Price * qty;
if (tier == CustomerTier.Vip)
price *= 0.9m;
else if (tier == CustomerTier.Regular && couponCode == "WELCOME10")
price *= 0.95m;
return price;
}
現在再打錯字,編譯器會直接擋下來:CustomerTier.VIP 這種寫法根本不會通過編譯,不必等到 Production 才發現問題
CustomerTier 解決了「打錯字」的問題,但 couponCode 還是一個裸的 string
如果優惠碼還有格式規則(例如一律轉大寫、不能有空白),這些規則該放在哪裡?
答案不是散落在每個用到它的地方,而是封裝進一個專屬的小物件:
public readonly record struct CouponCode
{
public string Value { get; }
public CouponCode(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("優惠碼不可為空白");
Value = value.Trim().ToUpperInvariant();
}
}
CouponCode 保證了一件事:只要它存在,它就一定是有效格式
呼叫端不用重複寫「先 trim、再轉大寫、再檢查空白」——這些規則只活在一個地方
這正是 Day 01 提過的 Primitive Obsession 診斷:
用字串代表狀態,打錯字不會編譯錯誤,只會在 Production 悄悄算錯
模具做出來之後,這句話就不再成立了
不是每一個 string 或 int 都要換成物件,判斷的重點是:
只要有一題的答案讓你不安,這個原始型別大概該有自己的模具了
明天,我們回頭看 CalculatePrice(Product product, int qty, CustomerTier tier, string couponCode) 這串參數本身——當一個方法的參數多到要背誦順序,那又是另一種警訊
模組一第四站:過長參數列表(Long Parameter List)