昨天,我們把五條規矩刻上了畫室門楣
今天,第一次套進具體場景
1296 年,佛羅倫斯開始蓋聖母百花大教堂,八角形的底座很早就完成了,唯獨中間那個洞
留給圓頂的位置,空了超過一百年
不是沒有錢,也不是沒有工匠,是沒有人知道該怎麼蓋
傳統做法要先搭一整套木造鷹架撐住重量,但這個圓頂的跨度,寬到找不到那麼長的木頭
1418 年,菲利普·布魯內雷斯基(Filippo Brunelleschi)給的答案
不是找到更粗的木頭,而是放棄鷹架這個假設本身
圓頂做成內外兩層,磚塊用人字紋排列,讓每一圈磚在砌上去的當下,就能自己撐住自己
每一圈磚,都是一個獨立完工的單位
砌完這一圈、確認它撐得住自己,才開始下一圈
沒有人想過要「一口氣」把整座圓頂澆築成一塊,那是不可能的事
沒有人扛得住那樣的重量,也沒有人看得懂那樣的結構
圓頂立起來了,不是因為磚頭堆得比較多,是因為每一圈磚
在被放上去的那一刻,就已經知道自己在整體結構裡的位置
這是模組一「臃腫者」要處理的五個警訊:
| Day | Code Smell | 一句話定位 |
|---|---|---|
| 03 | 過長方法 (Long Method) | 一個方法做了五件事,卻只有一個名字 |
| 04 | 巨大類別 (Large Class) | 什麼職責都攬在身上的「上帝類別」 |
| 05 | 原始型別執念 (Primitive Obsession) | 用一串 string 硬撐一個本該有名字的概念 |
| 06 | 過長參數列表 (Long Parameter List) | 呼叫一個方法,要先背下七個參數的順序 |
| 07 | 資料泥團 (Data Clumps) | 總是一起出現,卻始終沒被封裝成一個名字的三兄弟 |
第一站,從最常見的一種開始
回到 Day 01 那段「能跑」的 OrderProcessor
public class OrderProcessor
{
private readonly AppDbContext _db;
private readonly ILogger _log;
private readonly IEmailService _emailService;
private readonly ISmsService _smsService;
public OrderProcessor(AppDbContext db, ILogger log,
IEmailService emailService, ISmsService smsService)
{
_db = db;
_log = log;
_emailService = emailService;
_smsService = smsService;
}
public decimal Process(int customerId, int productId, int qty,
string customerType, string couponCode, bool sendEmail, bool sendSms)
{
var customer = _db.Customers.Find(customerId);
var product = _db.Products.Find(productId);
if (customer == null || product == null)
throw new InvalidOperationException("not found");
if (product.Stock < qty)
throw new InvalidOperationException("out of stock");
decimal price = product.Price * qty;
if (customerType == "VIP")
price = price * 0.9m;
else if (customerType == "REGULAR" && couponCode == "WELCOME10")
price = price * 0.95m;
product.Stock = product.Stock - qty;
_db.SaveChanges();
_log.Info("order processed: customer=" + customerId +
" product=" + productId + " qty=" + qty + " price=" + price);
if (sendEmail)
_emailService.Send(customer.Email, "Order Confirmed", "...");
if (sendSms)
_smsService.Send(customer.Phone, "...");
return price;
}
}
一個 Process 方法,同時做了:
五件事,疊在同一個方法裡,像沒分層就想澆築完成的圓頂
沒有人是故意把方法寫長的
通常的過程是這樣:
每一次加的都不多,加總起來,就是一整座撐不住自己的圓頂
問題不在「這段程式碼寫錯了」——它能編譯、能跑、demo 也會過,問題在於:
半年後,有人要改折扣邏輯,得先讀完全部 60 行,才敢確定改哪一段不會動到別的東西
這才是 Long Method 真正的代價:不是跑不動,是沒有人敢改
布魯內雷斯基的做法,換成程式碼語言,就是提煉方法 (Extract Method)
把每一個「獨立完工的單位」抽出來,各自負責一件事:
public class OrderProcessor
{
private readonly AppDbContext _db;
private readonly ILogger _log;
private readonly IEmailService _emailService;
private readonly ISmsService _smsService;
public OrderProcessor(AppDbContext db, ILogger log,
IEmailService emailService, ISmsService smsService)
{
_db = db;
_log = log;
_emailService = emailService;
_smsService = smsService;
}
public decimal Process(int customerId, int productId, int qty,
string customerType, string couponCode, bool sendEmail, bool sendSms)
{
var (customer, product) = LoadAndValidate(customerId, productId, qty);
decimal price = CalculatePrice(product, qty, customerType, couponCode);
DeductStock(product, qty);
LogOrder(customerId, productId, qty, price);
NotifyCustomer(customer, sendEmail, sendSms);
return price;
}
private (Customer, Product) LoadAndValidate(int customerId, int productId, int qty)
{
var customer = _db.Customers.Find(customerId);
var product = _db.Products.Find(productId);
if (customer == null || product == null)
throw new InvalidOperationException("not found");
if (product.Stock < qty)
throw new InvalidOperationException("out of stock");
return (customer, product);
}
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;
}
private void DeductStock(Product product, int qty)
{
product.Stock -= qty;
_db.SaveChanges();
}
private void LogOrder(int customerId, int productId, int qty, decimal price)
{
_log.Info($"order processed: customer={customerId} product={productId} qty={qty} price={price}");
}
private void NotifyCustomer(Customer customer, bool sendEmail, bool sendSms)
{
if (sendEmail)
_emailService.Send(customer.Email, "Order Confirmed", "...");
if (sendSms)
_smsService.Send(customer.Phone, "...");
}
}
Process 現在讀起來,像一份施工順序表:
驗證 → 算價 → 扣庫存 → 記錄 → 通知
每一段獨立完工,也各自可以單獨測試
眼尖的人可能注意到,CalculatePrice 裡還藏著一句:
if (customerType == "VIP")
這句話本身,就是 Day 05 要談的另一種壞味道 「原始型別執念(Primitive Obsession)」
不過在那之前,我們還要先去看看,一個類別如果把太多職責攬在身上,會變成什麼樣子
那就是明天的 「巨大類別(Large Class)」
不是所有方法都要拆到只剩三行,判斷的標準比較像這樣:
只要有一題答案是「不行」,就是該分層砌磚的時候了
明天我們把鏡頭拉遠,看一整間畫室
如果一個工地主任身兼建築師、監工、會計、採購,所有事情都攬在自己身上,會發生什麼事?
模組一第二站:巨大類別(Large Class)