<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Reference on Tarragon</title><link>https://tarrragon.github.io/blog/tags/reference/</link><description>Recent content in Reference on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 17 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/reference/index.xml" rel="self" type="application/rss+xml"/><item><title>跨邊界參照與狀態所有權</title><link>https://tarrragon.github.io/blog/ddd/cross-boundary-reference-ownership/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/cross-boundary-reference-ownership/</guid><description>&lt;p>領域模型跨越邊界持有的每一個 id，有效性由上游行為決定、持有端控制不了。上游把「更新」實作成「刪除重建」是實作自由，下游用推理猜「它應該保留」擋不住——合法的出處只有契約文件或一次實測取證。這條事實推導出本章三個判準的共同源頭：跨邊界參照的設計，從「對方會做什麼」開始、而不是從「我要存什麼」開始。&lt;/p>
&lt;p>本章承接 &lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a> 的身份語意討論，延伸到身份跨越系統邊界之後的生存問題；與 &lt;a href="https://tarrragon.github.io/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡&lt;/a> 的凍結端點段落互補——凍結是時間軸上的問題（何時），本章談的是空間軸上的問題（誰的 id、誰決定它活著）。&lt;/p>
&lt;h2 id="參照有效性三問">參照有效性三問&lt;/h2>
&lt;p>下游持有上游資料的 id 時，設計階段逐一回答：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>對方的哪些操作會讓這個 id 死亡？&lt;/strong> 合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都會讓舊 id 失效。盲區集中在業務動詞與實作的語意落差：同一個「合併」，有的實作是搬資料、有的實作是建新刪舊。持有端用前者的假設設計、遇到後者的實作時靜默失效。&lt;/li>
&lt;li>&lt;strong>有沒有一個跨越這些操作仍不變的身份？&lt;/strong> 如果存在——以它為錨、其餘參照動態解析。如果沒有——凍結參照只能當一次性用途、跨操作的功能改走查詢。&lt;/li>
&lt;li>&lt;strong>「不變」的出處是什麼？&lt;/strong> 文件說的、程式碼推理的、實測證實的——只有第三種算數。把實測結果固化為測試資產（&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/real-backend-verification/" data-link-title="真實後端驗證測試" data-link-desc="服務無法本機啟動、只有共用測試環境（staging）時，把對真實後端的行為驗證寫成常駐測試：離線降級為跳過、憑證失效必須紅燈，讓假後端固化的行為假設有地方對真實後端驗證">真實後端驗證測試&lt;/a>），從此不依賴任何人的記憶。&lt;/li>
&lt;/ol>
&lt;p>三個問題的回答決定參照的設計形態。未回答就動工的代價是靜默失效——操作對著死 id 回寫，回傳值看起來正常（目標不存在也沒有噪音），功能在特定操作之後全部失效、直到使用者回報。凍結參照在測試中的結構盲區另見 &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5&lt;/a>：由測試餵資料的 stub 同時控制凍結值與回應資料，讓「id 死亡」這個狀態在測試裡不可能出現。&lt;/p>
&lt;h2 id="穩定身份為錨解引用延後到使用時">穩定身份為錨、解引用延後到使用時&lt;/h2>
&lt;p>第二題的答案若為「存在」，設計形態收斂為：持有穩定身份、其餘參照在使用時動態解析。&lt;/p>
&lt;p>「reference by identity + 解引用時機延後」把「id → 實體」的解析從持有時延後到使用時，讓解析結果反映上游當前的真相。額外的韌性：即使身份轉移發生在本端不知情的時候（另一台裝置觸發合併），使用時解析照樣找到新家。&lt;/p>
&lt;p>查無結果時退回凍結值，搭配存在性檢查兜底——穩定身份的值域（如 uuid）讓失效的凍結值只會被擋下、不會誤中別的資料。退路的成本是一次額外查詢；退路被命中的頻率反映的是同步的及時性——頻率高代表催同步的時機需要調整（見下節）。&lt;/p>
&lt;p>第二題的答案若為「不存在」，凍結參照是唯一手段，但它的有效期等於上游不做重建操作的期間。有效期內凍結合法；跨越有效期的功能需要改走查詢路徑——以業務鍵（訂單號、合約號這類人為穩定的識別碼）重新定位，而非依賴上游的技術 id。&lt;/p>
&lt;h2 id="自持狀態與可導出狀態遷移分工">自持狀態與可導出狀態：遷移分工&lt;/h2>
&lt;p>上游身份轉移（合併、拆分）發生後，下游持有的多份「以上游 id 為 key」的狀態面臨遷移問題。遷移判準一個問題就夠：&lt;strong>這份狀態消失後，能否單靠向上游重新查詢完整重建？&lt;/strong>&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>答案&lt;/th>
 &lt;th>分類&lt;/th>
 &lt;th>遷移策略&lt;/th>
 &lt;th>錯分的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>能&lt;/td>
 &lt;td>可導出&lt;/td>
 &lt;td>不搬家，讓既有同步機制自然收斂&lt;/td>
 &lt;td>誤當自持 → 手工搬移引入第二個資料來源、搬移邏輯與同步邏輯分岔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不能&lt;/td>
 &lt;td>自持&lt;/td>
 &lt;td>身份轉移時通知搬家，設明確的轉移入口方法&lt;/td>
 &lt;td>誤當可導出 → 上游沒有這份資料、同步等不到救援、功能級事故&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩類狀態的程式碼形狀差異清楚：自持狀態有一個「身份轉移通知」的入口方法，且它是所有身份轉移操作的必經站；可導出狀態沒有任何遷移程式碼，只有既有的同步迴圈。&lt;/p>
