一個 Domain 檔案裡混著聚合根、Value Object 與服務類別,而統計函式正在使用根本不存在的枚舉值——這種狀態下先動手重構,修的方向由程式碼的技術形狀決定,而那個形狀本身就是偏離設計的結果。

設計驅動重構把設計文件的閱讀與驗證放在所有重構動作的最前面:先確定這段程式碼原本要做什麼,才有判斷「修對了沒有」的依據。

為什麼設計文件要在程式碼之前讀

看到壞味道就處理、看到重複就提取,這條重構路線在多數位置成立。它在 Domain 層的核心實體上有一個盲點:處理的依據是程式碼的形狀,而形狀不帶業務意圖。

「Library 追蹤的為什麼是 SourceType 而不是 ReadingStatus」這個問題答不出來的時候,技術上整齊的重構會在業務上引入錯誤,而且錯誤不會立刻顯現——型別對、測試也可能過。設計驅動重構的前提因此是:重構決策基於現有的設計文件,不基於程式碼的技術形狀。

驗證流程

第一階段:設計文件檢查

在動任何一行程式碼之前先讀文件,順序是需求規格、用例說明、UI 設計規格、錯誤處理設計。

這個閱讀順序的邏輯是:先確立業務目標(需求規格),再理解使用場景(用例),再看 UI 層如何呈現(UI 規格),最後檢查系統如何應對異常(錯誤處理)。這樣走一遍,Domain Root 在整個系統中扮演的角色就會變得清晰。

以 Library 實體為例,文件說明它需要支援不同來源類型的書籍統計——實體書、電子書、借閱書各有不同的計算邏輯。但程式碼裡用的是 ReadingStatus 枚舉,而且還在使用 'digital''borrowed' 這些根本不在枚舉定義裡的值。這不是設計問題,這是實作根本跑錯了方向。

第二階段:測試覆蓋率分析

讀完文件之後看測試。測試是另一種形式的設計文件,它記錄的是這段程式碼被期待如何運作。

這個階段最常出現的問題是測試本身的方向偏了。Domain 層的單元測試引用了 flutter_test 套件時,它在純 Dart 環境中無法執行——CI 因此有一段盲區,而測試檔案還在,看起來像有覆蓋。

此外還有大量被 skip 的測試。每個 skip 都是一個未被驗證的業務假設,累積起來就是風險。

第三階段:程式實作驗證

前兩個階段建立的是對設計意圖的理解。第三階段才對照程式碼,逐一確認哪裡偏離了設計。

這個階段的關鍵產出是一份問題清單,而且每個問題都有分類:是類型系統錯誤、是架構違規、是技術債務,還是可維護性問題。分類決定了後續的修復優先級。

問題分類與修復優先級

對照出來的問題分成四類,修復順序按影響範圍決定。

類型系統錯誤優先級最高。 使用不存在的枚舉值、把 String 傳給期望強類型的參數,這類問題進入生產環境就是執行時崩潰,排在第一批處理。

異常處理不統一緊隨其後。 Library 實體裡有自定義的 DuplicateBookException,但整個專案其他地方都統一使用 AppError 體系。這種不一致性不只是風格問題,它讓錯誤處理的呼叫端無法用一致的方式應對。

技術債務屬於第二優先級。 最典型的例子是 author 欄位使用 String 類型。這在功能上可以運作,但它限制了後續的多作者支援、譯者解析和富文字搜尋能力。這類問題有明確的演進路徑,但不是緊急的。

可維護性問題排在最後。 包括缺乏需求追蹤編號的註解、業務規則說明不足等。這些問題不影響功能,但它們是技術債務的溫床——幾個月後回到這段程式碼時,缺的正是這些上下文。

幾個具體的重構模式

類型系統修正

問題很常見:用字串或錯誤的枚舉值來做判斷,而不是使用正確的類型。

修正前:

1books.where((book) => book.readingStatus == 'digital')

修正後:

1books.where((book) => book.source.type.isDigital)

表面上看這只是換了一個屬性,但背後的意義完全不同:前者依賴字串的巧合匹配,後者依賴類型系統的保證。

異常處理統一

自定義異常類別散落在程式碼各處,是一種常見的技術債務模式。

修正前:

1throw DuplicateBookException('message', bookId: id, title: title);

修正後:

1return OperationResult.failure(
2  BusinessLogicError(
3    message: 'message',
4    businessRule: 'BOOK_DUPLICATE_PREVENTION',
5    code: ErrorCodes.duplicateBook,
6    context: {'bookId': id, 'title': title},
7  ),
8  'userMessage'
9);

這個轉換不只是換個類別,它同時把拋出異常改成了回傳結果——這是更適合 Domain 層的錯誤傳遞方式,讓呼叫端可以明確地決定如何處理每種失敗情況。

Value Object 重構

String 提升為 Value Object 是一個需要仔細計畫的重構,因為它涉及所有使用處的同步更新。

修正前:

1final String author;

修正後:

1final BookAuthor author;

關鍵在於轉換策略要設計好:BookAuthor.fromString(stringValue) 負責從舊有字串資料遷移,bookAuthor.displayValue 負責在需要字串輸出的場合提供相容性。有了這兩個轉換點,才能在保持向後相容性的前提下完成遷移。

分階段執行,每段必驗

每個修復階段結束後驗證三件事:dart analyze 無錯誤、核心功能正常運作、API 介面保持不變。跳過階段驗證的代價是問題會疊加——後面階段出錯時,錯誤來源可能在前面任何一個未驗證的階段裡。

設計文件優先擋掉的是什麼

順序放在前面的作用,是擋掉幾種看起來合理的錯誤方向:看到 authorString 就直接抽 Value Object,可能建出不符合業務需求的型別;看到自定義異常就直接統一,可能在型別系統的問題還沒修好的基礎上做出錯誤的抽象。

兩者的共同形狀是:技術判斷本身沒錯,而它作用在一個尚未被理解的業務結構上。讀設計文件的時間換到的是這個結構。

重構的測試在什麼條件下該保持穩定,走 行為優先的 TDD;架構層的違規怎麼在 commit 階段就被擋下,走 架構合規交給機制