"Repository"
- 觀測出口的職責三分
repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰:契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。
- 讀模型的升級判準
repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關:訊號決定該爬到哪一階,自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。
- Read Model
查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。
- Observation Outlet(觀測出口)
repository 只有 pull 介面、衍生視圖靠補償刷新,考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。
- Repository
查詢方法該留在 repository、還是該抽成獨立讀模型時使用。repository 是 aggregate 的存取抽象——回傳的形狀是 aggregate 的形狀,不是讀的形狀。
- CQRS
有人提議「上 CQRS」、或想知道讀寫分離該做到多徹底時使用。CQRS 是把讀操作與寫操作的模型拆開的架構決定——寫側守一致性、讀側服務查詢形狀,兩者可以各自有獨立的儲存與更新節奏。
- StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點
repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。
- 1000 本書、1001 次 SQL — N+1 查詢藏在 async mapper 裡
N+1 查詢由兩個各自合理的函式組合而成:單筆轉換函式順便查關聯(mapper 做 IO)、列表方法用 Future.wait 把它乘以 N。修法是 IO 上移——一次批次 IN 查詢、記憶體組裝、轉換函式變純。mapper 簽名是 async 就是訊號;開發期資料量小、惡化是上線後隨資料成長的乘法。
- mock 要配置 55 個方法、實際只用 5 個 — 測試痛是介面設計痛的探針
service 測試的 mock 負擔正比於它依賴的介面寬度:依賴四個大介面共 55 個方法、實際呼叫 5 個,91% 的 mock 配置是純浪費、還會炸 MissingStubError。修法是介面隔離——抽出只含實際使用方法的 Port,讓 mock 縮到跟真實依賴一樣窄;為未來預留的方法用 TODO 標記啟用時機。
- SQLite 只吃三種型別 — value object 在持久化邊界的序列化契約
把 value object 直接塞給 sqflite 會炸 Invalid argument——SQLite 只接受 num / String / Uint8List,VO 必須在 repository 邊界拆成基本型別、讀回時重建。用 toString/fromString 當轉換通道是權宜:它依賴兩者對稱這條沒人強制的隱性契約,正解是語意明確的序列化方法。
- 遷移計畫有寫入、有消費、缺讀出 — read-path 缺口與 fixture 假綠
資料模型遷移的通路要三段齊:寫入 backfill、讀取路徑、消費端 API。缺讀出那段時,新 API 拿到的永遠是空集合——而消費端測試的 fixture 自己建物件、不走真實讀取路徑,測試全綠掩蓋 runtime 靜默失效。依賴圖只列「誰先做」不列語意前提時,dashboard 的 ready 是假訊號。