&lt;p>可導出狀態「等下一輪同步」的延遲若不可接受，正確的加速做法是&lt;strong>催一次既有同步&lt;/strong>——走同一條同步路徑、只是提早跑。催同步與手工搬移的差異在於：催同步後資料來源仍然只有上游這一份，手工搬移會製造第二份——兩份資料的分岔只是時間問題。催同步時注意同步路徑內部的順序依賴（路徑內的過濾機制可能依賴某份前置資料的新鮮度），順序錯了催出來的是空結果。&lt;/p>
&lt;p>自持狀態的每一份都是一筆負債，債主是所有會改變上游身份的操作。盤點自持狀態清單、逐項問「上游為什麼不保存這個」——若上游未來補上這份資料，自持狀態就能降級為投影，遷移程式碼隨之刪除。這個分類判準與 &lt;a href="https://tarrragon.github.io/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準&lt;/a> 同構：讀模型問「讀的形狀還是聚合根的形狀」，這裡問「這份狀態的真相在誰手上」。&lt;/p>
&lt;h2 id="凍結快照的正當用途">凍結快照的正當用途&lt;/h2>
&lt;p>凍結參照應消滅、活解析應取代——這條原則有一個明確的邊界：呈現歷史事實的快照是凍結的正當用途。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>用途&lt;/th>
 &lt;th>正確形態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>呈現歷史事實（結帳當下的價格）&lt;/td>
 &lt;td>凍結 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a>——上游變動不該影響它&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回寫當前狀態（取消、追加）&lt;/td>
 &lt;td>活解析——必須命中上游當前的資料&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>同一筆記錄可以同時包含兩種欄位。出事的案例往往是把「顯示用的凍結值」順手拿去做「回寫用的定位」——凍結值在這個用途下跨越了它的有效期，失效的時機由上游的重建行為決定、持有端察覺不到。凍結時機的判準在 &lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡&lt;/a> 各自從身份語意和稽核面到達同一個結論：歷史記錄反映事件發生當下的世界。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>特定操作之後某些功能靜默失效（回寫目標不存在、查詢結果為空），且單元測試全綠——跨邊界參照的 id 可能已死亡。先確認上游操作的「保留 vs 重建」語意，再看持有端的解析時機。&lt;/li>
