iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Software Development

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

Day 24|為了「以後可能會畫」先買好的十種畫框:猜測性通用 (Speculative Generality)

  • 分享至 

  • xImage
  •  

有位畫室主人,開店前想得很遠

他心想:「以後客人可能會要油畫框、水彩框、粉彩框……」於是一口氣訂了十種畫框,擺滿整面牆

半年後盤點,九種畫框,一次都沒被用過,畫室目前,只接油畫委託

那九種畫框,不是「準備好了、還沒用到」,是佔用了牆面、綁住了一筆錢
只為了一個從未發生的「以後」

一個只有單一實作的抽象類別

Day 15 之後,訂單報表功能持續在長大
開發者想到「以後可能要支援匯出成 CSV、Excel、甚至 PDF」,於是先把架構「做好」:

public abstract class OrderExportGenerator
{
    protected readonly IReadOnlyList<Order> Orders;

    protected OrderExportGenerator(IReadOnlyList<Order> orders)
    {
        Orders = orders;
    }

    public abstract void Export(string path, string reservedForFutureLocale);
}

目前系統裡,唯一的子類別實作

public class CsvOrderExportGenerator : OrderExportGenerator
{
    public CsvOrderExportGenerator(IReadOnlyList<Order> orders) : base(orders) { }

    public override void Export(string path, string reservedForFutureLocale)
    {
        // ...寫出 CSV 的邏輯...
    }
}

呼叫端長這樣:

public class OrderExportService
{
    public void ExportMonthlyOrders(IReadOnlyList<Order> orders)
    {
        OrderExportGenerator generator = new CsvOrderExportGenerator(orders);
        generator.Export("monthly-orders.csv", null);
    }
}

十種畫框,只有一種真的被用到

打開這段程式碼,會看到三個「準備好但沒派上用場」的訊號:

  • 抽象類別 OrderExportGenerator,只有一個子類別 CsvOrderExportGenerator
    • 這個抽象層,沒有帶來任何實際的彈性,因為根本沒有第二種型態需要被抽換
  • 參數 reservedForFutureLocale,從建立以來,一直只被傳入 null
    • 它是為了「以後可能要支援多語系檔名」預留的,但那個「以後」還沒有到來
  • 變數宣告成父類別型別 OrderExportGenerator generator,但實際上只可能被指派成 CsvOrderExportGenerator多型帶來的彈性,在這裡是虛的

這跟今天開頭那十個畫框是同一種處境,它們看起來像是「未雨綢繆」,實際上只是把牆面(程式碼的複雜度)先佔住,換來一個可能永遠不會兌現的方便

更諷刺的是:如果半年後真的要支援 Excel 匯出,工程師打開這個抽象類別,大概率會發現
「當初猜的介面設計,跟 Excel 匯出真正需要的介面,對不上」

得先拆掉這層猜測性的架構,才能開始做真正的需求

摺疊繼承體系,回到當下真正需要的樣子

解法是摺疊繼承體系 (Collapse Hierarchy):把子類別的實作,直接併回一個具體類別,砍掉那層沒有實際作用的抽象

public class CsvOrderExportGenerator
{
    private readonly IReadOnlyList<Order> _orders;

    public CsvOrderExportGenerator(IReadOnlyList<Order> orders)
    {
        _orders = orders;
    }

    public void Export(string path)
    {
        // ...寫出 CSV 的邏輯...
    }
}

沒用過的參數 reservedForFutureLocale,用移除參數 (Remove Parameter) 直接拿掉

呼叫端也跟著變得誠實:

public class OrderExportService
{
    public void ExportMonthlyOrders(IReadOnlyList<Order> orders)
    {
        var generator = new CsvOrderExportGenerator(orders);
        generator.Export("monthly-orders.csv");
    }
}

少了一個抽象類別、少了一個永遠是 null 的參數,程式碼變得更簡單,卻沒有損失任何目前真正用得到的能力

那真的需要支援多種格式時,怎麼辦?

答案不是「永遠不要抽象」,是把抽象的時機,留到真正出現第二種需求的那一刻

到那時候,你手上有兩份真實的實作可以比較(CSV 跟 Excel),能提煉出真正符合兩者需求的介面
這比賽前猜測的抽象,準確得多,也省得下一次重構

這正是 YAGNI 原則(You Aren't Gonna Need It)的核心:
不是不做設計,是不要為了不確定的未來,在當下先付出複雜度的代價

兩種情況,通用設計是合理的

  • 你正在開發框架或函式庫:提供給別人使用的擴充點,本來就該預留彈性
    • 這裡的「使用者」是別的開發者,不是你自己猜測出來的未來
  • 為了讓測試能夠存取內部狀態:某些看似沒被業務邏輯呼叫的方法,可能是為了可測試性而刻意保留,動手刪除前,務必先確認測試案例有沒有用到

自我檢查清單

  1. 這個抽象類別或介面,目前有幾個實作?如果只有一個,這層抽象帶來了什麼實際的彈性?
  2. 這個方法的參數,是不是一直被傳入同一個值,卻沒有人記得為什麼要留著它?
  3. 如果有人問「為什麼設計得這麼複雜」,我的答案是不是「因為以後可能會需要」?
  4. 這段通用設計,是為了框架的使用者而做,還是為了我自己猜測的未來?
  5. 如果拿掉這層抽象,回到最直接的寫法,現在的需求還能被完整滿足嗎?

明日預告

模組四到這裡,六種贅肉都盤點完了

明天我們走進畫室的日常運作,看看師傅跟學徒之間,該怎麼分工才不會互相踩線

模組五:畫室的分工倫理(The Couplers),正式開工


上一篇
Day 23|畫室角落,早就沒人用的舊畫架:無用的程式碼 (Dead Code)
下一篇
Day 25|清倉日:敢刪,比敢加更需要判斷力
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言