同一個子系統膨脹兩次:異步查詢系統的過度設計震盪
觸發場景:Flutter 書籍管理 App 的「背景查詢書籍資料」子系統,在兩個月內走了兩輪「膨脹 → 大砍」:第一輪把大系統設計砍成一個純狀態追蹤器、第二輪把三個並存的實作版本砍回一個 疑問來源:第一輪已經用力砍過偽需求了,為什麼同一個子系統還會再膨脹一次? 整理目的:記下兩輪膨脹各自的機制、以及「真需求 vs 偽需求」的可操作檢驗法 本文邊界:素材是該專案 v0.4.1 與 v0.5.2 的重構記錄;量化數字(874 → 250 行等)出自 log 自報、未經獨立重算,本文引用時只取數量級意義
第一輪膨脹:設計期想像出來的需求
初版的異步查詢設計把它當一個大系統做:優先級佇列、重試策略、九種事件類型、Isolate 執行、三層快取。每一項單獨看都「像是背景查詢系統該有的」。
審查砍掉它們的依據不是「以後再說」,而是逐條檢驗後發現多數能力已經有別層在做:
- 重試策略——API 實作層(
GoogleBooksApiImplementation)已經處理 - 格式轉換——三層資料模型(DTO → EnrichmentData → Metadata)已經處理
- UI 非阻塞——統一 API 架構本身就是非同步的、不需要額外機制
砍完之後真需求只剩兩個:查詢狀態追蹤跟查詢取消。落地成一個 QueryTracker:一個 Map<String, SimpleQueryState>、一個 Map<String, Completer>、四個狀態值。這是第一輪的教訓形態——設計期的偽需求長得像完備性(「系統該有的都列上」),檢驗法是逐條問「這個能力在這個架構裡、已經有哪一層在負責?」答案存在,這條就是偽需求;把它做進來不只是浪費、還會製造兩層之間的職責衝突(兩層都重試 = 重試次數相乘)。
第二輪膨脹:迭代期不刪的舊版本
一個版本之後(v0.5.1),同一個子系統又膨脹了、但形態完全不同:Scanner 的查詢服務出現三個並存的實作版本(enhanced、integrated、v1)、一個 AsyncQueryManager 帶著回調地獄、查詢狀態在多個層級各自追蹤、資料所有權混亂。
這輪的成因與想像力無關——它是迭代的沉積:每次改進都新開一個版本、舊版本「先留著以防萬一」、狀態追蹤跟著每個版本各長一份。沒有人設計出這個複雜度,它是不刪除的累積結果。
第二次收縮(v0.5.2)的修法對準這個機制:BookQueryResult 成為查詢狀態的單一真相來源、BookQueryService 取代 AsyncQueryManager、三個實作統一成一個、回調改 Future。log 自報的規模:約 874 行減到約 250 行、核心類別 7 個減到 3 個、移除 12 個舊檔案;重構後的新架構測試 78 個全數通過。
兩輪機制不同、對策也不同
把兩輪並排,能看出「過度設計」這個標籤蓋住了兩種需要不同對策的問題:
| 輪次 | 膨脹機制 | 形態 | 對策 |
|---|---|---|---|
| 設計期 | 想像未來需求、追求完備性 | 用不到的佇列 / 事件 / 快取層 | 逐條檢驗「別層是否已負責」、需求清單要能對應到實際操作 |
| 迭代期 | 舊版本不刪、狀態隨版本增生 | N 個實作並存、多處狀態追蹤 | 版本並存視為暫態、開新版時排定舊版的刪除點;狀態收斂單源 |
第一輪的警訊出現在設計文件裡(能力清單長於操作清單);第二輪的警訊出現在檔案系統裡(同名服務帶 enhanced / v1 / unified 後綴並存)。第二輪也解釋了「砍過一次為什麼不免疫」——第一輪的對策防的是想像,防不了沉積。
檔名後綴是第二輪的早期訊號
第二輪膨脹在檔名上留下了可 grep 的痕跡:enhanced_scanner_book_info_service、..._v1、重構後又出現 ..._unified。把版本狀態編進檔名,意味著「哪個是現役版本」這個資訊只存在人的記憶裡——三個檔案都在、都能被 import、新程式碼引用哪個全憑作者當下的認知。同專案更早的重構也清過一批同型的 _simple / _equatable 後綴。訊號的用法:檔名裡出現版本形容詞的當下,就把「刪除舊版」排進同一輪工作,而不是等它長到三個版本。
相關閱讀
- 概念地基:DDD 領域驅動設計指南 的「從操作推導領域」——真需求清單的來源是使用者操作、不是系統完備性想像
- 上游事件:兩個 domain 各自實作同一個 API service——本文的統一 API 架構正是那次架構債修正的產物,兩篇合起來是 v0.4 架構稽核的完整弧線
- 同型原則:#77「現在不決定」是合法選項——設計期對想像需求的正確處置是「延後 + 條件」、不是先做起來放
- 後續案例:一行查詢放哪、一個測試留不留——同樣是查詢層的取捨,這次的膨脹方向是「為一個
.where開一個空殼 service」,判準是「查詢誰擁有的資料就掛給誰」
#flutter #dart #ddd #yagni #over-engineering #refactoring #async