&lt;li>修復 bug 只搬了一層 id、遺漏另一層——同一個業務動詞涉及多層資料（單據 + 明細 + 記錄），每層的重建行為要逐層實測、修復的完整性以層數計。&lt;/li>
&lt;li>手工搬移的程式碼與同步邏輯在同一份可導出狀態上各寫一次——搬移是多餘的第二份資料來源，改成催一次同步。&lt;/li>
&lt;li>同步催了卻拿到空結果——同步路徑有內部順序依賴（過濾器讀的前置資料還是舊的），催同步前先確保前置資料已更新。&lt;/li>
&lt;li>自持狀態的數量持續增長——逐項問「上游為什麼不保存」，每消除一項就少一類遷移 bug。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>身份語意的入口判準 → &lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a>&lt;/li>
&lt;li>凍結與稽核的時間軸判準 → &lt;a href="https://tarrragon.github.io/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡&lt;/a>&lt;/li>
&lt;li>可導出狀態與讀模型的同構關係 → &lt;a href="https://tarrragon.github.io/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準&lt;/a>&lt;/li>
&lt;li>凍結參照在測試中的結構盲區 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽&lt;/a>&lt;/li>
&lt;li>假後端模擬重建行為 → &lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試&lt;/a>&lt;/li>
&lt;li>參照層的 case → &lt;a href="https://tarrragon.github.io/blog/work-log/pos_cross_boundary_reference_lifecycle/" data-link-title="跨邊界參照的生命週期：前端凍結的 id，死活由後端決定" data-link-desc="POS App 的前端把後端資料的 id 凍結在本地追蹤記錄裡，後端的「合併」操作會重建資料——舊 id 全部失效，取消與追加功能無聲死亡。判準：跨邊界持有的每一個參照，都要回答「對方的哪些操作會讓它死」；穩定身份是實測出來的事實，不是推理出來的假設。">跨邊界參照的生命週期&lt;/a>&lt;/li>
&lt;li>遷移層的 case → &lt;a href="https://tarrragon.github.io/blog/work-log/pos_held_vs_derived_state_migration/" data-link-title="自持狀態與可導出狀態：上游身份轉移時，誰要搬家、誰自動對齊" data-link-desc="後端合併操作讓資料換了身份，前端持有的多份「以舊 id 為 key」的狀態怎麼辦？POS App 的答案分兩類：能從上游重新導出的狀態不用管、下一輪同步自動對齊；必須自行持有的狀態（差異比對基準、追蹤記錄）才需要通知搬家。分類錯誤的代價是兩個方向的 bug。">自持狀態與可導出狀態&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>領域模型跨越邊界持有的每一個 id，有效性由上游行為決定、持有端控制不了。上游把「更新」實作成「刪除重建」是實作自由，下游用推理猜「它應該保留」擋不住——合法的出處只有契約文件或一次實測取證。這條事實推導出本章三個判準的共同源頭：跨邊界參照的設計，從「對方會做什麼」開始、而不是從「我要存什麼」開始。</p>
<p>本章承接 <a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a> 的身份語意討論，延伸到身份跨越系統邊界之後的生存問題；與 <a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a> 的凍結端點段落互補——凍結是時間軸上的問題（何時），本章談的是空間軸上的問題（誰的 id、誰決定它活著）。</p>
<h2 id="參照有效性三問">參照有效性三問</h2>
<p>下游持有上游資料的 id 時，設計階段逐一回答：</p>
<ol>
<li><strong>對方的哪些操作會讓這個 id 死亡？</strong> 合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都會讓舊 id 失效。盲區集中在業務動詞與實作的語意落差：同一個「合併」，有的實作是搬資料、有的實作是建新刪舊。持有端用前者的假設設計、遇到後者的實作時靜默失效。</li>
<li><strong>有沒有一個跨越這些操作仍不變的身份？</strong> 如果存在——以它為錨、其餘參照動態解析。如果沒有——凍結參照只能當一次性用途、跨操作的功能改走查詢。</li>
<li><strong>「不變」的出處是什麼？</strong> 文件說的、程式碼推理的、實測證實的——只有第三種算數。把實測結果固化為測試資產（<a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a>、<a href="/blog/testing/03-protocol-integration-test/real-backend-verification/" data-link-title="真實後端驗證測試" data-link-desc="服務無法本機啟動、只有共用測試環境（staging）時，把對真實後端的行為驗證寫成常駐測試：離線降級為跳過、憑證失效必須紅燈，讓假後端固化的行為假設有地方對真實後端驗證">真實後端驗證測試</a>），從此不依賴任何人的記憶。</li>
</ol>
<p>三個問題的回答決定參照的設計形態。未回答就動工的代價是靜默失效——操作對著死 id 回寫，回傳值看起來正常（目標不存在也沒有噪音），功能在特定操作之後全部失效、直到使用者回報。凍結參照在測試中的結構盲區另見 <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5</a>：由測試餵資料的 stub 同時控制凍結值與回應資料，讓「id 死亡」這個狀態在測試裡不可能出現。</p>
<h2 id="穩定身份為錨解引用延後到使用時">穩定身份為錨、解引用延後到使用時</h2>
<p>第二題的答案若為「存在」，設計形態收斂為：持有穩定身份、其餘參照在使用時動態解析。</p>
<p>「reference by identity + 解引用時機延後」把「id → 實體」的解析從持有時延後到使用時，讓解析結果反映上游當前的真相。額外的韌性：即使身份轉移發生在本端不知情的時候（另一台裝置觸發合併），使用時解析照樣找到新家。</p>
<p>查無結果時退回凍結值，搭配存在性檢查兜底——穩定身份的值域（如 uuid）讓失效的凍結值只會被擋下、不會誤中別的資料。退路的成本是一次額外查詢；退路被命中的頻率反映的是同步的及時性——頻率高代表催同步的時機需要調整（見下節）。</p>
<p>第二題的答案若為「不存在」，凍結參照是唯一手段，但它的有效期等於上游不做重建操作的期間。有效期內凍結合法；跨越有效期的功能需要改走查詢路徑——以業務鍵（訂單號、合約號這類人為穩定的識別碼）重新定位，而非依賴上游的技術 id。</p>
<h2 id="自持狀態與可導出狀態遷移分工">自持狀態與可導出狀態：遷移分工</h2>
<p>上游身份轉移（合併、拆分）發生後，下游持有的多份「以上游 id 為 key」的狀態面臨遷移問題。遷移判準一個問題就夠：<strong>這份狀態消失後，能否單靠向上游重新查詢完整重建？</strong></p>
<table>
  <thead>
      <tr>
          <th>答案</th>
          <th>分類</th>
          <th>遷移策略</th>
          <th>錯分的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>能</td>
          <td>可導出</td>
          <td>不搬家，讓既有同步機制自然收斂</td>
          <td>誤當自持 → 手工搬移引入第二個資料來源、搬移邏輯與同步邏輯分岔</td>
      </tr>
      <tr>
          <td>不能</td>
          <td>自持</td>
          <td>身份轉移時通知搬家，設明確的轉移入口方法</td>
          <td>誤當可導出 → 上游沒有這份資料、同步等不到救援、功能級事故</td>
      </tr>
  </tbody>
