中世紀的畫,人物常常是「該重要的畫大一點」
國王比隨從大,聖人比信徒大,畫面熱鬧,卻沒有一致的空間邏輯
達文西的手稿裡,留下大量人體解剖圖、光影角度計算、透視消失點的幾何推導
在他真正下筆畫一張臉之前,他先搞懂了骨骼怎麼支撐肌肉
透視法的核心,是一套一致的規則:所有平行線,往畫面深處延伸,最終匯聚到同一個消失點
物體離觀察者越遠,就該按比例越小
這套規則一旦確立,畫面裡的每一個物件,都用同一套邏輯決定自己該多大、該畫在哪裡
物件導向的核心承諾,跟透視法很像
建立一套一致的結構規則,讓每個物件都照著同一套邏輯運作,不需要外部另外用大量條件判斷去區分「這個物件現在該怎麼表現」
這正是 Day 02 提過的開放封閉原則(Open-Closed Principle),在結構層級的具體樣貌
模組二「物件導向的濫用者」,要處理的是五個「有形無骨」的警訊:
| Day | Code Smell | 一句話定位 |
|---|---|---|
| 09 | Switch 陳述式 (Switch Statements) | 每加一種類型,就要在好幾個地方多補一個 case |
| 10 | 暫時欄位 (Temporary Field) | 一個欄位,只有在特定情況下才有意義,其他時候閒置著 |
| 11 | 拒絕的遺贈 (Refused Bequest) | 子類別繼承了招牌,卻不用招牌底下的技法 |
| 12 | 異曲同工的類別 (Alternative Classes with Different Interfaces) | 兩個做同一件事的類別,介面卻各自為政 |
| 13 | 不完整的程式庫類別 (Incomplete Library Class) | 別人的畫室借給你用,牆上卻釘不得釘子 |
第一站,從最直覺、也最容易一路長歪的模式開始 Switch Statements
畫室裡,蛋彩、油彩、濕壁畫,處理方式完全不同
蛋彩要現調現用,油彩可以放幾天,濕壁畫則必須在灰泥未乾前完成
但沒有一位老師傅,會站在畫布前想著:
「如果是蛋彩,就這樣做;如果是油彩,就那樣做;如果是濕壁畫……」
每種顏料,早就知道自己該怎麼被處理
老師傅只需要問顏料一句話:「準備好了嗎?」剩下的,顏料自己會回答
Day 07 之後,OrderRequest 裡還留著兩個 bool:SendEmail、SendSms
團隊決定支援第三種通知管道:推播(Push)
最直覺的做法,是照著原本的邏輯,多加一個分支:
public enum NotificationChannel { Email, Sms, Push }
private void NotifyCustomer(Customer customer, List<NotificationChannel> channels)
{
foreach (var channel in channels)
{
switch (channel)
{
case NotificationChannel.Email:
_emailService.Send(customer.Email, "Order Confirmed", BuildEmailBody(customer));
break;
case NotificationChannel.Sms:
_smsService.Send(customer.Phone, BuildSmsBody(customer));
break;
case NotificationChannel.Push:
_pushService.Send(customer.DeviceToken, BuildPushBody(customer));
break;
default:
throw new ArgumentException("未知的通知管道");
}
}
}
看起來很合理。加一個 case,加一段邏輯,完工
問題是,後台管理系統還有一個「通知預覽」功能,讓客服在發送前先看看內容長什麼樣:
private string PreviewNotification(Customer customer, NotificationChannel channel)
{
switch (channel)
{
case NotificationChannel.Email:
return BuildEmailBody(customer);
case NotificationChannel.Sms:
return BuildSmsBody(customer);
case NotificationChannel.Push:
return BuildPushBody(customer);
default:
throw new ArgumentException("未知的通知管道");
}
}
同樣一組 case,出現了第二次
半年後,如果要再加第四種管道 LINE 官方帳號
這兩個 switch 都得記得改,一個都不能漏
漏改一個,不會編譯錯誤,只會在客服打開預覽時
發現 LINE 通知永遠顯示不出內容而沒有人知道為什麼
回到畫室的比喻:與其讓老師傅站在畫布前,對每一種顏料重新判斷一次處理方式,不如讓每一種顏料,把自己的處理方式帶在身上
換成程式碼,這是以多型取代條件式 (Replace Conditional with Polymorphism)
第一步,定義一個所有通知管道都要遵守的共同介面:
public interface INotificationChannel
{
void Send(Customer customer);
string Preview(Customer customer);
}
第二步,讓每一種管道,自己實作自己的邏輯:
public class EmailNotificationChannel : INotificationChannel
{
private readonly IEmailService _emailService;
public EmailNotificationChannel(IEmailService emailService) => _emailService = emailService;
public void Send(Customer customer) =>
_emailService.Send(customer.Email, "Order Confirmed", Preview(customer));
public string Preview(Customer customer) => $"親愛的 {customer.Name},您的訂單已確認。";
}
public class SmsNotificationChannel : INotificationChannel
{
private readonly ISmsService _smsService;
public SmsNotificationChannel(ISmsService smsService) => _smsService = smsService;
public void Send(Customer customer) => _smsService.Send(customer.Phone, Preview(customer));
public string Preview(Customer customer) => $"訂單確認:{customer.Name} 您好。";
}
public class PushNotificationChannel : INotificationChannel
{
private readonly IPushService _pushService;
public PushNotificationChannel(IPushService pushService) => _pushService = pushService;
public void Send(Customer customer) => _pushService.Send(customer.DeviceToken, Preview(customer));
public string Preview(Customer customer) => $"{customer.Name},訂單已確認 🎉";
}
第三步,NotifyCustomer 不再需要知道「有哪幾種管道」,只需要對每一個管道說「該你上場了」:
private void NotifyCustomer(Customer customer, IEnumerable<INotificationChannel> channels)
{
foreach (var channel in channels)
channel.Send(customer);
}
PreviewNotification 也一樣簡化:
private string PreviewNotification(Customer customer, INotificationChannel channel) =>
channel.Preview(customer);
兩個 switch 全部消失
半年後要加 LINE 通知,只要新增一個 LineNotificationChannel 類別NotifyCustomer 跟 PreviewNotification 一行都不用改
switch 本身不是原罪,需要提防的是同一份型別判斷,重複出現在系統的不同角落。
switch 只出現一次,邏輯也很單純,直接留著往往比硬拆成多型更划算switch 是用在工廠裡,唯一的職責就是「根據類型,決定要 new 哪一個物件」真正該警覺的,是同一組 case,在兩個以上的地方各自判斷一次
switch 或連續的 if-else-if,是根據「物件的類型」在做決定嗎?switch?switch,會不會在執行期才被發現,而不是編譯期?switch 是不是可以換成「讓物件自己知道該怎麼做」?明天我們回到一個更隱蔽的問題:一個欄位,只有在某個方法執行的那幾毫秒裡才有意義,其他時間都是閒置的
模組二第二站:暫時欄位(Temporary Field)