觸發場景:Flutter 書籍管理 App 的錯誤處理系統重構。起點是一套公認過度設計的系統(雙目錄、包裝器、adapters),重構目標寫得斬釘截鐵:「完全放棄複雜設計、回歸原生 Dart Exception + ErrorCode、移除 80% 程式碼、零學習成本」。三個小版本後,一個帶五個欄位、五個工廠方法、JSON 序列化的 AppException 基類完工——階層回來了 疑問來源:這是重構失敗嗎?還是「回歸原生」這個目標本身有問題? 整理目的:記下「砍掉的實作」跟「實作回應的需求」是兩回事、以及怎麼在砍之前把兩者分開 本文邊界:素材是該專案 v0.9.0 的重構計畫與 v0.9.6 的實作記錄;效能數字(< 0.1ms、< 200 bytes)為 log 自報


被砍的東西、跟砍它的理由

舊系統的過度設計是實錘的,重構計畫列的證據都站得住:

  • 雙重系統並存:兩個目錄各有一個 app_error.dart、互相依賴、責任不明
  • 無資訊的欄位StandardErrorBusinessLogicError 的薄包裝,businessRule 欄位永遠是固定值 'LEGACY_STANDARD_ERROR'——欄位存在、但永遠同值,等於沒有這個欄位
  • 解不存在問題的機能:錯誤物件的深度複製與循環參照處理、多餘的 ErrorAdapters 轉換層

新架構的設計範例極簡:一個 ErrorCode enum、幾個 implements Exception 的輕量類別,codemessage 兩個欄位。目標指標全是刪減向的:程式碼 -80%、建立時間、記憶體、零學習成本。

三個版本後:階層原地重生

v0.9.6 交付的 AppException 長這樣:errorCodeuserMessagecontexttimestampstackTrace 五個欄位;withStackTracewrap 加五個分類便利工廠(validation / network / business / storage / platform);完整的 toJson / fromJson;從 errorCode 衍生 categoryisRecoverable。之後的版本再從它派生出五個分類子類、配上分類一致性的建構不變式。

對照 v0.9.0 的口號,這是違規;對照需求,每一個「長回來的東西」都有名有姓的消費者:

重生的機能消費者
userMessageUI 的錯誤顯示(繁中文案、跟開發者訊息分離)
JSON 序列化跨平台錯誤格式(Chrome Extension 端要吃同一種錯誤)
category / 子類錯誤路由與統計(哪類錯誤重試、哪類直接報)
isRecoverable錯誤處理流程的分支決策
wrap / stackTrace第三方異常的統一化、除錯追蹤

需求不死、只是換宿主。 舊系統被砍掉時、這些需求沒有跟著消失,它們安靜地等著,在新架構的第一批真實使用裡逐個把自己長回來。

這不是繞回原點——但成本可以更低

重生的版本跟舊系統不等價:沒有雙目錄、沒有薄包裝、沒有 adapters、沒有深拷貝。全砍再重長的過程實際上完成了一次需求過濾——有消費者的機能重生了、沒有的死透了。從結果看,這比在舊系統上修修補補乾淨。

但同樣的過濾有更便宜的做法:砍之前對舊系統逐機能問「這個機能有沒有真實的消費者?」——businessRule 固定值沒有(砍)、深拷貝沒有(砍)、userMessage 有 UI 在用(留、換宿主)、序列化有 Chrome Extension 在吃(留)。盤點的產物是一張「保留需求清單」,新架構從第一天就對著清單設計,不用等真實使用把需求一個個撞回來——這個專案為這輪錯誤系統遷移付出了整個中版本、幾十個子版本的 migration 工作量,其中一部分就是「長回來」的返工。

口號式目標不可驗收、需求清單可以

「回歸原生、零學習成本、砍 80%」作為重構目標的問題在驗收:v0.9.6 的 AppException 算不算違反「零學習成本」?五個工廠方法算不算「原生」?口號沒有判準,於是重生過程既沒有被擋下(沒人能說它違規)、也沒有被承認(文件仍然掛著回歸原生的旗)——方向的實質轉彎沒有留下決策記錄。

換成需求清單做目標(「錯誤系統要服務:UI 文案、跨平台序列化、分類路由;不服務:深拷貝、adapter 轉換」),每一次擴充都可以對表:在清單上、做;不在、先補清單再做。同一個結果、但每一步有據可查——這跟 VO 封裝擺盪的收束是同一個藥方:把邊界寫成決策記錄,下一任重構者才不會從自己撞到的那一面再推一次極端

判讀徵兆

  • 重構目標以口號形式出現(回歸原生、極簡、零依賴)而沒有「保留哪些需求」的清單——重生已在路上,差別只是有沒有人記錄它
  • 被砍系統裡有固定值欄位、無人呼叫的轉換層——真該砍的部分,砍之前留一行「為什麼它沒有消費者」
  • 新架構上線後前幾個版本連續「補回」被砍的機能——每一筆都是需求盤點的漏項,把它們補進當初缺席的清單
  • 效能目標(< 0.1ms、< 200 bytes)當重構主訴求——先確認錯誤建立真的在熱路徑上,否則這是拿好量的指標代替難量的設計問題

相關閱讀

  • 同專案的擺盪家族:VO 封裝擺盪——封裝政策的來回、本文是抽象層級的來回,共同根因都是邊界沒有決策記錄
  • 重生後的階層怎麼運作:Exception 型別綁 ErrorCategory 的建構不變式——本文的 AppException 家族在後續版本的分類不變式實戰
  • 過度設計的姊妹篇:異步查詢系統的過度設計震盪——那篇砍的多數是偽需求所以砍得掉;本文砍的混著真需求所以長回來,兩篇合起來是「砍之前先分真偽」的完整論證