<?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>Knowledge Cards on Tarragon</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/</link><description>Recent content in Knowledge Cards on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/ddd/knowledge-cards/index.xml" rel="self" type="application/rss+xml"/><item><title>Invariant</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/</guid><description>&lt;p>不變式是在物件整個生命週期都必須為真的業務規則。狀態只能沿流程轉換、某幾個欄位被同一條規則綁住必須一起換、錯誤代碼必須屬於對應分類——這些條件只要有一條被違反、物件就處於業務上不該存在的狀態。不變式的存在是判定型別為領域模型而非&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋&lt;/a>的關鍵依據：有不變式的型別需要領域模型的設計投資，沒有的就是資料袋。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>不變式跟輸入驗證的責任不同。不變式守「這個物件能不能存在」——違反時物件不該被建出來；輸入驗證守「使用者輸入對不對」——違反時要好好告訴使用者。兩者混放會讓格式驗證塞進建構子、或存在條件放進 validator。不變式跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a> 有交集——歷史 snapshot 凍結的是過去規則下合法的狀態、回讀時不一定通過新規則的不變式。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>型別上出現「請用某方法修改」「此欄位勿直接改」的註解時，不變式已經到場、強制還停在文件層。建構子或方法的 &lt;code>throw&lt;/code> / &lt;code>assert&lt;/code> 是不變式在執行層工作的證據；grep 得到零個對應檢查時，是宣稱約束但未強制。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>同一條不變式可以落在文件層（註解）、型別層（介面簽名）或執行層（建構子檢查），層次決定違反時發生什麼——靜默通過、編譯失敗、當場拒絕。選擇依違反代價與建置成本折算。教學層展開見 &lt;a href="https://tarrragon.github.io/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>不變式是在物件整個生命週期都必須為真的業務規則。狀態只能沿流程轉換、某幾個欄位被同一條規則綁住必須一起換、錯誤代碼必須屬於對應分類——這些條件只要有一條被違反、物件就處於業務上不該存在的狀態。不變式的存在是判定型別為領域模型而非<a href="/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋</a>的關鍵依據：有不變式的型別需要領域模型的設計投資，沒有的就是資料袋。</p>
<h2 id="概念位置">概念位置</h2>
<p>不變式跟輸入驗證的責任不同。不變式守「這個物件能不能存在」——違反時物件不該被建出來；輸入驗證守「使用者輸入對不對」——違反時要好好告訴使用者。兩者混放會讓格式驗證塞進建構子、或存在條件放進 validator。不變式跟 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a> 有交集——歷史 snapshot 凍結的是過去規則下合法的狀態、回讀時不一定通過新規則的不變式。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>型別上出現「請用某方法修改」「此欄位勿直接改」的註解時，不變式已經到場、強制還停在文件層。建構子或方法的 <code>throw</code> / <code>assert</code> 是不變式在執行層工作的證據；grep 得到零個對應檢查時，是宣稱約束但未強制。</p>
<h2 id="設計責任">設計責任</h2>
<p>同一條不變式可以落在文件層（註解）、型別層（介面簽名）或執行層（建構子檢查），層次決定違反時發生什麼——靜默通過、編譯失敗、當場拒絕。選擇依違反代價與建置成本折算。教學層展開見 <a href="/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次</a>。</p>
]]></content:encoded></item><item><title>Entity</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/</guid><description>&lt;p>Entity 的同一性由身份定義：欄位可以全部改變、只要身份參照不變就是同一個；兩個欄位完全相同的 entity 仍然是兩個。這條定義推導出 entity 的設計形狀——有生命週期、狀態沿業務流程演進、變更要有路徑。跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object&lt;/a> 相反：value object 的同一性由內容定義、替換實例對系統沒有影響。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Entity 是領域模型的一種形態——型別先判定為領域模型（有&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式&lt;/a>）、再判定身份語意（entity 或 value object）。判準是「操作需不需要 identity-based 回寫」：取消、改量、退貨這類要精確指到特定實體的操作，需要 entity；內容比對就足夠的操作用 value object。同一個業務概念的身份語意會隨生命週期階段改變——每個轉折點重問一次判準。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>改量、取消這類操作用內容比對定位對象——同內容的其他實體會被誤中，這是模型該升級成 entity 的訊號。entity 的變更路徑通常經由領域方法——如果 entity 同時有領域方法與全開放的覆寫工具，變更路徑正在退化。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Entity 需要決定身份的來源（資料庫序號、外部系統 ID、業務規則產生的識別碼）、生命週期的階段（何時誕生、何時交棒、何時成為歷史事實需要 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&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;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;/p></description><content:encoded><![CDATA[<p>Entity 的同一性由身份定義：欄位可以全部改變、只要身份參照不變就是同一個；兩個欄位完全相同的 entity 仍然是兩個。這條定義推導出 entity 的設計形狀——有生命週期、狀態沿業務流程演進、變更要有路徑。跟 <a href="/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object</a> 相反：value object 的同一性由內容定義、替換實例對系統沒有影響。</p>
<h2 id="概念位置">概念位置</h2>
<p>Entity 是領域模型的一種形態——型別先判定為領域模型（有<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式</a>）、再判定身份語意（entity 或 value object）。判準是「操作需不需要 identity-based 回寫」：取消、改量、退貨這類要精確指到特定實體的操作，需要 entity；內容比對就足夠的操作用 value object。同一個業務概念的身份語意會隨生命週期階段改變——每個轉折點重問一次判準。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>改量、取消這類操作用內容比對定位對象——同內容的其他實體會被誤中，這是模型該升級成 entity 的訊號。entity 的變更路徑通常經由領域方法——如果 entity 同時有領域方法與全開放的覆寫工具，變更路徑正在退化。</p>
<h2 id="設計責任">設計責任</h2>
<p>Entity 需要決定身份的來源（資料庫序號、外部系統 ID、業務規則產生的識別碼）、生命週期的階段（何時誕生、何時交棒、何時成為歷史事實需要 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a>）、以及變更的路徑（領域方法的設計，見 <a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a>）。判準的完整展開見 <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>。</p>
]]></content:encoded></item><item><title>Value Object</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/</guid><description>&lt;p>Value object 的同一性由內容定義：內容相等就是同一個、替換一個內容相同的實例對系統沒有任何影響。要「改」就是造一個新值換上去——不可變。跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 相反：entity 的同一性由身份定義。相等性定義本身可以承載業務規則：「什麼算同一個」是業務決策寫進相等性定義的例子。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Value object 有兩個獨立的價值。第一個跟 entity 的判準有關——操作以內容為對象（累加、合併、比對、替換）就用 value object。第二個是語意封閉：把一個領域概念的合法運算限縮成封閉集合——差集裡的每個運算都是等著被誤用的 API。語意封閉的價值獨立於同一性判定、也獨立於容器型別的類別——&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋&lt;/a>裡照樣可以放語意封閉的 value object。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>一個領域概念以裸的通用型別跨模組流通（金額是 double、識別碼是 string）、而它的合法運算遠少於底層型別——語意封閉的價值已成立。封裝後要給原始值一個語意明確的官方出口（見 &lt;a href="https://tarrragon.github.io/blog/ddd/construction-path-design/" data-link-title="建構路徑設計" data-link-desc="工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。">建構路徑設計&lt;/a>）。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Value object 需要決定相等性定義（哪些欄位參與比對、參與本身就是業務規則）、不可變策略、以及封裝出口。枚舉是 value object 的一種——粒度判準看消費者需求、不看分類系統本身。判準的完整展開見 &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;/p></description><content:encoded><![CDATA[<p>Value object 的同一性由內容定義：內容相等就是同一個、替換一個內容相同的實例對系統沒有任何影響。要「改」就是造一個新值換上去——不可變。跟 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 相反：entity 的同一性由身份定義。相等性定義本身可以承載業務規則：「什麼算同一個」是業務決策寫進相等性定義的例子。</p>
<h2 id="概念位置">概念位置</h2>
<p>Value object 有兩個獨立的價值。第一個跟 entity 的判準有關——操作以內容為對象（累加、合併、比對、替換）就用 value object。第二個是語意封閉：把一個領域概念的合法運算限縮成封閉集合——差集裡的每個運算都是等著被誤用的 API。語意封閉的價值獨立於同一性判定、也獨立於容器型別的類別——<a href="/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋</a>裡照樣可以放語意封閉的 value object。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>一個領域概念以裸的通用型別跨模組流通（金額是 double、識別碼是 string）、而它的合法運算遠少於底層型別——語意封閉的價值已成立。封裝後要給原始值一個語意明確的官方出口（見 <a href="/blog/ddd/construction-path-design/" data-link-title="建構路徑設計" data-link-desc="工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。">建構路徑設計</a>）。</p>
<h2 id="設計責任">設計責任</h2>
<p>Value object 需要決定相等性定義（哪些欄位參與比對、參與本身就是業務規則）、不可變策略、以及封裝出口。枚舉是 value object 的一種——粒度判準看消費者需求、不看分類系統本身。判準的完整展開見 <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>。</p>
]]></content:encoded></item><item><title>Aggregate Root</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/</guid><description>&lt;p>聚合根是對外代表一組資料一致性的邊界物件。外部操作只跟聚合根互動、不直接碰內部的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object&lt;/a>；聚合根負責保證邊界內所有&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式&lt;/a>在每次操作結束時成立。一致性的範圍就是聚合的邊界——邊界內是同一個事務、邊界外是最終一致。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>聚合根是本模組尚未展開的章節主題（模組 backlog）。目前在教學章中以例子形態出現：一個 POS 專案的商品從扁平結構長成雙層聚合——商品作為聚合根、規格作為子層——欄位歸屬的判準是「兩個規格會不會不同」（&lt;a href="https://tarrragon.github.io/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a>）。聚合的邊界跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 的生命週期邊界是兩個獨立的設計決策。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>多個物件的修改必須一起成功或一起失敗、而這些物件目前散在不同的存取路徑——缺少聚合根把它們收在同一個一致性邊界裡。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>聚合根的邊界劃太大會把不相關的物件鎖在同一個事務、限制併發；劃太小會讓跨聚合的一致性問題推到應用層。邊界的判準是「哪些不變式必須在同一次操作內成立」。完整教學等 case 累積後展開。&lt;/p></description><content:encoded><![CDATA[<p>聚合根是對外代表一組資料一致性的邊界物件。外部操作只跟聚合根互動、不直接碰內部的 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 或 <a href="/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object</a>；聚合根負責保證邊界內所有<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式</a>在每次操作結束時成立。一致性的範圍就是聚合的邊界——邊界內是同一個事務、邊界外是最終一致。</p>
<h2 id="概念位置">概念位置</h2>
<p>聚合根是本模組尚未展開的章節主題（模組 backlog）。目前在教學章中以例子形態出現：一個 POS 專案的商品從扁平結構長成雙層聚合——商品作為聚合根、規格作為子層——欄位歸屬的判準是「兩個規格會不會不同」（<a href="/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a>）。聚合的邊界跟 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 的生命週期邊界是兩個獨立的設計決策。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>多個物件的修改必須一起成功或一起失敗、而這些物件目前散在不同的存取路徑——缺少聚合根把它們收在同一個一致性邊界裡。</p>
<h2 id="設計責任">設計責任</h2>
<p>聚合根的邊界劃太大會把不相關的物件鎖在同一個事務、限制併發；劃太小會讓跨聚合的一致性問題推到應用層。邊界的判準是「哪些不變式必須在同一次操作內成立」。完整教學等 case 累積後展開。</p>
]]></content:encoded></item><item><title>Data Bag</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/data-bag/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/data-bag/</guid><description>&lt;p>資料袋承擔資料的搬運與呈現：DTO、API model、UI state、設定物件。它的特徵是任何欄位組合都是合法狀態——修改其中一欄、其餘欄位的意義照舊成立。因為組合全部合法、逐欄位覆寫工具（copyWith、setter、builder）在資料袋上語意清晰、沒有代價。DDD 社群常用「貧血模型（anemic model）」指沒有行為的資料容器——語意接近、但資料袋是正確形態而非缺陷，判定依&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式&lt;/a>有無而定。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>資料袋跟領域模型是型別類別的兩端。判準是「有沒有不允許任意組合的欄位」——有就是領域模型、沒有就是資料袋。資料袋裡照樣可以放語意封閉的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object&lt;/a>（如 Money）；容器的類別判定跟欄位的封閉性是正交的兩條軸。資料袋可以隨業務演進升級成領域模型——升級時機由演化訊號決定、而非預測。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>型別的每個欄位都可以獨立修改且不影響其餘欄位的語意——資料袋。型別上出現「請用某方法修改」的註解但欄位仍全部 public 可寫——判定可能錯了、讀 &lt;a href="https://tarrragon.github.io/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a> 重新檢視。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>資料袋加上領域模型的儀式（工廠、私有建構、變更方法）只會製造沒有規則可守的 boilerplate。判斷準則是「這個型別有沒有需要強制的規則」——沒有就不要建領域模型。完整判準見 &lt;a href="https://tarrragon.github.io/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>資料袋承擔資料的搬運與呈現：DTO、API model、UI state、設定物件。它的特徵是任何欄位組合都是合法狀態——修改其中一欄、其餘欄位的意義照舊成立。因為組合全部合法、逐欄位覆寫工具（copyWith、setter、builder）在資料袋上語意清晰、沒有代價。DDD 社群常用「貧血模型（anemic model）」指沒有行為的資料容器——語意接近、但資料袋是正確形態而非缺陷，判定依<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式</a>有無而定。</p>
<h2 id="概念位置">概念位置</h2>
<p>資料袋跟領域模型是型別類別的兩端。判準是「有沒有不允許任意組合的欄位」——有就是領域模型、沒有就是資料袋。資料袋裡照樣可以放語意封閉的 <a href="/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object</a>（如 Money）；容器的類別判定跟欄位的封閉性是正交的兩條軸。資料袋可以隨業務演進升級成領域模型——升級時機由演化訊號決定、而非預測。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>型別的每個欄位都可以獨立修改且不影響其餘欄位的語意——資料袋。型別上出現「請用某方法修改」的註解但欄位仍全部 public 可寫——判定可能錯了、讀 <a href="/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a> 重新檢視。</p>
<h2 id="設計責任">設計責任</h2>
<p>資料袋加上領域模型的儀式（工廠、私有建構、變更方法）只會製造沒有規則可守的 boilerplate。判斷準則是「這個型別有沒有需要強制的規則」——沒有就不要建領域模型。完整判準見 <a href="/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a>。</p>
]]></content:encoded></item><item><title>Snapshot</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/</guid><description>&lt;p>Snapshot 是某一時刻的狀態複本、凍結之後不隨來源資料的後續變更而改動。歷史訂單記錄下單當時的商品名稱與價格——商品後續改名、下架、調價，訂單仍顯示當時購買的內容。跟 live 參照相反：live 參照指向來源的當前狀態、來源改了就跟著變。兩者的選擇由「這筆資料有沒有成為事實」決定。凍結的是狀態；已發生的事實本身由 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 承載——snapshot 記「當時的世界長什麼樣」、event 記「當時發生了什麼」。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Snapshot 跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity&lt;/a> 的生命週期有關——概念成為歷史事實之後、live 內容參照要凍結。凍結的同時身份參照通常繼續承重（退貨鍵、取消鍵）。Snapshot 也是稽核的端點——記錄事件發生當下的世界、而不是現在的世界（見 &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;p>歷史記錄的顯示內容跟著現行資料變動（商品改名、訂單明細跟著變）——參照凍結時機漏掉了。進行中的資料不跟著最新狀態走（購物車品項的價格不跟會員身分變動）——凍結太早。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>凍結時機由業務對「過去」的要求決定——進行中的操作持 live 參照是正確的、成為事實之後凍結。業務流程有多個確認階段時、凍結時機取決於哪個階段之後的漂移對下游不可接受。凍結判準的完整展開見 &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;/p></description><content:encoded><![CDATA[<p>Snapshot 是某一時刻的狀態複本、凍結之後不隨來源資料的後續變更而改動。歷史訂單記錄下單當時的商品名稱與價格——商品後續改名、下架、調價，訂單仍顯示當時購買的內容。跟 live 參照相反：live 參照指向來源的當前狀態、來源改了就跟著變。兩者的選擇由「這筆資料有沒有成為事實」決定。凍結的是狀態；已發生的事實本身由 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 承載——snapshot 記「當時的世界長什麼樣」、event 記「當時發生了什麼」。</p>
<h2 id="概念位置">概念位置</h2>
<p>Snapshot 跟 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a> 的生命週期有關——概念成為歷史事實之後、live 內容參照要凍結。凍結的同時身份參照通常繼續承重（退貨鍵、取消鍵）。Snapshot 也是稽核的端點——記錄事件發生當下的世界、而不是現在的世界（見 <a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a>）。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>歷史記錄的顯示內容跟著現行資料變動（商品改名、訂單明細跟著變）——參照凍結時機漏掉了。進行中的資料不跟著最新狀態走（購物車品項的價格不跟會員身分變動）——凍結太早。</p>
<h2 id="設計責任">設計責任</h2>
<p>凍結時機由業務對「過去」的要求決定——進行中的操作持 live 參照是正確的、成為事實之後凍結。業務流程有多個確認階段時、凍結時機取決於哪個階段之後的漂移對下游不可接受。凍結判準的完整展開見 <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>。</p>
]]></content:encoded></item><item><title>Port</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/port/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/port/</guid><description>&lt;p>port 是 domain 對外宣告的介面：領域層用領域語言把需求說完——「我需要一個能存書、能查書的地方」——不提資料庫、不提網路。依賴方向因此朝內：實作端依賴介面、介面屬於領域，技術選型換掉時領域碼不動。port 的具體實作是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a>、兩者插上的位置是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>port 跟一般 interface 的差別在歸屬與語言：宣告在領域層、以領域概念命名（BookRepository 而非 SqliteClient）、方法簽名只用領域型別。介面本身是型別層的約束載體——它強制了「呼叫方看不見技術細節」，與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant&lt;/a> 的型別層強制同一個機制。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>介面檔案位於 domain 目錄、簽名沒有框架型別，是 port 健康的訊號。介面裡出現 DatabaseConnection、HttpClient 這類技術型別時，port 已被實作細節滲透——呼叫方被迫認識它不該認識的層。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>port 定義「領域需要什麼」，不保證「有人供給它」——宣告了介面、沒有實作被插上，在 mock 測試裡不會有任何紅燈。供給的驗證屬組裝層，教學層展開見 &lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>。port 的歸屬判準（逐型別問「這是誰的詞彙」）在 reactive 場景的完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>port 是 domain 對外宣告的介面：領域層用領域語言把需求說完——「我需要一個能存書、能查書的地方」——不提資料庫、不提網路。依賴方向因此朝內：實作端依賴介面、介面屬於領域，技術選型換掉時領域碼不動。port 的具體實作是 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a>、兩者插上的位置是 <a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>port 跟一般 interface 的差別在歸屬與語言：宣告在領域層、以領域概念命名（BookRepository 而非 SqliteClient）、方法簽名只用領域型別。介面本身是型別層的約束載體——它強制了「呼叫方看不見技術細節」，與 <a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant</a> 的型別層強制同一個機制。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>介面檔案位於 domain 目錄、簽名沒有框架型別，是 port 健康的訊號。介面裡出現 DatabaseConnection、HttpClient 這類技術型別時，port 已被實作細節滲透——呼叫方被迫認識它不該認識的層。</p>
<h2 id="設計責任">設計責任</h2>
<p>port 定義「領域需要什麼」，不保證「有人供給它」——宣告了介面、沒有實作被插上，在 mock 測試裡不會有任何紅燈。供給的驗證屬組裝層，教學層展開見 <a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a>。port 的歸屬判準（逐型別問「這是誰的詞彙」）在 reactive 場景的完整推導見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
]]></content:encoded></item><item><title>Adapter</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/</guid><description>&lt;p>adapter 是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a> 的具體實作：把領域宣告的需求翻譯成某項技術的操作——SQLite 的查詢、HTTP 的請求、檔案系統的讀寫。技術細節被擋在六角形之外的位置就是這裡：領域只看得見 port、看不見 adapter。adapter 在 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a> 被插上 port；測試裡的 mock 是 adapter 的替身。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>adapter 有兩側。driven adapter 被領域呼叫（資料庫、外部服務的實作）；driving adapter 呼叫領域（UI 事件處理、API handler）。兩側的共同責任是翻譯：領域語言與技術語言在這裡互換、不讓任何一邊的詞彙滲到對面。adapter 對外接的介面是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>、兩者插上的位置是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>實作類 import 框架套件、以技術命名（SqliteBookRepository），是 adapter 各安其位的訊號。領域規則出現在 adapter 裡（折扣計算寫在 repository、狀態判斷寫在 handler）是反向滲透——規則離開了領域模型、&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant&lt;/a> 的強制位置跟著失守。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>adapter 寫完、測試通過，只證明它自己行為正確；它有沒有被插上 port、插的是不是它，是組裝層的問題。接線的證言與強制層選擇見 &lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>。adapter 作為變更偵測機制的具體歸屬案例（寫入點 emit vs 儲存層 hook 的選型）見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>adapter 是 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a> 的具體實作：把領域宣告的需求翻譯成某項技術的操作——SQLite 的查詢、HTTP 的請求、檔案系統的讀寫。技術細節被擋在六角形之外的位置就是這裡：領域只看得見 port、看不見 adapter。adapter 在 <a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a> 被插上 port；測試裡的 mock 是 adapter 的替身。</p>
<h2 id="概念位置">概念位置</h2>
<p>adapter 有兩側。driven adapter 被領域呼叫（資料庫、外部服務的實作）；driving adapter 呼叫領域（UI 事件處理、API handler）。兩側的共同責任是翻譯：領域語言與技術語言在這裡互換、不讓任何一邊的詞彙滲到對面。adapter 對外接的介面是 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>、兩者插上的位置是 <a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>實作類 import 框架套件、以技術命名（SqliteBookRepository），是 adapter 各安其位的訊號。領域規則出現在 adapter 裡（折扣計算寫在 repository、狀態判斷寫在 handler）是反向滲透——規則離開了領域模型、<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant</a> 的強制位置跟著失守。</p>
<h2 id="設計責任">設計責任</h2>
<p>adapter 寫完、測試通過，只證明它自己行為正確；它有沒有被插上 port、插的是不是它，是組裝層的問題。接線的證言與強制層選擇見 <a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a>。adapter 作為變更偵測機制的具體歸屬案例（寫入點 emit vs 儲存層 hook 的選型）見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
]]></content:encoded></item><item><title>Composition Root</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/</guid><description>&lt;p>composition root（組裝根）是應用程式唯一的組裝起點：所有 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a> 在這裡被插上 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a>——DI 容器的註冊、啟動流程的接線集中於此。它通常是 main 函式旁的一小塊：知道全部具體型別的地方、也是唯一該知道的地方。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>composition root 是組裝層的核心、不是組裝層的全部：路由表以單點註冊靠攏它，UI 事件的接線散佈在各畫面——位置不同、同屬組裝層的責任。組裝層守「正確的路徑走得通」，與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant&lt;/a> 守「違反的路徑走不通」互為對偶。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>具體型別的建構與註冊集中在一處，是組裝責任歸位的訊號。new 語句散落各層、或 provider 停在佔位 throw「requires override」，是組裝責任洩漏或組裝未完成——後者在 mock 測試下全綠、只有無 override 的解析會暴露。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>組裝有沒有完成，在以 mock 為基礎的測試套件裡沒有證言；補證言的接線測試、可達性作為不變式的強制層選擇，教學層展開見 &lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>composition root（組裝根）是應用程式唯一的組裝起點：所有 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a> 在這裡被插上 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a>——DI 容器的註冊、啟動流程的接線集中於此。它通常是 main 函式旁的一小塊：知道全部具體型別的地方、也是唯一該知道的地方。</p>
<h2 id="概念位置">概念位置</h2>
<p>composition root 是組裝層的核心、不是組裝層的全部：路由表以單點註冊靠攏它，UI 事件的接線散佈在各畫面——位置不同、同屬組裝層的責任。組裝層守「正確的路徑走得通」，與 <a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant</a> 守「違反的路徑走不通」互為對偶。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>具體型別的建構與註冊集中在一處，是組裝責任歸位的訊號。new 語句散落各層、或 provider 停在佔位 throw「requires override」，是組裝責任洩漏或組裝未完成——後者在 mock 測試下全綠、只有無 override 的解析會暴露。</p>
<h2 id="設計責任">設計責任</h2>
<p>組裝有沒有完成，在以 mock 為基礎的測試套件裡沒有證言；補證言的接線測試、可達性作為不變式的強制層選擇，教學層展開見 <a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a>。</p>
]]></content:encoded></item><item><title>Dependency Injection</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/dependency-injection/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/dependency-injection/</guid><description>&lt;p>依賴注入（dependency injection、DI）把「建構依賴」跟「使用依賴」分成兩個責任：物件宣告它需要什麼（通常以 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a> 這樣的介面表達）、實例由外部在組裝時提供——最基本的形式是建構子參數。物件自己建構依賴時、依賴的選擇被寫死在使用處；改由外部注入後、同一段程式碼在 production 拿到真實 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a>、在測試拿到替身。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>DI 容器是框架提供的註冊與解析機制、把「哪個介面對應哪個實作」收成一份註冊表：啟動時註冊、使用時解析——每一條可解析的註冊項就是一個注入項、部分生態稱它 provider。注入的集中點是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a>：全部具體型別在這裡被決定。測試框架的 override 機制（用替身蓋過註冊項）是 DI 給測試的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">seam&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>建構子收介面、具體實作的選擇集中在組裝處，是注入責任歸位的訊號。類別內部直接建構自己的依賴（new 具體型別、呼叫全域單例）時、該依賴在測試裡換不掉、技術選擇散落各層。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>注入讓「誰提供真實作」成為一個獨立責任。這個責任有沒有被履行——每個注入項在無 override 環境解析得了——在行為測試裡沒有證言（綠燈證明不了它）、要由 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試&lt;/a> 驗證；教學層展開見 &lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>依賴注入（dependency injection、DI）把「建構依賴」跟「使用依賴」分成兩個責任：物件宣告它需要什麼（通常以 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a> 這樣的介面表達）、實例由外部在組裝時提供——最基本的形式是建構子參數。物件自己建構依賴時、依賴的選擇被寫死在使用處；改由外部注入後、同一段程式碼在 production 拿到真實 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a>、在測試拿到替身。</p>
<h2 id="概念位置">概念位置</h2>
<p>DI 容器是框架提供的註冊與解析機制、把「哪個介面對應哪個實作」收成一份註冊表：啟動時註冊、使用時解析——每一條可解析的註冊項就是一個注入項、部分生態稱它 provider。注入的集中點是 <a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a>：全部具體型別在這裡被決定。測試框架的 override 機制（用替身蓋過註冊項）是 DI 給測試的 <a href="/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">seam</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>建構子收介面、具體實作的選擇集中在組裝處，是注入責任歸位的訊號。類別內部直接建構自己的依賴（new 具體型別、呼叫全域單例）時、該依賴在測試裡換不掉、技術選擇散落各層。</p>
<h2 id="設計責任">設計責任</h2>
<p>注入讓「誰提供真實作」成為一個獨立責任。這個責任有沒有被履行——每個注入項在無 override 環境解析得了——在行為測試裡沒有證言（綠燈證明不了它）、要由 <a href="/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試</a> 驗證；教學層展開見 <a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a>。</p>
]]></content:encoded></item><item><title>Test Seam</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/test-seam/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/test-seam/</guid><description>&lt;p>測試 seam 是不修改程式本體就能替換其中一段行為的位置——術語出自 Michael Feathers 的《Working Effectively with Legacy Code》。物件導向程式最常見的 seam 是介面加注入點：&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a> 宣告需求、&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">依賴注入&lt;/a> 提供替換的入口，測試在這裡把真實 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a> 換成 mock、讓 domain 邏輯脫離資料庫與網路單獨驗證。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>seam 是分層架構測試承諾的兌現機制：「domain 可以脫離 infrastructure 測試」的具體操作、就是在 seam 換上替身。DI 容器的 override（測試用替身蓋過註冊項）是 seam 的容器化形式；&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a> 集中的組裝、正是 seam 替換的對象。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>測試不碰 production 程式碼就能把資料庫、網路、時鐘換成替身，是 seam 工作中的訊號。想測某段邏輯必須連上真實服務、或得修改本體才塞得進替身，是 seam 缺席的訊號。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>seam 有一體兩面：替換掉的正是 production 的組裝、於是「組裝有沒有完成」在以 seam 為基礎的測試裡沒有證言（綠燈證明不了組裝完成）。mock 測試全綠與功能可用之間的缺口由 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試&lt;/a> 補；這道影子的完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>測試 seam 是不修改程式本體就能替換其中一段行為的位置——術語出自 Michael Feathers 的《Working Effectively with Legacy Code》。物件導向程式最常見的 seam 是介面加注入點：<a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a> 宣告需求、<a href="/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">依賴注入</a> 提供替換的入口，測試在這裡把真實 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a> 換成 mock、讓 domain 邏輯脫離資料庫與網路單獨驗證。</p>
<h2 id="概念位置">概念位置</h2>
<p>seam 是分層架構測試承諾的兌現機制：「domain 可以脫離 infrastructure 測試」的具體操作、就是在 seam 換上替身。DI 容器的 override（測試用替身蓋過註冊項）是 seam 的容器化形式；<a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a> 集中的組裝、正是 seam 替換的對象。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>測試不碰 production 程式碼就能把資料庫、網路、時鐘換成替身，是 seam 工作中的訊號。想測某段邏輯必須連上真實服務、或得修改本體才塞得進替身，是 seam 缺席的訊號。</p>
<h2 id="設計責任">設計責任</h2>
<p>seam 有一體兩面：替換掉的正是 production 的組裝、於是「組裝有沒有完成」在以 seam 為基礎的測試裡沒有證言（綠燈證明不了組裝完成）。mock 測試全綠與功能可用之間的缺口由 <a href="/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試</a> 補；這道影子的完整推導見 <a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a>。</p>
]]></content:encoded></item><item><title>Wiring Test</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/wiring-test/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/wiring-test/</guid><description>&lt;p>接線測試（wiring test）只回答一個問題：&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a> 插上 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a> 了沒。做法是讓組裝路徑保持零 override——每個 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">依賴注入&lt;/a> 的注入項真實解析、路由表真實導航、UI 事件真實觸發——功能邏輯的對錯留給行為測試。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>一個測試通過時能證明的事（&lt;a href="https://tarrragon.github.io/blog/ddd/" data-link-title="DDD 領域驅動設計指南" data-link-desc="領域模型的理論與判準層：一袋欄位還是領域模型、什麼時候值得建 entity、不變式該落在哪一層強制、狀態轉換怎麼留下稽核軌跡、建構路徑怎麼設計。語言無關，實作限制路由到各語言模組。">本模組&lt;/a> 稱「證言」）由執行環境決定：在 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">test seam&lt;/a> 換上 mock 之後、綠燈對組裝再無證明力，接線測試因此是測試層面組裝證言的唯一來源（編譯期生成組裝碼的生態、編譯器先接住「缺實作」一類，其餘仍歸這裡）。跟端對端測試的分工是範圍換速度：E2E 在目標平台連行為一起驗、接線測試只驗接線，換取在 host 環境（開發機、非目標裝置）快速且確定地執行。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>個別測試用 override 是正當的 seam 用法；訊號要看專案層級——連一個零 override 的解析測試都沒有時、組裝處於無人作證的狀態。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>零 override 的邊界是組裝路徑：domain 到入口之間不替換任何一段、最外圈的 infrastructure（遠端服務這類）可以在邊界替換。回傳假值的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/placeholder/" data-link-title="Placeholder" data-link-desc="「開發中」頁、空 callback、拋出例外的注入項該怎麼管理時使用。佔位是先立介面後補實作的合法中間態——它的失效讓測試綠燈、比文件層約束更靜默。">佔位&lt;/a> 解析得了、導航也會發生、在接線測試眼中是正常構件，要靠行為斷言或發版冒煙走查。這條界線在應用程式層怎麼守、&lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a> 有完整推導。&lt;/p></description><content:encoded><![CDATA[<p>接線測試（wiring test）只回答一個問題：<a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a> 插上 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a> 了沒。做法是讓組裝路徑保持零 override——每個 <a href="/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">依賴注入</a> 的注入項真實解析、路由表真實導航、UI 事件真實觸發——功能邏輯的對錯留給行為測試。</p>
<h2 id="概念位置">概念位置</h2>
<p>一個測試通過時能證明的事（<a href="/blog/ddd/" data-link-title="DDD 領域驅動設計指南" data-link-desc="領域模型的理論與判準層：一袋欄位還是領域模型、什麼時候值得建 entity、不變式該落在哪一層強制、狀態轉換怎麼留下稽核軌跡、建構路徑怎麼設計。語言無關，實作限制路由到各語言模組。">本模組</a> 稱「證言」）由執行環境決定：在 <a href="/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">test seam</a> 換上 mock 之後、綠燈對組裝再無證明力，接線測試因此是測試層面組裝證言的唯一來源（編譯期生成組裝碼的生態、編譯器先接住「缺實作」一類，其餘仍歸這裡）。跟端對端測試的分工是範圍換速度：E2E 在目標平台連行為一起驗、接線測試只驗接線，換取在 host 環境（開發機、非目標裝置）快速且確定地執行。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>個別測試用 override 是正當的 seam 用法；訊號要看專案層級——連一個零 override 的解析測試都沒有時、組裝處於無人作證的狀態。</p>
<h2 id="設計責任">設計責任</h2>
<p>零 override 的邊界是組裝路徑：domain 到入口之間不替換任何一段、最外圈的 infrastructure（遠端服務這類）可以在邊界替換。回傳假值的 <a href="/blog/ddd/knowledge-cards/placeholder/" data-link-title="Placeholder" data-link-desc="「開發中」頁、空 callback、拋出例外的注入項該怎麼管理時使用。佔位是先立介面後補實作的合法中間態——它的失效讓測試綠燈、比文件層約束更靜默。">佔位</a> 解析得了、導航也會發生、在接線測試眼中是正常構件，要靠行為斷言或發版冒煙走查。這條界線在應用程式層怎麼守、<a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a> 有完整推導。</p>
]]></content:encoded></item><item><title>Placeholder</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/placeholder/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/placeholder/</guid><description>&lt;p>佔位（placeholder）是「先立介面、實作後補」的合法開發中間態：指向「開發中」頁的路由、空的事件 callback、拋出「requires override」的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">注入項&lt;/a>、回傳 hardcoded 假資料的 stub。節奏本身沒有問題、問題在佔位的失效形態：漏網的佔位通過所有以 mock 為基礎的驗收、終點站是使用者的回報。與它相鄰的組裝概念見 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>失效的靜默程度比文件層約束（層次判準見 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant&lt;/a>）再深一級：文件層失效至少留下「規則寫在那裡、沒人遵守」的對照證據；佔位讓測試綠燈——型別層看它是合法構件、行為測試的 override 讓它永遠沒被觸發（override 的機制見 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">test seam&lt;/a>）。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>佔位頁的型別名、「requires override」的拋出語句、空函式體都是靜態可掃描的形態。回傳假值的佔位在掃描與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試&lt;/a> 眼中都是正常構件——注入項解析得了、導航也會發生。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>佔位需要一個獨立的攔截點：靜態可掃描的形態進發版前置檢查、掃到即警告、由人判斷是刻意中間態還是漏網；回傳假值的那一類靠行為斷言或實機冒煙走查。攔截點怎麼落進發版流程、&lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a> 有完整處置。&lt;/p></description><content:encoded><![CDATA[<p>佔位（placeholder）是「先立介面、實作後補」的合法開發中間態：指向「開發中」頁的路由、空的事件 callback、拋出「requires override」的 <a href="/blog/ddd/knowledge-cards/dependency-injection/" data-link-title="Dependency Injection" data-link-desc="物件的依賴該由誰提供、測試怎麼換掉真實依賴時使用。依賴注入把「建構依賴」跟「使用依賴」分成兩個責任——使用方宣告需要什麼、提供方在組裝時決定給什麼。">注入項</a>、回傳 hardcoded 假資料的 stub。節奏本身沒有問題、問題在佔位的失效形態：漏網的佔位通過所有以 mock 為基礎的驗收、終點站是使用者的回報。與它相鄰的組裝概念見 <a href="/blog/ddd/knowledge-cards/composition-root/" data-link-title="Composition Root" data-link-desc="依賴組裝、路由註冊該集中在哪、組裝斷裂去哪檢查時使用。composition root 是應用程式唯一的組裝起點——DI、路由、事件接線的集中處。">composition root</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>失效的靜默程度比文件層約束（層次判準見 <a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant</a>）再深一級：文件層失效至少留下「規則寫在那裡、沒人遵守」的對照證據；佔位讓測試綠燈——型別層看它是合法構件、行為測試的 override 讓它永遠沒被觸發（override 的機制見 <a href="/blog/ddd/knowledge-cards/test-seam/" data-link-title="Test Seam" data-link-desc="測試想把真實依賴換成替身、該從哪裡換時使用。seam 是不修改程式本體就能替換其中一段行為的位置——介面加注入點是物件導向最常見的形式。">test seam</a>）。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>佔位頁的型別名、「requires override」的拋出語句、空函式體都是靜態可掃描的形態。回傳假值的佔位在掃描與 <a href="/blog/ddd/knowledge-cards/wiring-test/" data-link-title="Wiring Test" data-link-desc="行為測試全綠、還想知道組裝有沒有完成時使用。接線測試在組裝路徑零 override 的環境解析每個注入項、走真實路由表，只驗「port 插上了 adapter」。">接線測試</a> 眼中都是正常構件——注入項解析得了、導航也會發生。</p>
<h2 id="設計責任">設計責任</h2>
<p>佔位需要一個獨立的攔截點：靜態可掃描的形態進發版前置檢查、掃到即警告、由人判斷是刻意中間態還是漏網；回傳假值的那一類靠行為斷言或實機冒煙走查。攔截點怎麼落進發版流程、<a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a> 有完整處置。</p>
]]></content:encoded></item><item><title>Read Model</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/</guid><description>&lt;p>read model 是為讀需求的形狀而建的查詢側模型：畫面或報表要什麼形狀（統計值、扁平列表、跨 aggregate 拼裝）、它就長什麼形狀。repository 回傳 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate&lt;/a> 的形狀、守一致性邊界；read model 回傳讀的形狀、不承擔寫入責任。兩者的分工判準是一句自檢問句：「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。讀側的介面宣告是一種 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>——同樣以領域語言表達、由查詢方的需要定義形狀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>read model 是 CQRS 讀寫分離的讀側，與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 的寫側形成互補：aggregate 守一致性邊界、read model 服務讀的形狀。它的存在不以 CQRS 全套為前提——讀側是一道階梯：消費端自行投影、讀 port 抽離（獨立的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>）、專用投影、事件同步的獨立儲存，每一階都是 read model 概念的某種深度。低階的 read model 可以只是 ViewModel 裡幾行 &lt;code>map&lt;/code>；高階的 read model 有自己的儲存與更新節奏，接受與寫側的最終一致性。階梯橫跨 ephemeral（ViewModel 內聯投影，沒有獨立生命週期）到 durable（獨立儲存、事件同步），但各階共享同一個設計責任——「讀的形狀由讀需求定義、不由 aggregate 形狀決定」——因此視為同一概念的深度變體。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>repository 介面長出回傳統計值、分頁切片、反正規化投影的方法，是讀的形狀開始混進 aggregate 介面的訊號。反向的訊號同樣可觀察：還沒有第二條讀需求就先建投影層，抽象沒有消費者、每個新查詢多穿一層介面。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>read model 定義「讀側要什麼形狀」，升級到哪一階由訊號決定、不由架構偏好決定——五個升級訊號（讀需求增生、形狀偏離、負載分歧、新鮮度分級、獨立演進）與階梯各階的代價，教學層展開見 &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></description><content:encoded><![CDATA[<p>read model 是為讀需求的形狀而建的查詢側模型：畫面或報表要什麼形狀（統計值、扁平列表、跨 aggregate 拼裝）、它就長什麼形狀。repository 回傳 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate</a> 的形狀、守一致性邊界；read model 回傳讀的形狀、不承擔寫入責任。兩者的分工判準是一句自檢問句：「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。讀側的介面宣告是一種 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>——同樣以領域語言表達、由查詢方的需要定義形狀。</p>
<h2 id="概念位置">概念位置</h2>
<p>read model 是 CQRS 讀寫分離的讀側，與 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 的寫側形成互補：aggregate 守一致性邊界、read model 服務讀的形狀。它的存在不以 CQRS 全套為前提——讀側是一道階梯：消費端自行投影、讀 port 抽離（獨立的 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>）、專用投影、事件同步的獨立儲存，每一階都是 read model 概念的某種深度。低階的 read model 可以只是 ViewModel 裡幾行 <code>map</code>；高階的 read model 有自己的儲存與更新節奏，接受與寫側的最終一致性。階梯橫跨 ephemeral（ViewModel 內聯投影，沒有獨立生命週期）到 durable（獨立儲存、事件同步），但各階共享同一個設計責任——「讀的形狀由讀需求定義、不由 aggregate 形狀決定」——因此視為同一概念的深度變體。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>repository 介面長出回傳統計值、分頁切片、反正規化投影的方法，是讀的形狀開始混進 aggregate 介面的訊號。反向的訊號同樣可觀察：還沒有第二條讀需求就先建投影層，抽象沒有消費者、每個新查詢多穿一層介面。</p>
<h2 id="設計責任">設計責任</h2>
<p>read model 定義「讀側要什麼形狀」，升級到哪一階由訊號決定、不由架構偏好決定——五個升級訊號（讀需求增生、形狀偏離、負載分歧、新鮮度分級、獨立演進）與階梯各階的代價，教學層展開見 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a>。</p>
]]></content:encoded></item><item><title>Domain Event</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/</guid><description>&lt;p>domain event 是一筆已發生的業務事實的記錄：過去式命名（&lt;code>BookAdded&lt;/code>、&lt;code>OrderPaid&lt;/code>）、發布後不可變、消費者關心「發生了什麼」。跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a> 的差別在時間語意：snapshot 記的是某時刻的狀態（現在式），event 記的是某時刻發生的事（過去式）。與狀態流的差別在錯過的代價：event 錯過代表事實遺失（審計斷檔、下游流程沒觸發），狀態流錯過無代價（下一次快照涵蓋一切）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>domain event 是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 執行業務操作後的副產品——aggregate 保證一致性、event 把「剛才發生的事」通知外界。event 的消費者是跨 domain 流程、審計、通知等「需要知道發生了什麼」的角色。它跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model&lt;/a> 在 CQRS 第四階交會：讀模型由事件同步時，消費者問的確實是「發生了什麼」（用事實重建投影），這是 event 的合法消費。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>event 被借用成 UI 刷新訊號時會出現四個判讀訊號：為了讓某頁刷新而補發事件、監聽端掛全事件過濾器、移除事件前要查有沒有畫面靠它刷新、event payload 開始塞當前完整狀態。任一出現，回到載體判準重新選——消費者問「現在是什麼」時，正確載體是狀態流。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>domain event 記錄業務事實、服務審計與跨 domain 通訊。event 的發布時機由業務語意決定（「這件事值得記錄、有下游需要它」），涵蓋面是業務事實的集合——跟狀態流的涵蓋面（寫入操作的集合，結構性涵蓋）正交。載體選用判準、案例演進與判讀訊號展開在 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流&lt;/a>；事件對命令與查詢的責任結構分界展開在 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-command-and-query/" data-link-title="domain event 與命令、查詢的分界" data-link-desc="事件類別以動詞開頭、或事件成對出現 Requested/Provided 帶 correlation id 時使用。事件只承載已發生的事實——命令有唯一處理者與成敗、查詢是一問一答的對話；把意圖或對話裝進事件通道、責任結構會跟著錯位。">domain event 與命令、查詢&lt;/a>。命名的過去式約定展開在 &lt;a href="https://tarrragon.github.io/blog/work-log/domain_event_naming_past_tense/" data-link-title="BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格" data-link-desc="domain event 用過去式命名（BookImported）因為事件是已發生的事實；動詞開頭（ImportBook）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">Domain Event 命名的過去式&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>domain event 是一筆已發生的業務事實的記錄：過去式命名（<code>BookAdded</code>、<code>OrderPaid</code>）、發布後不可變、消費者關心「發生了什麼」。跟 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a> 的差別在時間語意：snapshot 記的是某時刻的狀態（現在式），event 記的是某時刻發生的事（過去式）。與狀態流的差別在錯過的代價：event 錯過代表事實遺失（審計斷檔、下游流程沒觸發），狀態流錯過無代價（下一次快照涵蓋一切）。</p>
<h2 id="概念位置">概念位置</h2>
<p>domain event 是 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 執行業務操作後的副產品——aggregate 保證一致性、event 把「剛才發生的事」通知外界。event 的消費者是跨 domain 流程、審計、通知等「需要知道發生了什麼」的角色。它跟 <a href="/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model</a> 在 CQRS 第四階交會：讀模型由事件同步時，消費者問的確實是「發生了什麼」（用事實重建投影），這是 event 的合法消費。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>event 被借用成 UI 刷新訊號時會出現四個判讀訊號：為了讓某頁刷新而補發事件、監聽端掛全事件過濾器、移除事件前要查有沒有畫面靠它刷新、event payload 開始塞當前完整狀態。任一出現，回到載體判準重新選——消費者問「現在是什麼」時，正確載體是狀態流。</p>
<h2 id="設計責任">設計責任</h2>
<p>domain event 記錄業務事實、服務審計與跨 domain 通訊。event 的發布時機由業務語意決定（「這件事值得記錄、有下游需要它」），涵蓋面是業務事實的集合——跟狀態流的涵蓋面（寫入操作的集合，結構性涵蓋）正交。載體選用判準、案例演進與判讀訊號展開在 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>；事件對命令與查詢的責任結構分界展開在 <a href="/blog/ddd/domain-event-vs-command-and-query/" data-link-title="domain event 與命令、查詢的分界" data-link-desc="事件類別以動詞開頭、或事件成對出現 Requested/Provided 帶 correlation id 時使用。事件只承載已發生的事實——命令有唯一處理者與成敗、查詢是一問一答的對話；把意圖或對話裝進事件通道、責任結構會跟著錯位。">domain event 與命令、查詢</a>。命名的過去式約定展開在 <a href="/blog/work-log/domain_event_naming_past_tense/" data-link-title="BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格" data-link-desc="domain event 用過去式命名（BookImported）因為事件是已發生的事實；動詞開頭（ImportBook）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">Domain Event 命名的過去式</a>。</p>
]]></content:encoded></item><item><title>State Stream（狀態流）</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/</guid><description>&lt;p>狀態流是持續發布「某份資料當前值」的觀測載體：新值蓋過舊值、只有最新值有意義、錯過中間值無代價——下一次快照涵蓋一切。與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 的分界在消費者的問題：狀態流回答「現在是什麼」、event 回答「發生了什麼」。與 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a> 的差別在時間方向：snapshot 凍結某一刻、保證歷史不隨現在漂移；狀態流永遠指向現在、舊值被覆蓋是設計語意。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>狀態流是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">觀測出口&lt;/a> 的載體形態：觀測出口定義「資料變了、我能告訴你」這個能力歸屬哪一層、狀態流定義這個通知的語意——連續快照、不是離散事實。它的涵蓋面是寫入操作的集合：emit 掛在每個寫入方法尾端、新路徑必然經過、涵蓋不靠任何人記得發——與 domain event「業務語意決定發布點、逐事實設計」的涵蓋方式正交。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統缺狀態流出口時，補償的形狀是訊號：導航返回點出現手動 reload、為了讓某頁刷新而補發事件、監聽端掛全事件過濾器。事件被誤用成狀態流的完整判讀訊號表在 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流&lt;/a>。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>狀態流承擔「畫面、快取、衍生視圖對資料當前值的觀測」，錯過的代價為零是它跟 event 不可互換的原因——需要逐筆事實（審計、下游流程）的消費者拿快照會斷檔。載體選用判準展開見 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流&lt;/a>、實作選型（broadcast、初始值、dispose）見 &lt;a href="https://tarrragon.github.io/blog/work-log/flutter_streamprovider_wraps_repository_watch/" data-link-title="StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點" data-link-desc="repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。">StreamProvider 包 repository watch stream&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>狀態流是持續發布「某份資料當前值」的觀測載體：新值蓋過舊值、只有最新值有意義、錯過中間值無代價——下一次快照涵蓋一切。與 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 的分界在消費者的問題：狀態流回答「現在是什麼」、event 回答「發生了什麼」。與 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a> 的差別在時間方向：snapshot 凍結某一刻、保證歷史不隨現在漂移；狀態流永遠指向現在、舊值被覆蓋是設計語意。</p>
<h2 id="概念位置">概念位置</h2>
<p>狀態流是 <a href="/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">觀測出口</a> 的載體形態：觀測出口定義「資料變了、我能告訴你」這個能力歸屬哪一層、狀態流定義這個通知的語意——連續快照、不是離散事實。它的涵蓋面是寫入操作的集合：emit 掛在每個寫入方法尾端、新路徑必然經過、涵蓋不靠任何人記得發——與 domain event「業務語意決定發布點、逐事實設計」的涵蓋方式正交。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統缺狀態流出口時，補償的形狀是訊號：導航返回點出現手動 reload、為了讓某頁刷新而補發事件、監聽端掛全事件過濾器。事件被誤用成狀態流的完整判讀訊號表在 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>狀態流承擔「畫面、快取、衍生視圖對資料當前值的觀測」，錯過的代價為零是它跟 event 不可互換的原因——需要逐筆事實（審計、下游流程）的消費者拿快照會斷檔。載體選用判準展開見 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>、實作選型（broadcast、初始值、dispose）見 <a href="/blog/work-log/flutter_streamprovider_wraps_repository_watch/" data-link-title="StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點" data-link-desc="repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。">StreamProvider 包 repository watch stream</a>。</p>
]]></content:encoded></item><item><title>Observation Outlet（觀測出口）</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/</guid><description>&lt;p>觀測出口是 repository 對外提供的「資料變了」持續通知能力——pull 介面（&lt;code>getAllBooks()&lt;/code> 回 &lt;code>Future&lt;/code>）的 push 對應（&lt;code>watchBooks()&lt;/code> 回 &lt;code>Stream&lt;/code>）。它的載體是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">狀態流&lt;/a>：通知的內容是資料當前值、不是業務事實。能力橫跨三層——契約（介面宣告）、機制（變更偵測）、組裝（框架訂閱），每一層的歸屬由該層產出的表達語言決定。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>觀測出口的契約層是一種 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>：簽名只用語言標準庫與 domain entity、放 domain repository 介面，與 pull 方法形成對稱。機制層（broadcast controller、寫入點 emit）歸 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter&lt;/a>；組裝層（框架 provider 包裝）歸 DI／presentation。三層歸屬的完整判準與「需求來源不決定歸屬」的推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>repository 缺觀測出口時，每個衍生視圖各自解「怎麼知道資料變了」：導航返回點補 reload、EventBus 橋接刷新、多個視圖各自維護 load 時機。補償策略的交叉與涵蓋缺口是這個能力該補的訊號。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>觀測出口讓「資料變更」成為可訂閱的一級節點，涵蓋面等於寫入操作的集合——emit 掛在寫入方法尾端、新路徑自動涵蓋。它通知「現在是什麼」、不記錄「發生了什麼」——後者是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 的責任，兩者正交。落地的實作點（broadcast、初始值、dispose）見 &lt;a href="https://tarrragon.github.io/blog/work-log/flutter_streamprovider_wraps_repository_watch/" data-link-title="StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點" data-link-desc="repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。">StreamProvider 包 repository watch stream&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>觀測出口是 repository 對外提供的「資料變了」持續通知能力——pull 介面（<code>getAllBooks()</code> 回 <code>Future</code>）的 push 對應（<code>watchBooks()</code> 回 <code>Stream</code>）。它的載體是 <a href="/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">狀態流</a>：通知的內容是資料當前值、不是業務事實。能力橫跨三層——契約（介面宣告）、機制（變更偵測）、組裝（框架訂閱），每一層的歸屬由該層產出的表達語言決定。</p>
<h2 id="概念位置">概念位置</h2>
<p>觀測出口的契約層是一種 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>：簽名只用語言標準庫與 domain entity、放 domain repository 介面，與 pull 方法形成對稱。機制層（broadcast controller、寫入點 emit）歸 <a href="/blog/ddd/knowledge-cards/adapter/" data-link-title="Adapter" data-link-desc="把領域需求翻譯成具體技術操作的實作該放哪、跟領域的邊界在哪時使用。adapter 是 port 的具體實作——技術細節被擋在六角形之外的位置。">adapter</a>；組裝層（框架 provider 包裝）歸 DI／presentation。三層歸屬的完整判準與「需求來源不決定歸屬」的推導見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>repository 缺觀測出口時，每個衍生視圖各自解「怎麼知道資料變了」：導航返回點補 reload、EventBus 橋接刷新、多個視圖各自維護 load 時機。補償策略的交叉與涵蓋缺口是這個能力該補的訊號。</p>
<h2 id="設計責任">設計責任</h2>
<p>觀測出口讓「資料變更」成為可訂閱的一級節點，涵蓋面等於寫入操作的集合——emit 掛在寫入方法尾端、新路徑自動涵蓋。它通知「現在是什麼」、不記錄「發生了什麼」——後者是 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 的責任，兩者正交。落地的實作點（broadcast、初始值、dispose）見 <a href="/blog/work-log/flutter_streamprovider_wraps_repository_watch/" data-link-title="StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點" data-link-desc="repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。">StreamProvider 包 repository watch stream</a>。</p>
]]></content:encoded></item><item><title>Repository</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/repository/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/repository/</guid><description>&lt;p>repository 把「怎麼存、怎麼查」包裝成領域語言的介面：呼叫端看到的是存書、查書這類操作，看不到底層是資料庫、檔案還是記憶體。它是一種 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>——依賴方向朝內、簽名只用領域型別——差別在 repository 專職 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 的存取。repository 回傳的形狀是 aggregate 的形狀（entity 或 entity 集合），這條界線是它跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model&lt;/a> 分工的起點。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>repository 預設是 pull 介面：呼叫端主動問「現在的資料長怎樣」，一次拿到一份 aggregate 形狀的答案。它不天生具備「資料變了通知我」的推送能力——這條能力屬於 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet&lt;/a>，是 repository 介面之上的另一層職責，需要另外設計才會出現。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>repository 介面開始長出 &lt;code>getMonthlyStatistics()&lt;/code>、&lt;code>searchWithPagination()&lt;/code> 這類回傳統計值或反正規化形狀的方法，是讀的形狀混進 aggregate 介面的訊號——查詢集合已經大到值得思考該不該抽獨立的讀側介面，量測與升級路徑見 &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>repository 的設計責任是守住 aggregate 一致性邊界的存取入口，不是最佳化每一種讀需求的效能與形狀——後者是 read model 的責任。repository 是否該同時扛下變更通知，判準不是「需求來自誰」而是介面用什麼語言表達，完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>repository 把「怎麼存、怎麼查」包裝成領域語言的介面：呼叫端看到的是存書、查書這類操作，看不到底層是資料庫、檔案還是記憶體。它是一種 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>——依賴方向朝內、簽名只用領域型別——差別在 repository 專職 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 的存取。repository 回傳的形狀是 aggregate 的形狀（entity 或 entity 集合），這條界線是它跟 <a href="/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model</a> 分工的起點。</p>
<h2 id="概念位置">概念位置</h2>
<p>repository 預設是 pull 介面：呼叫端主動問「現在的資料長怎樣」，一次拿到一份 aggregate 形狀的答案。它不天生具備「資料變了通知我」的推送能力——這條能力屬於 <a href="/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet</a>，是 repository 介面之上的另一層職責，需要另外設計才會出現。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>repository 介面開始長出 <code>getMonthlyStatistics()</code>、<code>searchWithPagination()</code> 這類回傳統計值或反正規化形狀的方法，是讀的形狀混進 aggregate 介面的訊號——查詢集合已經大到值得思考該不該抽獨立的讀側介面，量測與升級路徑見 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a>。</p>
<h2 id="設計責任">設計責任</h2>
<p>repository 的設計責任是守住 aggregate 一致性邊界的存取入口，不是最佳化每一種讀需求的效能與形狀——後者是 read model 的責任。repository 是否該同時扛下變更通知，判準不是「需求來自誰」而是介面用什麼語言表達，完整推導見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
]]></content:encoded></item><item><title>Bounded Context</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/bounded-context/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/bounded-context/</guid><description>&lt;p>bounded context 是一個模型與詞彙維持一致的邊界：邊界內，同一個詞（例如「訂單」）只有一種意義、同一套業務規則統一適用；跨過邊界，同一個詞可能換了意義、規則跟著換。&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a> 守的一致性邊界在單一 aggregate 內；bounded context 守的是更大一層——一整套模型、詞彙與規則在哪裡開始、哪裡結束。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>單一 bounded context 內的架構判準，跨過邊界不必然照搬。讀模型升級的四階梯（消費端投影 → 抽讀 port → 專用投影 → 事件同步的獨立儲存）假設操作發生在單一 bounded context 內；報表服務訂閱多個 domain 的事件建置共享視圖，是跨 bounded context 的讀模型，這時引入的關注點——跨服務事件契約穩定性、schema 演進、跨信任邊界的最終一致性——不是「多買一種能力」能概括，完整推導見 &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;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a>，其中攜帶足量狀態給下游的作法見 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/event-carried-state-transfer/" data-link-title="Event-Carried State Transfer" data-link-desc="跨服務的 domain event payload 開始塞進足量當前狀態、要判斷這是正當設計還是載體誤用時使用。event-carried state transfer 是刻意讓事件攜帶足量狀態、讓下游服務不必回頭查詢來源的設計。">event-carried state transfer&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>一個判準句在單一服務內成立、換到跨服務場景就開始要補額外的機制（契約版本、schema 遷移、補償流程），是踩到 bounded context 邊界的訊號——原本的判準沒有錯，是它的適用範圍被劃在邊界內，邊界外要重新論證。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>bounded context 的設計責任是替模型畫出「在哪裡可以直接假設一致」的邊界，邊界外的互動改走顯式的契約與轉換，不能沿用邊界內的內部語意。這條邊界也是判斷同一套架構結論能不能套用的分界——邊界內成立的推導，邊界外要重新論證，不是預設繼續有效。&lt;/p></description><content:encoded><![CDATA[<p>bounded context 是一個模型與詞彙維持一致的邊界：邊界內，同一個詞（例如「訂單」）只有一種意義、同一套業務規則統一適用；跨過邊界，同一個詞可能換了意義、規則跟著換。<a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a> 守的一致性邊界在單一 aggregate 內；bounded context 守的是更大一層——一整套模型、詞彙與規則在哪裡開始、哪裡結束。</p>
<h2 id="概念位置">概念位置</h2>
<p>單一 bounded context 內的架構判準，跨過邊界不必然照搬。讀模型升級的四階梯（消費端投影 → 抽讀 port → 專用投影 → 事件同步的獨立儲存）假設操作發生在單一 bounded context 內；報表服務訂閱多個 domain 的事件建置共享視圖，是跨 bounded context 的讀模型，這時引入的關注點——跨服務事件契約穩定性、schema 演進、跨信任邊界的最終一致性——不是「多買一種能力」能概括，完整推導見 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a>。這類跨邊界溝通的常見載體是 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a>，其中攜帶足量狀態給下游的作法見 <a href="/blog/ddd/knowledge-cards/event-carried-state-transfer/" data-link-title="Event-Carried State Transfer" data-link-desc="跨服務的 domain event payload 開始塞進足量當前狀態、要判斷這是正當設計還是載體誤用時使用。event-carried state transfer 是刻意讓事件攜帶足量狀態、讓下游服務不必回頭查詢來源的設計。">event-carried state transfer</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>一個判準句在單一服務內成立、換到跨服務場景就開始要補額外的機制（契約版本、schema 遷移、補償流程），是踩到 bounded context 邊界的訊號——原本的判準沒有錯，是它的適用範圍被劃在邊界內，邊界外要重新論證。</p>
<h2 id="設計責任">設計責任</h2>
<p>bounded context 的設計責任是替模型畫出「在哪裡可以直接假設一致」的邊界，邊界外的互動改走顯式的契約與轉換，不能沿用邊界內的內部語意。這條邊界也是判斷同一套架構結論能不能套用的分界——邊界內成立的推導，邊界外要重新論證，不是預設繼續有效。</p>
]]></content:encoded></item><item><title>EventBus</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/event-bus/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/event-bus/</guid><description>&lt;p>需要在同一個行程內把「發生了一件事」廣播給多個訂閱者時，用的是 EventBus——行程內的發布／訂閱事件匯流排。發布端呼叫 publish、不需要知道誰在聽；訂閱端註冊監聽器、不需要知道是誰發的。這個解耦讓 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 的發布點跟消費點可以獨立增減，不必逐一牽線。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>EventBus 是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 的傳輸機制、不是事件本身：event 是「發生了什麼」的事實記錄，EventBus 是讓這個事實從發布端跑到訂閱端的管道。EventBus 只解決「怎麼送達」，送達之後訂閱端該把事件當事實通知還是當變更信號來用，是另一個判準，見 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>訂閱端掛上「監聽全部事件、任何事件進來就重新查詢」這種全事件過濾器，是把 EventBus 兼職成變更通知管道的訊號——這條路徑上線初期有效，但涵蓋面等於「有沒有人記得發事件」，跟寫入操作的集合不天然相等，多個視圖各自解「怎麼知道資料變了」時容易補出交叉且不完整的補償策略。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>EventBus 只承擔「發布／訂閱」這一層機制、不承擔涵蓋保證——涵蓋是靠每個寫入路徑記得呼叫 publish 換來的，EventBus 本身不驗證有沒有漏發。需要涵蓋面天然等於寫入操作集合的場景，正確載體是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet&lt;/a> 的狀態流、不是把 EventBus 的監聽範圍擴大，完整案例見 &lt;a href="https://tarrragon.github.io/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>需要在同一個行程內把「發生了一件事」廣播給多個訂閱者時，用的是 EventBus——行程內的發布／訂閱事件匯流排。發布端呼叫 publish、不需要知道誰在聽；訂閱端註冊監聽器、不需要知道是誰發的。這個解耦讓 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 的發布點跟消費點可以獨立增減，不必逐一牽線。</p>
<h2 id="概念位置">概念位置</h2>
<p>EventBus 是 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 的傳輸機制、不是事件本身：event 是「發生了什麼」的事實記錄，EventBus 是讓這個事實從發布端跑到訂閱端的管道。EventBus 只解決「怎麼送達」，送達之後訂閱端該把事件當事實通知還是當變更信號來用，是另一個判準，見 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>訂閱端掛上「監聽全部事件、任何事件進來就重新查詢」這種全事件過濾器，是把 EventBus 兼職成變更通知管道的訊號——這條路徑上線初期有效，但涵蓋面等於「有沒有人記得發事件」，跟寫入操作的集合不天然相等，多個視圖各自解「怎麼知道資料變了」時容易補出交叉且不完整的補償策略。</p>
<h2 id="設計責任">設計責任</h2>
<p>EventBus 只承擔「發布／訂閱」這一層機制、不承擔涵蓋保證——涵蓋是靠每個寫入路徑記得呼叫 publish 換來的，EventBus 本身不驗證有沒有漏發。需要涵蓋面天然等於寫入操作集合的場景，正確載體是 <a href="/blog/ddd/knowledge-cards/observation-outlet/" data-link-title="Observation Outlet（觀測出口）" data-link-desc="repository 只有 pull 介面、衍生視圖靠補償刷新，考慮補「資料變了」的推送能力時使用。觀測出口是 pull 介面的 push 對應——能力橫跨三層、歸屬由各層的表達語言決定。">observation outlet</a> 的狀態流、不是把 EventBus 的監聽範圍擴大，完整案例見 <a href="/blog/ddd/observation-outlet-responsibility-split/" data-link-title="觀測出口的職責三分" data-link-desc="repository 要補「資料變了」的推送能力、卻不確定 Stream 介面放 domain 算不算洩漏時使用。歸屬判準是介面用什麼語言表達、不是需求來自誰：契約歸 domain、變更偵測歸 infrastructure、框架訂閱歸組裝層。">觀測出口的職責三分</a>。</p>
]]></content:encoded></item><item><title>CQRS</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/cqrs/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/cqrs/</guid><description>&lt;p>多數系統預設讀寫共用同一個模型：同一組 entity 既承接寫入的一致性檢查、也承接查詢的形狀需求。CQRS（Command Query Responsibility Segregation）是把這兩個責任拆開的架構決定——寫側走 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root&lt;/a>、守住不變式與一致性邊界；讀側走 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model&lt;/a>、只服務查詢要的形狀，不承擔寫入責任。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>CQRS 是一道讀側設計階梯的頂端、而非二選一的開關。讀側的設計從「消費端自行投影 aggregate 形狀」開始，中間經過「抽獨立讀 port」「查詢形狀專用投影」，到頂端才是 CQRS 全套——讀寫獨立儲存、由 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 同步讀模型。多數專案的正確位置在階梯低處、不是頂端，升級由訊號驅動而非架構偏好。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>讀寫分離的討論從「查詢方法該不該搬出 repository」，逐漸變成「讀模型需不需要自己的儲存、能不能接受跟寫側短暫不一致」，是逼近階梯頂端的訊號——第四階的代價是最終一致性進入系統語意，畫面可能短暫顯示舊值，而這是設計內行為，不是 bug。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>要不要上 CQRS 全套，不看「這個模式聽起來很適合」，看五個具體訊號（讀需求增生、形狀偏離、負載分歧、新鮮度分級、獨立演進）是否命中——本卡只回答「CQRS 是什麼、階梯怎麼分階」，訊號的完整推導與升級路徑是 &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></description><content:encoded><![CDATA[<p>多數系統預設讀寫共用同一個模型：同一組 entity 既承接寫入的一致性檢查、也承接查詢的形狀需求。CQRS（Command Query Responsibility Segregation）是把這兩個責任拆開的架構決定——寫側走 <a href="/blog/ddd/knowledge-cards/aggregate-root/" data-link-title="Aggregate Root" data-link-desc="跨物件一致性的邊界設計時使用。聚合根是對外代表一組資料一致性的邊界物件——外部只跟它互動、它保證內部的不變式。">aggregate root</a>、守住不變式與一致性邊界；讀側走 <a href="/blog/ddd/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="查詢該由 repository 回 aggregate、還是該有自己的查詢側模型時使用。read model 是為讀需求的形狀而建的模型——回答「畫面需要什麼形狀」、與 aggregate 的形狀分離。">read model</a>、只服務查詢要的形狀，不承擔寫入責任。</p>
<h2 id="概念位置">概念位置</h2>
<p>CQRS 是一道讀側設計階梯的頂端、而非二選一的開關。讀側的設計從「消費端自行投影 aggregate 形狀」開始，中間經過「抽獨立讀 port」「查詢形狀專用投影」，到頂端才是 CQRS 全套——讀寫獨立儲存、由 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 同步讀模型。多數專案的正確位置在階梯低處、不是頂端，升級由訊號驅動而非架構偏好。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>讀寫分離的討論從「查詢方法該不該搬出 repository」，逐漸變成「讀模型需不需要自己的儲存、能不能接受跟寫側短暫不一致」，是逼近階梯頂端的訊號——第四階的代價是最終一致性進入系統語意，畫面可能短暫顯示舊值，而這是設計內行為，不是 bug。</p>
<h2 id="設計責任">設計責任</h2>
<p>要不要上 CQRS 全套，不看「這個模式聽起來很適合」，看五個具體訊號（讀需求增生、形狀偏離、負載分歧、新鮮度分級、獨立演進）是否命中——本卡只回答「CQRS 是什麼、階梯怎麼分階」，訊號的完整推導與升級路徑是 <a href="/blog/ddd/read-model-upgrade-signals/" data-link-title="讀模型的升級判準" data-link-desc="repository 開始長出畫面專用查詢方法、或有人提議「上 CQRS」時使用。讀側是一道階梯而不是開關：訊號決定該爬到哪一階，自檢問句是「這個查詢回傳的是讀的形狀、還是 aggregate 的形狀」。">讀模型的升級判準</a> 這篇的內容，要判斷自己的專案該停在哪一階、讀那篇。</p>
]]></content:encoded></item><item><title>Event-Carried State Transfer</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/event-carried-state-transfer/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/event-carried-state-transfer/</guid><description>&lt;p>event-carried state transfer 是刻意讓 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event&lt;/a> 的 payload 攜帶足量的當前狀態、不只是「發生了什麼」的事實本身。目的是讓下游服務收到事件後就能拿到完整資訊、不必回頭同步查詢來源系統——payload 帶全量狀態在這裡是設計選擇，不是把事件當狀態通知的誤用。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這個設計只在跨服務、跨信任邊界的情境下成立。同進程情境下，事件開始攜帶全量狀態通常是載體錯位的訊號——消費者真正要問的是「現在是什麼」，正確載體是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream&lt;/a> 而非塞胖的事件；跨服務時「回頭查詢來源」本身有同步耦合的成本，event-carried state transfer 用胖 payload 換掉這個成本，是同一個訊號在不同信任邊界下的相反判讀，完整推導見 &lt;a href="https://tarrragon.github.io/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>事件的 payload 從「哪本書、什麼時間」這類事實欄位，長出「現在的全貌」這類當前狀態欄位，是判讀的觸發點；判讀不看 payload 胖不胖，看消費者是不是跟事件來源在同一個信任邊界——邊界內是錯位、邊界外是正當設計。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>event-carried state transfer 換來的是下游服務不必為了拿到完整資訊而同步呼叫來源，代價是事件 payload 的 schema 跟著狀態演進、下游要一起處理相容性。這個代價只有在真的省下跨服務同步查詢時才划算，同進程情境沒有這筆成本可省，應優先考慮 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>event-carried state transfer 是刻意讓 <a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a> 的 payload 攜帶足量的當前狀態、不只是「發生了什麼」的事實本身。目的是讓下游服務收到事件後就能拿到完整資訊、不必回頭同步查詢來源系統——payload 帶全量狀態在這裡是設計選擇，不是把事件當狀態通知的誤用。</p>
<h2 id="概念位置">概念位置</h2>
<p>這個設計只在跨服務、跨信任邊界的情境下成立。同進程情境下，事件開始攜帶全量狀態通常是載體錯位的訊號——消費者真正要問的是「現在是什麼」，正確載體是 <a href="/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream</a> 而非塞胖的事件；跨服務時「回頭查詢來源」本身有同步耦合的成本，event-carried state transfer 用胖 payload 換掉這個成本，是同一個訊號在不同信任邊界下的相反判讀，完整推導見 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>事件的 payload 從「哪本書、什麼時間」這類事實欄位，長出「現在的全貌」這類當前狀態欄位，是判讀的觸發點；判讀不看 payload 胖不胖，看消費者是不是跟事件來源在同一個信任邊界——邊界內是錯位、邊界外是正當設計。</p>
<h2 id="設計責任">設計責任</h2>
<p>event-carried state transfer 換來的是下游服務不必為了拿到完整資訊而同步呼叫來源，代價是事件 payload 的 schema 跟著狀態演進、下游要一起處理相容性。這個代價只有在真的省下跨服務同步查詢時才划算，同進程情境沒有這筆成本可省，應優先考慮 <a href="/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream</a>。</p>
]]></content:encoded></item><item><title>Shared Mutable State（共享可變狀態）</title><link>https://tarrragon.github.io/blog/ddd/knowledge-cards/shared-mutable-state/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/knowledge-cards/shared-mutable-state/</guid><description>&lt;p>共享可變狀態是一個值放在多方都讀得到、任一方都改得動的位置，而它的當前值決定其他地方的行為。控制器持有的欄位、服務層的快取、被多個畫面訂閱的狀態物件都屬於這一類。它跟 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot&lt;/a> 相對：snapshot 凍結某一時刻的複本、不隨現在漂移；共享可變狀態則沒有版本，讀到的永遠是最後一次寫入的結果。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>共享可變狀態的代價不在讀寫本身，在於它&lt;strong>會長出跨函式的時序約束&lt;/strong>：「這個值必須活過那一次重設」「A 要在 B 清空之前讀完」。這類約束是 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant&lt;/a> 的一種，但落點的處境不同——單一物件的不變式可以做進建構子或型別簽名，跨函式的時序約束在型別簽名裡沒有位置、建構子也檢查不到，剩下的落點只有一條會紅的測試（強制層的取捨見 &lt;a href="https://tarrragon.github.io/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次&lt;/a>）。&lt;/p>
&lt;p>把值收成呼叫時傳入的參數，可見範圍縮到那一次呼叫，約束就不存在了。所以遇到這類約束時的第一個問題是「這個值需不需要共享」，而不是「誰來守這個順序」。&lt;/p>
&lt;p>&lt;strong>可被觀察不等於可被寫入&lt;/strong>。一個值要讓畫面訂閱（&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream&lt;/a> 的形態）是讀側需求，跟「誰改得動它」是兩個獨立的決定。兩者被同一個欄位承擔時，「因為畫面要看所以只好共享」會被誤當成「所以也只好開放多方寫入」。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>註解出現「重設時不要清掉這個值」「先讀再清」這類順序叮嚀時，共享可變狀態已經長出時序約束，而強制還停在文件層。測試要先擺好某個欄位才跑得起來、而那個欄位不在被測函式的參數列裡，是同一個訊號的測試側形態。同一個欄位的寫入點散在多個控制器或服務時，「當前值是誰寫的」已經無法從單一位置回答。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>先問這個值需不需要共享——能收成參數就消除整組約束，不需要任何一層守它。必須共享時把寫入點收斂到單一路徑（讀可以開放、寫要有唯一入口），並把「可觀察」與「可寫入」分開設計。約束消不掉時，交給一條會紅的行為測試，而不是交給一行叮嚀順序的註解——判準與當場可執行的驗證見 &lt;a href="https://tarrragon.github.io/blog/report/protective-comment-signals-missing-enforcement/" data-link-title="寫註解的動機是怕被改壞時，要處理的是那個約束、不是那行文字" data-link-desc="準備為一段程式寫註解、而動機是怕有人改壞它時使用。註解不參與執行、改壞的當下不產生訊號；防護需求要先問這個約束能不能被消除，不能消除才交給會發聲的機制，而判定靠當場破壞。">#253 寫註解的動機是怕被改壞時要處理的是那個約束&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>共享可變狀態是一個值放在多方都讀得到、任一方都改得動的位置，而它的當前值決定其他地方的行為。控制器持有的欄位、服務層的快取、被多個畫面訂閱的狀態物件都屬於這一類。它跟 <a href="/blog/ddd/knowledge-cards/snapshot/" data-link-title="Snapshot" data-link-desc="歷史記錄是否應該凍結當時狀態時使用。Snapshot 是某一時刻的狀態複本——歷史不隨現在的資料漂移。">snapshot</a> 相對：snapshot 凍結某一時刻的複本、不隨現在漂移；共享可變狀態則沒有版本，讀到的永遠是最後一次寫入的結果。</p>
<h2 id="概念位置">概念位置</h2>
<p>共享可變狀態的代價不在讀寫本身，在於它<strong>會長出跨函式的時序約束</strong>：「這個值必須活過那一次重設」「A 要在 B 清空之前讀完」。這類約束是 <a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">invariant</a> 的一種，但落點的處境不同——單一物件的不變式可以做進建構子或型別簽名，跨函式的時序約束在型別簽名裡沒有位置、建構子也檢查不到，剩下的落點只有一條會紅的測試（強制層的取捨見 <a href="/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次</a>）。</p>
<p>把值收成呼叫時傳入的參數，可見範圍縮到那一次呼叫，約束就不存在了。所以遇到這類約束時的第一個問題是「這個值需不需要共享」，而不是「誰來守這個順序」。</p>
<p><strong>可被觀察不等於可被寫入</strong>。一個值要讓畫面訂閱（<a href="/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">state stream</a> 的形態）是讀側需求，跟「誰改得動它」是兩個獨立的決定。兩者被同一個欄位承擔時，「因為畫面要看所以只好共享」會被誤當成「所以也只好開放多方寫入」。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>註解出現「重設時不要清掉這個值」「先讀再清」這類順序叮嚀時，共享可變狀態已經長出時序約束，而強制還停在文件層。測試要先擺好某個欄位才跑得起來、而那個欄位不在被測函式的參數列裡，是同一個訊號的測試側形態。同一個欄位的寫入點散在多個控制器或服務時，「當前值是誰寫的」已經無法從單一位置回答。</p>
<h2 id="設計責任">設計責任</h2>
<p>先問這個值需不需要共享——能收成參數就消除整組約束，不需要任何一層守它。必須共享時把寫入點收斂到單一路徑（讀可以開放、寫要有唯一入口），並把「可觀察」與「可寫入」分開設計。約束消不掉時，交給一條會紅的行為測試，而不是交給一行叮嚀順序的註解——判準與當場可執行的驗證見 <a href="/blog/report/protective-comment-signals-missing-enforcement/" data-link-title="寫註解的動機是怕被改壞時，要處理的是那個約束、不是那行文字" data-link-desc="準備為一段程式寫註解、而動機是怕有人改壞它時使用。註解不參與執行、改壞的當下不產生訊號；防護需求要先問這個約束能不能被消除，不能消除才交給會發聲的機制，而判定靠當場破壞。">#253 寫註解的動機是怕被改壞時要處理的是那個約束</a>。</p>
]]></content:encoded></item></channel></rss>