iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

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

Day 09|每加一種類型就多畫一筆的素描:Switch 陳述式 (Switch Statements)

  • 分享至 

  • xImage
  •  

中世紀的畫,人物常常是「該重要的畫大一點」
國王比隨從大,聖人比信徒大,畫面熱鬧,卻沒有一致的空間邏輯

達文西的手稿裡,留下大量人體解剖圖、光影角度計算、透視消失點的幾何推導
在他真正下筆畫一張臉之前,他先搞懂了骨骼怎麼支撐肌肉

透視法的核心,是一套一致的規則:所有平行線,往畫面深處延伸,最終匯聚到同一個消失點

物體離觀察者越遠,就該按比例越小
這套規則一旦確立,畫面裡的每一個物件,都用同一套邏輯決定自己該多大、該畫在哪裡

物件導向的核心承諾,跟透視法很像

建立一套一致的結構規則,讓每個物件都照著同一套邏輯運作,不需要外部另外用大量條件判斷去區分「這個物件現在該怎麼表現」

這正是 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 裡還留著兩個 boolSendEmailSendSms

團隊決定支援第三種通知管道:推播(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 類別
NotifyCustomerPreviewNotification 一行都不用改

不是所有 switch 都有罪

switch 本身不是原罪,需要提防的是同一份型別判斷,重複出現在系統的不同角落

  • 如果 switch 只出現一次,邏輯也很單純,直接留著往往比硬拆成多型更划算
  • 如果 switch 是用在工廠裡,唯一的職責就是「根據類型,決定要 new 哪一個物件」
    (這種用法本身就是多型結構的入口,不需要再拆)

真正該警覺的,是同一組 case,在兩個以上的地方各自判斷一次

自我檢查清單

  1. 這個 switch 或連續的 if-else-if,是根據「物件的類型」在做決定嗎?
  2. 系統裡有沒有另一個地方,判斷著一模一樣的類型?
  3. 上次加一個新類型,是不是同時改了好幾個 switch
  4. 如果漏改其中一個 switch,會不會在執行期才被發現,而不是編譯期?
  5. 這個 switch 是不是可以換成「讓物件自己知道該怎麼做」?

明日預告

明天我們回到一個更隱蔽的問題:一個欄位,只有在某個方法執行的那幾毫秒裡才有意義,其他時間都是閒置的

模組二第二站:暫時欄位(Temporary Field)


上一篇
Day 08|退到工地外,看圓頂的比例:五個警訊,同時出現先修哪一個?
系列文
文藝復興:這段程式碼,好像有點味道9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言