"Value-Object"
- entity 與 value object 的判準
同一個業務概念該建成 entity 還是 value object:判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。
- 值物件的 Dart 實作路徑
一個領域值該不該脫離裸的通用型別、以及在 Dart 用哪種載體實作時使用。手寫 immutable class、freezed 產生器、extension type 零成本包裝的成本結構不同——欄位數、要不要 runtime 身份、boilerplate 容忍度決定選哪條,以及從原始型別遷移過去怎麼鎖住行為不變。
- Value Object
判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。
- 建構路徑設計
工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。
- 「978ABC」被拒的理由寫著長度不對 — 驗證的兩層分工與順序陷阱
輸入驗證有兩層職責:建構期不變式守「這個物件能不能存在」、無狀態 validator 守「使用者輸入對不對」,混在一起會讓測試建不出 fixture、錯誤訊息歸錯類。順序陷阱:先標準化再檢查等於先銷毀證據再診斷——含字母的 ISBN 被削成三位數、錯誤訊息說長度不對。
- SQLite 只吃三種型別 — value object 在持久化邊界的序列化契約
把 value object 直接塞給 sqflite 會炸 Invalid argument——SQLite 只接受 num / String / Uint8List,VO 必須在 repository 邊界拆成基本型別、讀回時重建。用 toString/fromString 當轉換通道是權宜:它依賴兩者對稱這條沒人強制的隱性契約,正解是語意明確的序列化方法。
- Value Object 的封裝擺盪:從全移除、完全封裝、到加回 .value getter
VO 的封裝邊界在兩個極端之間來回——純字串(零封裝)跟完全封裝(禁止取原始值)各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口,而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。
- 同一個品項、四個 model — value object 什麼時候該升級成 entity
同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例,含 snapshot 與 live reference 的凍結時機。
- 兩個 ImportResult 各自都合理 — 傘狀名的碰撞與做一半的重命名
同一個 domain 裡兩個類別都叫 ImportResult:一個是驗證結果、一個是操作結果,各自誕生時都合理。修法是依語意責任命名(ImportValidationResult);而重命名是一組原子操作——類別名、檔名、import、文件宣稱,做一半留下的不一致比不做更迷惑,且宣稱完成與實際完成的漂移要靠稽核抓。
- 取個原始值有四種寫法 — VO 的 toString 洩漏與 accessor 不一致
value object 家族的取值 accessor 各自為政(displayValue、toString、裸欄位)時,殺傷力會在測試層爆開:自定義 toString 讓裸字串斷言全數過期、四種取法讓修復者自己都寫錯。止血是 helper 函數庫集中取值知識、根治是家族統一 accessor 命名。
- 金額型別的三段遷移:double、Decimal、再到 Money extension type
金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」;用 Dart extension type 包成 Money 之後,型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。