觸發場景:Flutter 專案要上多語言,盤點後發現 Domain 層有 947 處中文硬編碼——enum IssueType { missingTitle('缺失標題') } 這類寫法散在七個模組 疑問來源:這些字串當初寫起來很自然(enum 帶個顯示名稱、result 物件帶個 summary),為什麼會變成架構問題? 整理目的:記下「訊息內容」與「訊息文字」的分層責任劃分、以及大量硬編碼的遷移策略 本文邊界:素材是一個 Flutter 書籍管理 App 的重構記錄(第一個模組完成時的狀態);分層原則語言無關、translator extension 是 Dart 的實作載體


947 處是怎麼長出來的

硬編碼不是一次寫壞的,是每一處都很合理地長出來的。典型形態:

1// Domain 層
2enum IssueType {
3  missingTitle('缺失標題'),   // UI 文字在 Domain 層
4  ...
5}

enum 定義同步問題的類型,順手帶上顯示名稱——單獨看每一處都是便利的選擇:呼叫端要顯示時直接取,少一層轉換。同樣模式的還有 SyncResult.summary 這種直接組出中文句子的 getter、以及 value object 上的 displayName 屬性。七個模組累積下來的分佈:synchronization 25 處、import 46、search 47、export 52、scanner 82、version_management 90、library 563。

問題在多語言需求出現時一次引爆:文字寫死在 Domain 層,翻譯就得改 Domain——而 Domain 層理應對「呈現給誰、用什麼語言」一無所知。

劃分判準:領域事實 vs 呈現

修法的核心是把每個字串問一次:這是領域事實、還是呈現?

「這筆書目缺標題」是領域事實——同步檢查的產出、業務邏輯的分支依據。「缺失標題」四個中文字是呈現——同一個事實在英文介面叫 missing title。兩者的變動理由不同(業務規則 vs 語言與文案),依變動理由分層:

 1// Domain 層:只有代碼
 2enum SyncIssueCode {
 3  missingTitle('MISSING_TITLE'),
 4  ...
 5}
 6
 7// UI 層:translator extension
 8extension SyncIssueCodeTranslator on SyncIssueCode {
 9  String toLocalizedTitle(AppLocalizations l10n) {
10    switch (this) {
11      case SyncIssueCode.missingTitle:
12        return l10n.syncIssueMissingTitle;   // 翻譯在 UI 層
13    }
14  }
15}

exhaustive switch 讓這個橋接有編譯期保證:Domain 新增一個代碼、UI 層的 translator 沒跟上就編譯失敗——兩層的同步靠型別系統守、不靠人記得。

組合訊息的拆法:結構化資料取代成品字串

純代碼替換解決不了所有 947 處。有些字串是組合出來的——「同步完成,3 筆受影響」這種帶數量的訊息,Domain 層原本直接回成品字串。重構的處理是把資料模板拆開:

  • CheckResult.message 改成 messageCode(枚舉)、新增 affectedCount 屬性——數量是領域事實、由 Domain 提供
  • SyncResult.summary(回中文字串的 getter)移除、改成 resultCodesummaryData(結構化資料)——句子怎麼組、單複數怎麼變化,是 UI 層拿著資料跟 arb 模板的事

判讀方式:Domain 層的職責邊界劃在「提供組句需要的全部事實」,句子本身屬於呈現層。

遷移策略:從最小模組先行

七個模組的處理順序按硬編碼數量由少到多:synchronization(25 處)先做、library(563 處)最後。第一個模組承擔的是驗證模式——6 個代碼枚舉怎麼切、translator extension 怎麼組織、arb 鍵怎麼命名、測試怎麼跟著改,全套模式在最小的模組上走通(103 個相關測試全過),後面六個模組就是重複已驗證的模式。反過來從 563 處的 library 開刀,模式還沒定就得承擔最大的返工面。

測試的連動也在第一個模組現形:直接斷言中文字串的測試(expect(issue.description, '缺失標題'))全部要改成斷言枚舉。這類測試本來就把呈現細節烤進了斷言——重構把它們一併修正成對領域事實的斷言。

判讀徵兆

  • Domain 層的 enum 建構子參數是給人看的自然語言(而不是代碼常數)
  • value object 或 result 物件上有 displayNamesummarymessage 這類回傳成句文字的成員
  • 測試斷言裡出現 UI 文案字串
  • 多語言需求評估時,翻譯範圍清單裡出現 domain 目錄

四個訊號指向同一件事:呈現知識滲進了領域層,多語言只是讓它現形的第一個下游需求——文案改版、A/B 測試文案、依角色顯示不同措辭,都會撞上同一堵牆。

相關閱讀