</table>
<p>兩類狀態的程式碼形狀差異清楚：自持狀態有一個「身份轉移通知」的入口方法，且它是所有身份轉移操作的必經站；可導出狀態沒有任何遷移程式碼，只有既有的同步迴圈。</p>
<p>可導出狀態「等下一輪同步」的延遲若不可接受，正確的加速做法是<strong>催一次既有同步</strong>——走同一條同步路徑、只是提早跑。催同步與手工搬移的差異在於：催同步後資料來源仍然只有上游這一份，手工搬移會製造第二份——兩份資料的分岔只是時間問題。催同步時注意同步路徑內部的順序依賴（路徑內的過濾機制可能依賴某份前置資料的新鮮度），順序錯了催出來的是空結果。</p>
<p>自持狀態的每一份都是一筆負債，債主是所有會改變上游身份的操作。盤點自持狀態清單、逐項問「上游為什麼不保存這個」——若上游未來補上這份資料，自持狀態就能降級為投影，遷移程式碼隨之刪除。這個分類判準與 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a> 同構：讀模型問「讀的形狀還是聚合根的形狀」，這裡問「這份狀態的真相在誰手上」。</p>
<h2 id="凍結快照的正當用途">凍結快照的正當用途</h2>
<p>凍結參照應消滅、活解析應取代——這條原則有一個明確的邊界：呈現歷史事實的快照是凍結的正當用途。</p>
<table>
  <thead>
      <tr>
          <th>用途</th>
          <th>正確形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>呈現歷史事實（結帳當下的價格）</td>
          <td>凍結 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a>——上游變動不該影響它</td>
      </tr>
      <tr>
          <td>回寫當前狀態（取消、追加）</td>
          <td>活解析——必須命中上游當前的資料</td>
      </tr>
  </tbody>
