iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Software Development

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

Day 12|兩位畫家,同一個場景,不同的簽名方式:異曲同工的類別 (Alternative Classes with Different Interfaces)

  • 分享至 

  • xImage
  •  

兩間相隔一條街的畫室,各自接了「聖母領報」這個題材的委託
構圖幾乎一樣,天使、聖母、百合花,連光線角度都相近

但兩位畫家彼此不認識,也沒看過對方的畫
簽名的方式,一個簽在畫布左下角、寫全名;一個簽在畫框背面、只留縮寫

同一件事,被做了兩次,還用了兩種完全不同的方式標記完工

兩個團隊,各自做了 LINE 通知

Day 09 定義了 INotificationChannel,統一了 Email、Sms、Push 的介面

專案後來拆成兩個小隊平行開發,都接到「支援 LINE 官方帳號通知」的需求

但誰都不知道對方也在做

A 小隊寫出來的版本:

public class LineNotifier
{
    private readonly ILineApiClient _client;
    public LineNotifier(ILineApiClient client) => _client = client;

    public void Push(string userId, string text)
    {
        _client.PushMessage(userId, text);
    }
}

B 小隊寫出來的版本:

public class LineMessageSender
{
    private readonly ILineApiClient _client;
    public LineMessageSender(ILineApiClient client) => _client = client;

    public void SendMessage(string to, string content)
    {
        _client.PushMessage(to, content);
    }
}

兩個類別,呼叫同一支底層 API,做的是完全一樣的事
「推送一則 LINE 訊息」

差別只在:一個叫 Push,一個叫 SendMessage
一個參數叫 userIdtext,一個叫 tocontent

Code Review 抓到的困惑

Code Review 時,第三位工程師看到兩個 PR,同時新增了功能重疊的類別,忍不住問:

「所以以後要串 LINE 通知,該用 LineNotifier,還是 LineMessageSender?」

沒有人能立刻回答

  • 兩份實作各自維護,同一個 API 呼叫方式,要修 bug 得改兩個地方
  • 兩個類別都沒有實作 Day 09 定義的 INotificationChannel沒辦法跟 EmailNotificationChannelSmsNotificationChannel 放進同一個列表統一呼叫
  • 半年後,新來的工程師要加 LINE 通知功能,會先花時間搞懂「這兩個類別的差異到底是什麼」

答案是:沒有差異,只是兩個人各自簽了不同的名字

這正是異曲同工的類別最典型的成因,不是技術問題,是溝通斷層
兩位畫家不是能力不夠,是根本沒看過彼此的畫布

讓兩位畫家,用同一種方式簽名

解法的第一步,不是急著刪掉其中一個,而是先統一介面

兩個類別做的事完全相同,讓它們都遵守 Day 09 已經定義好的 INotificationChannel 契約:

public interface INotificationChannel
{
    void Send(Customer customer);
    string Preview(Customer customer);
}

一旦介面統一,兩份實作立刻現出原形,它們是同一件事的兩份拷貝

public class LineNotificationChannel : INotificationChannel
{
    private readonly ILineApiClient _client;
    public LineNotificationChannel(ILineApiClient client) => _client = client;

    public void Send(Customer customer) =>
        _client.PushMessage(customer.LineUserId, Preview(customer));

    public string Preview(Customer customer) => $"{customer.Name},您的訂單已確認 🎉";
}

LineNotifierLineMessageSender 都可以刪除,換成這一個類別

所有原本呼叫舊版本的地方,改成呼叫 LineNotificationChannel,並且跟 Day 09 的其他管道一樣,被丟進同一個 IEnumerable<INotificationChannel> 裡統一處理

不用再判斷「這次要用哪一個」,因為現在真的只剩一個

光統一介面就夠了嗎?

不一定。統一介面之後,實作內容可能還有落差:

  • 如果兩份實作的邏輯完全一樣(像今天的例子),統一介面之後可以直接刪掉重複的一份
  • 如果部分邏輯不同(例如 A 版本多做了重試機制),可以把共通部分抽到一個新的父類別,讓兩個版本各自繼承,保留各自的差異

目標不是「一定只能留一個類別」,是讓所有做同一件事的類別,用同一套語言溝通

怎麼提早發現這種重複

異曲同工的類別,最難的不是重構,是發現它存在,因為表面上看起來完全是兩回事

  • Code Review 時,多問一句:「這個功能,是不是專案裡已經有類似的東西了?」
  • 幫核心概念(像「發送通知」)建立清楚的介面規範,讓新功能第一天就往同一個契約靠攏,而不是先各自發展再回頭合併
  • 定期做一次「跨小隊」的程式碼巡覽,尤其是在多團隊平行開發的專案裡

自我檢查清單

  1. 專案裡有沒有兩個類別,做的事情概念上完全相同,方法名稱卻不一樣?
  2. 這兩個類別,是不是由不同的人、在不同時間、互不知情的狀況下寫出來的?
  3. 如果幫它們統一介面,實作內容會不會發現其實一模一樣?
  4. 新人要用這個功能時,能不能一眼看出該用哪一個類別,不必先問資深同事?
  5. 團隊有沒有一個機制,讓新功能開發前,能先確認「是不是已經有現成的做法」?

明日預告

明天我們遇到一種不一樣的困境,不是自己人寫重複了
別人的函式庫,少了我們需要的那個方法,而我們動不了它的原始碼

模組二最終站:不完整的程式庫類別(Incomplete Library Class)


上一篇
Day 11|繼承了畫室招牌,卻不用畫室的技法:拒絕的遺贈 (Refused Bequest)
下一篇
Day 13|租來的畫室,牆上釘不得釘子:不完整的程式庫類別 (Incomplete Library Class)
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言