跨邊界參照與狀態所有權
領域模型跨越邊界持有的每一個 id,有效性由上游行為決定、持有端控制不了。上游把「更新」實作成「刪除重建」是實作自由,下游用推理猜「它應該保留」擋不住——合法的出處只有契約文件或一次實測取證。這條事實推導出本章三個判準的共同源頭:跨邊界參照的設計,從「對方會做什麼」開始、而不是從「我要存什麼」開始。
本章承接 entity 與 value object 的判準 的身份語意討論,延伸到身份跨越系統邊界之後的生存問題;與 狀態轉換與稽核軌跡 的凍結端點段落互補——凍結是時間軸上的問題(何時),本章談的是空間軸上的問題(誰的 id、誰決定它活著)。
參照有效性三問
下游持有上游資料的 id 時,設計階段逐一回答:
- 對方的哪些操作會讓這個 id 死亡? 合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都會讓舊 id 失效。盲區集中在業務動詞與實作的語意落差:同一個「合併」,有的實作是搬資料、有的實作是建新刪舊。持有端用前者的假設設計、遇到後者的實作時靜默失效。
- 有沒有一個跨越這些操作仍不變的身份? 如果存在——以它為錨、其餘參照動態解析。如果沒有——凍結參照只能當一次性用途、跨操作的功能改走查詢。
- 「不變」的出處是什麼? 文件說的、程式碼推理的、實測證實的——只有第三種算數。把實測結果固化為測試資產(語意級假後端與流程測試、真實後端驗證測試),從此不依賴任何人的記憶。
三個問題的回答決定參照的設計形態。未回答就動工的代價是靜默失效——操作對著死 id 回寫,回傳值看起來正常(目標不存在也沒有噪音),功能在特定操作之後全部失效、直到使用者回報。凍結參照在測試中的結構盲區另見 T.C5:由測試餵資料的 stub 同時控制凍結值與回應資料,讓「id 死亡」這個狀態在測試裡不可能出現。
穩定身份為錨、解引用延後到使用時
第二題的答案若為「存在」,設計形態收斂為:持有穩定身份、其餘參照在使用時動態解析。
「reference by identity + 解引用時機延後」把「id → 實體」的解析從持有時延後到使用時,讓解析結果反映上游當前的真相。額外的韌性:即使身份轉移發生在本端不知情的時候(另一台裝置觸發合併),使用時解析照樣找到新家。
查無結果時退回凍結值,搭配存在性檢查兜底——穩定身份的值域(如 uuid)讓失效的凍結值只會被擋下、不會誤中別的資料。退路的成本是一次額外查詢;退路被命中的頻率反映的是同步的及時性——頻率高代表催同步的時機需要調整(見下節)。
第二題的答案若為「不存在」,凍結參照是唯一手段,但它的有效期等於上游不做重建操作的期間。有效期內凍結合法;跨越有效期的功能需要改走查詢路徑——以業務鍵(訂單號、合約號這類人為穩定的識別碼)重新定位,而非依賴上游的技術 id。
自持狀態與可導出狀態:遷移分工
上游身份轉移(合併、拆分)發生後,下游持有的多份「以上游 id 為 key」的狀態面臨遷移問題。遷移判準一個問題就夠:這份狀態消失後,能否單靠向上游重新查詢完整重建?
| 答案 | 分類 | 遷移策略 | 錯分的代價 |
|---|---|---|---|
| 能 | 可導出 | 不搬家,讓既有同步機制自然收斂 | 誤當自持 → 手工搬移引入第二個資料來源、搬移邏輯與同步邏輯分岔 |
| 不能 | 自持 | 身份轉移時通知搬家,設明確的轉移入口方法 | 誤當可導出 → 上游沒有這份資料、同步等不到救援、功能級事故 |
兩類狀態的程式碼形狀差異清楚:自持狀態有一個「身份轉移通知」的入口方法,且它是所有身份轉移操作的必經站;可導出狀態沒有任何遷移程式碼,只有既有的同步迴圈。
可導出狀態「等下一輪同步」的延遲若不可接受,正確的加速做法是催一次既有同步——走同一條同步路徑、只是提早跑。催同步與手工搬移的差異在於:催同步後資料來源仍然只有上游這一份,手工搬移會製造第二份——兩份資料的分岔只是時間問題。催同步時注意同步路徑內部的順序依賴(路徑內的過濾機制可能依賴某份前置資料的新鮮度),順序錯了催出來的是空結果。
自持狀態的每一份都是一筆負債,債主是所有會改變上游身份的操作。盤點自持狀態清單、逐項問「上游為什麼不保存這個」——若上游未來補上這份資料,自持狀態就能降級為投影,遷移程式碼隨之刪除。這個分類判準與 讀模型的升級判準 同構:讀模型問「讀的形狀還是聚合根的形狀」,這裡問「這份狀態的真相在誰手上」。
凍結快照的正當用途
凍結參照應消滅、活解析應取代——這條原則有一個明確的邊界:呈現歷史事實的快照是凍結的正當用途。
| 用途 | 正確形態 |
|---|---|
| 呈現歷史事實(結帳當下的價格) | 凍結 snapshot——上游變動不該影響它 |
| 回寫當前狀態(取消、追加) | 活解析——必須命中上游當前的資料 |
同一筆記錄可以同時包含兩種欄位。出事的案例往往是把「顯示用的凍結值」順手拿去做「回寫用的定位」——凍結值在這個用途下跨越了它的有效期,失效的時機由上游的重建行為決定、持有端察覺不到。凍結時機的判準在 entity 與 value object 的判準 與 狀態轉換與稽核軌跡 各自從身份語意和稽核面到達同一個結論:歷史記錄反映事件發生當下的世界。
判讀訊號
- 特定操作之後某些功能靜默失效(回寫目標不存在、查詢結果為空),且單元測試全綠——跨邊界參照的 id 可能已死亡。先確認上游操作的「保留 vs 重建」語意,再看持有端的解析時機。
- 修復 bug 只搬了一層 id、遺漏另一層——同一個業務動詞涉及多層資料(單據 + 明細 + 記錄),每層的重建行為要逐層實測、修復的完整性以層數計。
- 手工搬移的程式碼與同步邏輯在同一份可導出狀態上各寫一次——搬移是多餘的第二份資料來源,改成催一次同步。
- 同步催了卻拿到空結果——同步路徑有內部順序依賴(過濾器讀的前置資料還是舊的),催同步前先確保前置資料已更新。
- 自持狀態的數量持續增長——逐項問「上游為什麼不保存」,每消除一項就少一類遷移 bug。
下一步路由
- 身份語意的入口判準 → entity 與 value object 的判準
- 凍結與稽核的時間軸判準 → 狀態轉換與稽核軌跡
- 可導出狀態與讀模型的同構關係 → 讀模型的升級判準
- 凍結參照在測試中的結構盲區 → T.C5 凍結參照失效被 stub 遮蔽
- 假後端模擬重建行為 → 語意級假後端與流程測試
- 參照層的 case → 跨邊界參照的生命週期
- 遷移層的 case → 自持狀態與可導出狀態