</table>
<p>同一筆記錄可以同時包含兩種欄位。出事的案例往往是把「顯示用的凍結值」順手拿去做「回寫用的定位」——凍結值在這個用途下跨越了它的有效期，失效的時機由上游的重建行為決定、持有端察覺不到。凍結時機的判準在 <a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a> 與 <a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a> 各自從身份語意和稽核面到達同一個結論：歷史記錄反映事件發生當下的世界。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>特定操作之後某些功能靜默失效（回寫目標不存在、查詢結果為空），且單元測試全綠——跨邊界參照的 id 可能已死亡。先確認上游操作的「保留 vs 重建」語意，再看持有端的解析時機。</li>
<li>修復 bug 只搬了一層 id、遺漏另一層——同一個業務動詞涉及多層資料（單據 + 明細 + 記錄），每層的重建行為要逐層實測、修復的完整性以層數計。</li>
<li>手工搬移的程式碼與同步邏輯在同一份可導出狀態上各寫一次——搬移是多餘的第二份資料來源，改成催一次同步。</li>
<li>同步催了卻拿到空結果——同步路徑有內部順序依賴（過濾器讀的前置資料還是舊的），催同步前先確保前置資料已更新。</li>
<li>自持狀態的數量持續增長——逐項問「上游為什麼不保存」，每消除一項就少一類遷移 bug。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>身份語意的入口判準 → <a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a></li>
<li>凍結與稽核的時間軸判準 → <a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a></li>
<li>可導出狀態與讀模型的同構關係 → <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a></li>
<li>凍結參照在測試中的結構盲區 → <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽</a></li>
<li>假後端模擬重建行為 → <a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a></li>
<li>參照層的 case → <a href="/blog/work-log/pos_cross_boundary_reference_lifecycle/" data-link-title="跨邊界參照的生命週期：前端凍結的 id，死活由後端決定" data-link-desc="POS App 的前端把後端資料的 id 凍結在本地追蹤記錄裡，後端的「合併」操作會重建資料——舊 id 全部失效，取消與追加功能無聲死亡。判準：跨邊界持有的每一個參照，都要回答「對方的哪些操作會讓它死」；穩定身份是實測出來的事實，不是推理出來的假設。">跨邊界參照的生命週期</a></li>
<li>遷移層的 case → <a href="/blog/work-log/pos_held_vs_derived_state_migration/" data-link-title="自持狀態與可導出狀態：上游身份轉移時，誰要搬家、誰自動對齊" data-link-desc="後端合併操作讓資料換了身份，前端持有的多份「以舊 id 為 key」的狀態怎麼辦？POS App 的答案分兩類：能從上游重新導出的狀態不用管、下一輪同步自動對齊；必須自行持有的狀態（差異比對基準、追蹤記錄）才需要通知搬家。分類錯誤的代價是兩個方向的 bug。">自持狀態與可導出狀態</a></li>
</ul>
]]></content:encoded></item><item><title>跨邊界參照的生命週期：前端凍結的 id，死活由後端決定</title><link>https://tarrragon.github.io/blog/work-log/pos_cross_boundary_reference_lifecycle/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/pos_cross_boundary_reference_lifecycle/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：POS App 的前端為後端事件建立本地追蹤記錄，記錄裡存了事件當下的兩個後端 id（單據 id、明細列 id），後續的取消、追加操作用這兩個 id 回寫。後端執行「合併兩張單據」後，這些操作全部失效。
&lt;strong>疑問來源&lt;/strong>：合併後前端已把記錄「改掛」到新單據——為什麼還是壞？
&lt;strong>整理目的&lt;/strong>：把「跨邊界參照的生命週期」整理成可判準的問題清單：哪些 id 會死、哪個 id 不死、持有端該怎麼設計。
&lt;strong>本文邊界&lt;/strong>：測試層的對策（語意級假後端）另見 &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-事故解剖兩層-id兩種命運">1. 事故解剖：兩層 id、兩種命運&lt;/h2>
&lt;p>後端「合併兩張單據」的實際行為（埋 log 實測，非文件推理）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料&lt;/th>
 &lt;th>合併時的命運&lt;/th>
 &lt;th>對前端凍結參照的意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>單據&lt;/td>
 &lt;td>建新單據、刪舊單據&lt;/td>
 &lt;td>凍結的單據 id 死亡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>明細列&lt;/td>
 &lt;td>全部重建（全新 id）&lt;/td>
 &lt;td>凍結的明細 id 死亡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件記錄&lt;/td>
 &lt;td>&lt;strong>保留，只改外鍵&lt;/strong>&lt;/td>
 &lt;td>事件 id 是唯一跨合併不變的身份&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一版修復只處理了單據層（合併後通知本地記錄改掛新單據 id），明細層的 id 照樣死——因為修復者的心智模型裡「合併」是搬家，而後端的實作是&lt;strong>重生&lt;/strong>。同一個業務動詞，兩端對「什麼保留、什麼重建」的理解不同，這正是跨邊界參照最危險的地方。&lt;/p>
