有些顏料配方,不是由一位畫家獨自掌握,而是畫室裡每一個工作站,各自記著一份配方筆記
準備底漆的學徒記一份、調肉色的師傅記一份、修飾金箔邊框的工匠也記一份
如果贊助人要求換一種底料,這件事聽起來很單純
但畫室裡三、四個工作站,每一個人手上的筆記,都要跟著改,改完還要祈禱,沒有人漏改
Day 15 把 OrderReport 拆乾淨了。同一時間,另一組人也在擴充訂單相關功能:
OrderProcessor:客戶下單時處理訂單OrderImportService:客服用 CSV 批次匯入舊系統的歷史訂單OrderEditService:客服人員手動修改一筆尚未出貨的訂單三個服務,都各自寫了同一條規則「訂單商品數量必須大於零,且單筆金額不能超過 10 萬元」
public class OrderProcessor
{
public decimal Process(OrderRequest request)
{
if (request.Line.Qty <= 0 || request.Line.Qty * 100 > 100_000)
throw new InvalidOperationException("訂單不符合規則");
// ...原有處理邏輯...
return 0;
}
}
public class OrderImportService
{
public void Import(OrderRequest request)
{
if (request.Line.Qty <= 0 || request.Line.Qty * 100 > 100_000)
throw new InvalidOperationException("訂單不符合規則");
// ...匯入邏輯...
}
}
public class OrderEditService
{
public void Edit(OrderRequest request)
{
if (request.Line.Qty <= 0 || request.Line.Qty * 100 > 100_000)
throw new InvalidOperationException("訂單不符合規則");
// ...編輯邏輯...
}
}
business 提出一條新規則 「VIP 客戶的單筆訂單,至少要有 2 件商品,才能享有折扣」
負責這個需求的工程師,得依序打開三個檔案,在每一個地方都補上幾乎一模一樣的判斷式:
if (request.Tier == CustomerTier.Vip && request.Line.Qty < 2)
throw new InvalidOperationException("VIP 訂單至少需要兩件商品");
三個檔案,三次手術
改完 OrderProcessor 跟 OrderImportService,改到一半被會議打斷
OrderEditService 忘了改
兩週後,客服透過編輯功能,把一筆 VIP 訂單改成只有一件商品,系統完全沒有攔下來
沒有人做錯任何一行程式碼,這條規則本身寫得完全正確
問題是它被抄寫了三次,其中一次,忘了跟上進度
解法的核心思想:「這筆訂單是否有效」這個問題,不該由三個外部服務各自回答一次,該由訂單自己回答一次,其他人只管問它
換成程式碼,這是搬移方法 (Move Method)
把散落在三個服務裡的驗證邏輯,全部搬回 OrderRequest 自己身上
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; }
public void Validate()
{
if (Line.Qty <= 0 || Line.Qty * 100 > 100_000)
throw new InvalidOperationException("訂單不符合規則");
if (Tier == CustomerTier.Vip && Line.Qty < 2)
throw new InvalidOperationException("VIP 訂單至少需要兩件商品");
}
}
三個服務,改成只呼叫這一個共同的入口:
public class OrderProcessor
{
public decimal Process(OrderRequest request)
{
request.Validate();
// ...原有處理邏輯...
return 0;
}
}
public class OrderImportService
{
public void Import(OrderRequest request)
{
request.Validate();
// ...匯入邏輯...
}
}
public class OrderEditService
{
public void Edit(OrderRequest request)
{
request.Validate();
// ...編輯邏輯...
}
}
現在只有一份配方筆記
下一次規則異動,只要改 OrderRequest.Validate() 一個地方
三個服務會自動套用最新規則,不需要任何人記得「還有哪裡也要改」
Day 15 的發散式變更,是一個類別扛了太多互不相關的理由
今天的霰彈式修改,是一個單一的理由,卻被拆散到太多不相關的類別
兩者的成因常常互為因果,上一次為了解決發散式變更,把職責拆得太細,拆過頭了,就變成了今天的霰彈式修改
拆分本身不是目標,讓每一種變化的理由,只對應到一個修改的地方,才是目標
明天我們看兩套繼承體系,像鏡子一樣互相對應,每加一位新的畫室成員,都得同時在兩本花名冊上,各記一筆
模組三最終站:平行繼承體系(Parallel Inheritance Hierarchies)