自持狀態與可導出狀態:上游身份轉移時,誰要搬家、誰自動對齊
觸發場景:POS App 前端有多個服務各自持有「以單據 id 為 key」的狀態——輪詢的差異比對基準、事件追蹤記錄、當前品項快照。後端合併兩張單據後,舊 id 全部作廢,這些狀態何去何從? 疑問來源:最初的直覺是「全部搬家」——把每一份狀態的 key 都改成新 id。實作後發現有些搬了是白搬、有些不搬會出大事,差異在哪? 整理目的:把「上游身份轉移時的前端狀態遷移」整理成一個二分判準:自持 vs 可導出。 本文邊界:讀側設計的完整階梯見 讀模型的升級判準。
1. 三份狀態、兩種命運
合併發生後,前端各服務持有的狀態逐一檢視:
| 狀態 | 性質 | 遷移策略 | 不遷移的後果 |
|---|---|---|---|
| 輪詢差異比對基準 | 自持——「上一輪看到什麼」只存在於前端記憶,上游沒有這份資料 | 收到合併通知時把基準搬到新 id 下 | 新 id 查無基準 → 整張單據被誤判為新增 → 重複列印 |
| 事件追蹤記錄 | 自持——事件當下的內容與處理進度,上游不保存這個視角 | 通知搬家(改掛新 id) | 以 id 查詢的畫面從此找不到記錄 |
| 當前品項快照 | 可導出——每一輪輪詢整批重建自上游 | 不搬,下一輪同步自動對齊 | 至多一個輪詢週期的短暫過時 |
判準一句話:這份狀態消失後,能不能單靠向上游重新查詢完整重建? 能 → 可導出,讓同步機制自然收斂;不能 → 自持,身份轉移時必須主動遷移。
2. 兩個方向的分類錯誤
把自持誤當可導出(沒搬家):差異比對基準等不到任何同步來拯救——上游根本沒有「你上次看到什麼」的資料。結果是功能級事故(重複列印、記錄失聯)。
把可導出誤當自持(多搬家):對快照做手工搬移,短期看似加速對齊,實際引入了第二個資料來源——搬移邏輯與同步邏輯對同一份狀態各寫一次,兩者的分岔遲早出現。本案初版就搬了快照,後來發現搬進去的是舊內容(明細 id 還是死的),真正的新內容要等下一輪同步——手工搬移只製造了「看起來已對齊」的假象。
正確形態裡兩類狀態的程式碼形狀差異很清楚:自持狀態有一個「身份轉移通知」的入口方法;可導出狀態沒有任何遷移程式碼,只有既有的同步迴圈。
3. 加速對齊的正確位置:催一次同步,而不是代替同步
可導出狀態「等下一輪」的延遲若不可接受(本案:合併後使用者可能立刻取消品項,需要新明細 id),正確做法不是手工搬——是立即觸發一次同步。同一條同步路徑、只是提早跑。
這裡還埋著一個順序陷阱:同步路徑內部有依賴(輪詢用本地單據列表過濾資料),催同步之前要先讓依賴就緒(先同步列表、再刷新快照),否則催出來的是空結果、反而污染比對基準。「催同步」不是一個呼叫,是一小段有順序約束的編排——這個順序錯誤實際發生過,由流程測試首跑抓到(T.C6)。
4. 與讀模型階梯的關係
「可導出狀態」就是讀模型精神在前端的形態:它是上游真相的投影,正確性靠「可隨時重建」保證,而不是靠維護它的每一次增量修改都正確。判準也同構——讀模型的升級判準問「讀的形狀還是 aggregate(聚合根)的形狀」,這裡問「這份狀態的真相在誰手上」。真相在上游 → 投影化、別自持;真相只在本端 → 承認自持、為它設計完整的生命週期(包含身份轉移)。
長期方向:自持狀態越少越好。本案的追蹤記錄之所以必須自持,是因為上游不保存事件的處理進度——若上游未來補上這份資料,追蹤記錄就能降級為投影,「合併搬家」的程式碼隨之刪除。每一份自持狀態都是一筆負債,債主是所有會改變上游身份的操作。
5. 可複用的判準
- 上游身份轉移時,對每份以上游 id 為 key 的狀態問:「消失後能否單靠重新查詢完整重建?」——能,不搬;不能,通知搬家。
- 可導出狀態的加速對齊 = 催一次既有同步(注意內部順序依賴),不是另寫搬移邏輯。
- 自持狀態要有明確的「身份轉移」入口,且它是所有身份轉移操作(合併、拆分)的必經站。
- 盤點自持狀態清單、逐項問「上游為什麼不保存這個」——每消除一項,就少一類遷移 bug。
下一步
- 自持 vs 可導出的遷移判準(理論層)→ 跨邊界參照與狀態所有權
- 讀側階梯的理論層 → 讀模型的升級判準
- 身份轉移的參照層問題 → 跨邊界參照的生命週期
- 順序陷阱被抓到的過程 → T.C6 流程測試首跑抓到順序 bug