&lt;h2 id="2-判準每一個跨邊界參照都要回答三個問題">2. 判準：每一個跨邊界參照都要回答三個問題&lt;/h2>
&lt;p>前端（或任何下游系統）持有上游資料的 id 時，這個參照的有效性完全由上游的行為決定。設計時逐一回答：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>對方的哪些操作會讓這個 id 死亡？&lt;/strong>（合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都是 id 屠宰場）&lt;/li>
&lt;li>&lt;strong>有沒有一個跨越這些操作仍不變的身份？&lt;/strong>（本案是事件記錄 id——後端只改它的外鍵）&lt;/li>
&lt;li>&lt;strong>這個「不變」是文件說的、推理出的，還是實測證實的？&lt;/strong>——只有第三種算數。上游把「更新」實作成「刪除重建」是實作自由，下游的推理攔不住。&lt;/li>
&lt;/ol>
&lt;p>第二題的答案決定架構：存在穩定身份 → 以它為錨、其餘參照動態解析；不存在 → 凍結參照只能當一次性用途，跨操作的功能要改走查詢。&lt;/p>
&lt;h2 id="3-模式穩定身份為錨活解析為主凍結值為退路">3. 模式：穩定身份為錨、活解析為主、凍結值為退路&lt;/h2>
&lt;p>修復後的形態：&lt;/p>
&lt;ul>
&lt;li>本地記錄仍凍結事件當下的 id（顯示用途夠用，也是最後退路）&lt;/li>
&lt;li>需要&lt;strong>回寫&lt;/strong>的操作（取消、追加）不信任凍結值——以穩定身份（事件 id）向當前快照反查「現在有效的單據 id＋明細 id」&lt;/li>
&lt;li>查無（同步空窗、資料已消失）才退回凍結值，且下游有存在性檢查兜底——id 是 uuid，失效的凍結值只會被擋下，不會誤中別的資料&lt;/li>
&lt;/ul>
&lt;p>這個模式的通用名字是「reference by identity + 解引用時機延後」：把「id → 實體」的解析從&lt;strong>持有時&lt;/strong>延後到&lt;strong>使用時&lt;/strong>，讓解析結果永遠反映上游當前的真相。額外的紅利是跨端韌性——即使身份轉移發生在本端不知情的時候（另一台裝置觸發合併），使用時解析照樣找得到新家。&lt;/p>
&lt;h2 id="4-邊界情況凍結有凍結的正當用途">4. 邊界情況：凍結有凍結的正當用途&lt;/h2>
&lt;p>不是所有凍結參照都該消滅。同一個專案裡，「已結帳品項」的商品資訊就是&lt;strong>刻意凍結的快照&lt;/strong>——結帳當下的名稱與價格，本來就不該隨商品目錄後續的修改而變。判準的分水嶺：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>用途&lt;/th>
 &lt;th>正確形態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>呈現歷史事實（結帳當下的價格）&lt;/td>
 &lt;td>凍結快照——上游變動不該影響它&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回寫當前狀態（取消、追加）&lt;/td>
 &lt;td>活解析——必須命中上游當前的資料&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>同一筆記錄可以同時包含兩種欄位；出事的專案往往是把「顯示用的凍結快照」順手拿去做「回寫用的定位」。&lt;/p>
&lt;h2 id="5-可複用的判準">5. 可複用的判準&lt;/h2>
&lt;ol>
&lt;li>跨邊界持有的每個 id，設計時回答：誰會殺它、什麼不會死、證據是實測還是推理。&lt;/li>
&lt;li>回寫操作以穩定身份使用時解析；凍結值只做顯示與最後退路。&lt;/li>
&lt;li>上游動詞的「保留 vs 重建」語意要實測一次、固化為測試資產（假後端＋真實後端驗證），不要留在人的記憶。&lt;/li>
&lt;li>區分快照欄位的用途：呈現歷史 → 凍結正確；定位回寫 → 凍結是地雷。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;ul>
&lt;li>測試層怎麼防 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試&lt;/a>&lt;/li>
&lt;li>參照有效性與狀態所有權的理論層 → &lt;a href="https://tarrragon.github.io/blog/ddd/cross-boundary-reference-ownership/" data-link-title="跨邊界參照與狀態所有權" data-link-desc="下游持有上游資料的 id、操作靠這個 id 回寫時：上游的哪些操作會讓 id 死亡、有沒有跨操作不變的穩定身份、身份轉移後本端的狀態搬不搬家——參照設計的判準與遷移分工">跨邊界參照與狀態所有權&lt;/a>&lt;/li>
&lt;li>身份判準的理論層 → &lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a>&lt;/li>
&lt;li>快照的概念卡 → &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：POS App 的前端為後端事件建立本地追蹤記錄，記錄裡存了事件當下的兩個後端 id（單據 id、明細列 id），後續的取消、追加操作用這兩個 id 回寫。後端執行「合併兩張單據」後，這些操作全部失效。
<strong>疑問來源</strong>：合併後前端已把記錄「改掛」到新單據——為什麼還是壞？
<strong>整理目的</strong>：把「跨邊界參照的生命週期」整理成可判準的問題清單：哪些 id 會死、哪個 id 不死、持有端該怎麼設計。
<strong>本文邊界</strong>：測試層的對策（語意級假後端）另見 <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽</a>。</p></blockquote>
<hr>
<h2 id="1-事故解剖兩層-id兩種命運">1. 事故解剖：兩層 id、兩種命運</h2>
<p>後端「合併兩張單據」的實際行為（埋 log 實測，非文件推理）：</p>
<table>
  <thead>
      <tr>
          <th>資料</th>
          <th>合併時的命運</th>
          <th>對前端凍結參照的意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單據</td>
          <td>建新單據、刪舊單據</td>
          <td>凍結的單據 id 死亡</td>
      </tr>
      <tr>
          <td>明細列</td>
          <td>全部重建（全新 id）</td>
          <td>凍結的明細 id 死亡</td>
      </tr>
      <tr>
          <td>事件記錄</td>
          <td><strong>保留，只改外鍵</strong></td>
          <td>事件 id 是唯一跨合併不變的身份</td>
      </tr>
  </tbody>
</table>
<p>第一版修復只處理了單據層（合併後通知本地記錄改掛新單據 id），明細層的 id 照樣死——因為修復者的心智模型裡「合併」是搬家，而後端的實作是<strong>重生</strong>。同一個業務動詞，兩端對「什麼保留、什麼重建」的理解不同，這正是跨邊界參照最危險的地方。</p>
<h2 id="2-判準每一個跨邊界參照都要回答三個問題">2. 判準：每一個跨邊界參照都要回答三個問題</h2>
<p>前端（或任何下游系統）持有上游資料的 id 時，這個參照的有效性完全由上游的行為決定。設計時逐一回答：</p>
<ol>
<li><strong>對方的哪些操作會讓這個 id 死亡？</strong>（合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都是 id 屠宰場）</li>
<li><strong>有沒有一個跨越這些操作仍不變的身份？</strong>（本案是事件記錄 id——後端只改它的外鍵）</li>
<li><strong>這個「不變」是文件說的、推理出的，還是實測證實的？</strong>——只有第三種算數。上游把「更新」實作成「刪除重建」是實作自由，下游的推理攔不住。</li>
</ol>
<p>第二題的答案決定架構：存在穩定身份 → 以它為錨、其餘參照動態解析；不存在 → 凍結參照只能當一次性用途，跨操作的功能要改走查詢。</p>
<h2 id="3-模式穩定身份為錨活解析為主凍結值為退路">3. 模式：穩定身份為錨、活解析為主、凍結值為退路</h2>
<p>修復後的形態：</p>
<ul>
<li>本地記錄仍凍結事件當下的 id（顯示用途夠用，也是最後退路）</li>
<li>需要<strong>回寫</strong>的操作（取消、追加）不信任凍結值——以穩定身份（事件 id）向當前快照反查「現在有效的單據 id＋明細 id」</li>
<li>查無（同步空窗、資料已消失）才退回凍結值，且下游有存在性檢查兜底——id 是 uuid，失效的凍結值只會被擋下，不會誤中別的資料</li>
</ul>
<p>這個模式的通用名字是「reference by identity + 解引用時機延後」：把「id → 實體」的解析從<strong>持有時</strong>延後到<strong>使用時</strong>，讓解析結果永遠反映上游當前的真相。額外的紅利是跨端韌性——即使身份轉移發生在本端不知情的時候（另一台裝置觸發合併），使用時解析照樣找得到新家。</p>
<h2 id="4-邊界情況凍結有凍結的正當用途">4. 邊界情況：凍結有凍結的正當用途</h2>
<p>不是所有凍結參照都該消滅。同一個專案裡，「已結帳品項」的商品資訊就是<strong>刻意凍結的快照</strong>——結帳當下的名稱與價格，本來就不該隨商品目錄後續的修改而變。判準的分水嶺：</p>
<table>
  <thead>
      <tr>
          <th>用途</th>
          <th>正確形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>呈現歷史事實（結帳當下的價格）</td>
          <td>凍結快照——上游變動不該影響它</td>
      </tr>
      <tr>
          <td>回寫當前狀態（取消、追加）</td>
          <td>活解析——必須命中上游當前的資料</td>
      </tr>
  </tbody>
</table>
<p>同一筆記錄可以同時包含兩種欄位；出事的專案往往是把「顯示用的凍結快照」順手拿去做「回寫用的定位」。</p>
<h2 id="5-可複用的判準">5. 可複用的判準</h2>
<ol>
<li>跨邊界持有的每個 id，設計時回答：誰會殺它、什麼不會死、證據是實測還是推理。</li>
<li>回寫操作以穩定身份使用時解析；凍結值只做顯示與最後退路。</li>
<li>上游動詞的「保留 vs 重建」語意要實測一次、固化為測試資產（假後端＋真實後端驗證），不要留在人的記憶。</li>
<li>區分快照欄位的用途：呈現歷史 → 凍結正確；定位回寫 → 凍結是地雷。</li>
</ol>
<h2 id="下一步">下一步</h2>
<ul>
<li>測試層怎麼防 → <a href="/blog/testing/cases/stale-reference-stub-blindspot/" data-link-title="T.C5 凍結參照失效被 stub 遮蔽 — 測試全綠、功能全壞" data-link-desc="前端把後端資料的 id 凍結在本地記錄裡，後端的合併操作會重建資料、舊 id 全部失效 — 單元測試的 stub 由測試自己餵資料，永遠不會經歷「id 死亡」，bug 存在期間所有測試綠燈">T.C5 凍結參照失效被 stub 遮蔽</a>、<a href="/blog/testing/01-test-strategy-layers/semantic-fake-backend/" data-link-title="語意級假後端與流程測試" data-link-desc="bug 的成因是對後端行為的假設錯誤、由測試餵資料的 stub 驗證不出來時：建一個持有狀態、模擬已證實後端行為的假後端（test double 分類的 fake），讓流程測試走完整的多服務互動鏈">語意級假後端與流程測試</a></li>
<li>參照有效性與狀態所有權的理論層 → <a href="/blog/ddd/cross-boundary-reference-ownership/" data-link-title="跨邊界參照與狀態所有權" data-link-desc="下游持有上游資料的 id、操作靠這個 id 回寫時：上游的哪些操作會讓 id 死亡、有沒有跨操作不變的穩定身份、身份轉移後本端的狀態搬不搬家——參照設計的判準與遷移分工">跨邊界參照與狀態所有權</a></li>
<li>身份判準的理論層 → <a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a></li>
<li>快照的概念卡 → <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a></li>
</ul>
]]></content:encoded></item></channel></rss>