<?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>Command on Tarragon</title><link>https://tarrragon.github.io/blog/tags/command/</link><description>Recent content in Command on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 16 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/command/index.xml" rel="self" type="application/rss+xml"/><item><title>domain event 與命令、查詢的分界</title><link>https://tarrragon.github.io/blog/ddd/domain-event-vs-command-and-query/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/domain-event-vs-command-and-query/</guid><description>&lt;p>跨模組的訊息有三種責任形狀。&lt;strong>命令&lt;/strong>（command）表達意圖：「去做這件事」，有唯一的處理者、可以拒絕、可以失敗。&lt;strong>&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;/strong> 記錄事實：「這件事已經發生」，有任意多的訂閱者、沒有拒絕的位置。&lt;strong>查詢&lt;/strong>（query）是對話：「告訴我這個」，一問一答、呼叫端要等到答案。三者的分界是責任結構、不是命名風格。本章的判準一句話：&lt;strong>看訊息對消費者的要求&lt;/strong>——要求執行、給命令；只供反應、給事件；要等答案、給查詢通道。&lt;/p>
&lt;h2 id="三種訊息的責任結構">三種訊息的責任結構&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>命令&lt;/th>
 &lt;th>domain event&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;code>ImportBook&lt;/code>）&lt;/td>
 &lt;td>過去式（&lt;code>BookImported&lt;/code>）&lt;/td>
 &lt;td>疑問（&lt;code>getBookInfo()&lt;/code>）&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;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>不等、fire and forget&lt;/td>
 &lt;td>必等——答案是流程的前提&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「失敗語意」那一列是三者最深的差異。命令的處理者可以說不：庫存不足、狀態不允許、驗證失敗——失敗是命令語意的一部分、發送端要準備收到拒絕。事件沒有這個位置：&lt;code>BookImported&lt;/code> 發出時匯入已經完成，訂閱者遲到、出錯、根本不存在，都改變不了這個事實——訂閱者的失敗是訂閱者自己的問題、不回流到發布端。查詢的失敗又是另一種：查不到、等太久、通道斷了，每一種都要有回應路徑、因為呼叫端停在那裡等。&lt;/p>
&lt;p>責任結構決定演進自由度：事件的發布端不知道誰在聽、加訂閱者不改發布端；命令的發送端跟處理者是一對一契約、換處理者要重新對齊語意；查詢的兩端綁最緊、簽名一動兩邊都動。&lt;/p>
&lt;h2 id="命名是責任結構的第一道宣告">命名是責任結構的第一道宣告&lt;/h2>
&lt;p>事件用過去式命名、是把責任結構烙在讀者第一眼接觸的地方：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">BookImported&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">DomainEvent&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="c1">// 事實：已發生、不可否認
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ImportBook&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">DomainEvent&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="err">命令的形狀——「去匯入這本書」&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ImportBook&lt;/code> 這個名字的問題超出風格：動詞開頭是命令的形狀，訂閱者會用命令的心智模型寫處理邏輯——以為自己能影響流程、以為失敗會被重試。過去式的 &lt;code>Imported&lt;/code> 宣告木已成舟，訂閱者知道自己只能對事實做反應。一個書籍管理 App 的命名規範修正記錄了這條分界的完整推導、以及過去式字尾自動檢查的脆弱性（不規則動詞全數漏接——偵測可以機械化、判定要看語意）：&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）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">事件命名的過去式是語意類別、不是風格&lt;/a>。&lt;/p>
&lt;h2 id="查詢裝進事件通道手工重建-rpc">查詢裝進事件通道：手工重建 RPC&lt;/h2>
&lt;p>第二種錯位的方向相反：把對話裝進通知通道。同一個 App 的借閱功能要在建立借閱記錄前查書籍資訊，直接呼叫另一個 domain 的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/repository/" data-link-title="Repository" data-link-desc="查詢方法該留在 repository、還是該抽成獨立讀模型時使用。repository 是 aggregate 的存取抽象——回傳的形狀是 aggregate 的形狀，不是讀的形狀。">repository&lt;/a> 違反依賴方向；修正方案選了事件驅動的 request/response——&lt;code>BookInfoRequested&lt;/code> 發出、&lt;code>BookInfoProvided&lt;/code> 回應。耦合確實解掉了，但設計規格裡跟著出現的每一項機制、都是同步呼叫免費附贈的東西：&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a> 配對請求回應&lt;/td>
 &lt;td>呼叫堆疊天然對應&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>waitFor + timeout&lt;/td>
 &lt;td>函式 return&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「查無資料」的錯誤事件&lt;/td>
 &lt;td>回傳 null／拋例外&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>超時與通道故障路徑&lt;/td>
 &lt;td>不存在——同步呼叫沒有「沒人接」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的形狀有個名字：用事件通道手工重建 RPC。事件的天然語意是通知——fire and forget、任意多訂閱者、無所謂回應；一問一答的對話硬放上通知通道，RPC 的基礎設施就得自己蓋一遍、而且每個跨 domain 查詢點都要再蓋一遍。名字先洩漏了錯位：&lt;code>BookInfoRequested&lt;/code> 的「Requested」記錄的是「有人想要」這個請求、不是業務世界發生的事實——事件命名的過去式測試在這裡當了偵測器，兩條判準在名字上交會。&lt;/p>
&lt;p>事件真正買到、而更便宜的工具給不了的能力有兩樣：&lt;strong>時間解耦&lt;/strong>（發布者不等回應、雙方可以不同時在線）與&lt;strong>基數解耦&lt;/strong>（一個事件任意多訂閱者）。這個案例的查詢是同進程、一對一、呼叫端必須等到答案——三個特徵全部用不到事件的能力。解依賴方向有便宜一個量級的工具：消費端宣告自己需要的 &lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port&lt;/a>、對方提供實作、組裝層接線，依賴倒置同樣成立、呼叫還是一次普通的 await。完整比價（含 Port 版的介面設計）見 &lt;a href="https://tarrragon.github.io/blog/work-log/cross_domain_event_request_response_cost/" data-link-title="用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單" data-link-desc="domain 直接查另一個 domain 的 repository 違反依賴方向；改事件驅動的 request/response 解了耦、但帳單具體：correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」：前者用消費端 Port 就夠、後者才值得付事件的價。">用事件做同步查詢等於手工重建 RPC&lt;/a>。&lt;/p>
&lt;p>判準收斂成兩行：&lt;/p>
&lt;ul>
&lt;li>要解的是&lt;strong>依賴方向&lt;/strong>（同進程、一對一、要等答案）→ 消費端 port、付一張介面的價&lt;/li>
&lt;li>要解的是&lt;strong>時間、部署或基數&lt;/strong>（跨進程、離線佇列、多方反應）→ 事件、correlation 與 timeout 是這個量級的合理成本&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&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;code>ImportBook&lt;/code>）&lt;/td>
 &lt;td>命令穿著事件的衣服——訂閱者會以為自己能影響流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件成對出現 &lt;code>XxxRequested&lt;/code>／&lt;code>XxxProvided&lt;/code> + correlation id&lt;/td>
 &lt;td>對話穿著事件的衣服——在通知通道上手工重建 RPC&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發布事件後 &lt;code>waitFor&lt;/code>／await 回應才能往下走&lt;/td>
 &lt;td>呼叫端語意是同步查詢、事件只是繞路&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訂閱者不只一個、或處理可以離線延後&lt;/td>
 &lt;td>反向確認：這時事件才是本命、別退回 port&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前三個訊號的修法方向一致：回到責任結構重新分類——是意圖就改命令通道（或直接呼叫 use case）、是對話就改查詢（消費端 port）、確認是事實才留在事件。第四個訊號防反向矯枉：把所有跨 domain 通訊都收回同步呼叫、會把真正需要時間與基數解耦的場景也一起收掉。&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="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">狀態流&lt;/a>（連續觀測）的分界是另一個維度：那裡兩邊都不要求執行、差在「發生了什麼」與「現在是什麼」的時間語意，見 &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/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>事件的另一條分界（對狀態流）在 &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>——兩章合起來把 domain event 對三個鄰居（命令、查詢、狀態流）的邊界畫完。兩個 case 的完整記錄：&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）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">事件命名的過去式&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/work-log/cross_domain_event_request_response_cost/" data-link-title="用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單" data-link-desc="domain 直接查另一個 domain 的 repository 違反依賴方向；改事件驅動的 request/response 解了耦、但帳單具體：correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」：前者用消費端 Port 就夠、後者才值得付事件的價。">用事件做同步查詢等於手工重建 RPC&lt;/a>；port 解依賴的實戰在 &lt;a href="https://tarrragon.github.io/blog/work-log/flutter_port_interface_mock_hell_isp/" data-link-title="mock 要配置 55 個方法、實際只用 5 個 — 測試痛是介面設計痛的探針" data-link-desc="service 測試的 mock 負擔正比於它依賴的介面寬度：依賴四個大介面共 55 個方法、實際呼叫 5 個，91% 的 mock 配置是純浪費、還會炸 MissingStubError。修法是介面隔離——抽出只含實際使用方法的 Port，讓 mock 縮到跟真實依賴一樣窄；為未來預留的方法用 TODO 標記啟用時機。">mock 55 個方法只用 5 個&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>跨模組的訊息有三種責任形狀。<strong>命令</strong>（command）表達意圖：「去做這件事」，有唯一的處理者、可以拒絕、可以失敗。<strong><a href="/blog/ddd/knowledge-cards/domain-event/" data-link-title="Domain Event" data-link-desc="系統裡出現「為了讓某頁刷新而補發事件」或「監聽端掛全事件過濾器」時使用。domain event 是已發生的業務事實——過去式命名、發布後不可變、錯過代表事實遺失。">domain event</a></strong> 記錄事實：「這件事已經發生」，有任意多的訂閱者、沒有拒絕的位置。<strong>查詢</strong>（query）是對話：「告訴我這個」，一問一答、呼叫端要等到答案。三者的分界是責任結構、不是命名風格。本章的判準一句話：<strong>看訊息對消費者的要求</strong>——要求執行、給命令；只供反應、給事件；要等答案、給查詢通道。</p>
<h2 id="三種訊息的責任結構">三種訊息的責任結構</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>命令</th>
          <th>domain event</th>
          <th>查詢</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>語意</td>
          <td>去做這件事</td>
          <td>這件事已發生</td>
          <td>告訴我這個</td>
      </tr>
      <tr>
          <td>命名形狀</td>
          <td>祈使（<code>ImportBook</code>）</td>
          <td>過去式（<code>BookImported</code>）</td>
          <td>疑問（<code>getBookInfo()</code>）</td>
      </tr>
      <tr>
          <td>消費者</td>
          <td>唯一處理者</td>
          <td>任意多訂閱者</td>
          <td>唯一回應者</td>
      </tr>
      <tr>
          <td>失敗語意</td>
          <td>可拒絕、可失敗、可重試</td>
          <td>無——事實不可否認</td>
          <td>有超時、有錯誤回傳</td>
      </tr>
      <tr>
          <td>發送端等不等</td>
          <td>視流程——常等處理結果</td>
          <td>不等、fire and forget</td>
          <td>必等——答案是流程的前提</td>
      </tr>
  </tbody>
</table>
<p>「失敗語意」那一列是三者最深的差異。命令的處理者可以說不：庫存不足、狀態不允許、驗證失敗——失敗是命令語意的一部分、發送端要準備收到拒絕。事件沒有這個位置：<code>BookImported</code> 發出時匯入已經完成，訂閱者遲到、出錯、根本不存在，都改變不了這個事實——訂閱者的失敗是訂閱者自己的問題、不回流到發布端。查詢的失敗又是另一種：查不到、等太久、通道斷了，每一種都要有回應路徑、因為呼叫端停在那裡等。</p>
<p>責任結構決定演進自由度：事件的發布端不知道誰在聽、加訂閱者不改發布端；命令的發送端跟處理者是一對一契約、換處理者要重新對齊語意；查詢的兩端綁最緊、簽名一動兩邊都動。</p>
<h2 id="命名是責任結構的第一道宣告">命名是責任結構的第一道宣告</h2>
<p>事件用過去式命名、是把責任結構烙在讀者第一眼接觸的地方：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">BookImported</span> <span class="kd">extends</span> <span class="n">DomainEvent</span> <span class="p">{</span> <span class="p">}</span>   <span class="c1">// 事實：已發生、不可否認
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">ImportBook</span> <span class="kd">extends</span> <span class="n">DomainEvent</span> <span class="p">{</span> <span class="p">}</span>     <span class="o">//</span> <span class="err">命令的形狀——「去匯入這本書」</span></span></span></code></pre></div><p><code>ImportBook</code> 這個名字的問題超出風格：動詞開頭是命令的形狀，訂閱者會用命令的心智模型寫處理邏輯——以為自己能影響流程、以為失敗會被重試。過去式的 <code>Imported</code> 宣告木已成舟，訂閱者知道自己只能對事實做反應。一個書籍管理 App 的命名規範修正記錄了這條分界的完整推導、以及過去式字尾自動檢查的脆弱性（不規則動詞全數漏接——偵測可以機械化、判定要看語意）：<a href="/blog/work-log/domain_event_naming_past_tense/" data-link-title="BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格" data-link-desc="domain event 用過去式命名（BookImported）因為事件是已發生的事實；動詞開頭（ImportBook）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">事件命名的過去式是語意類別、不是風格</a>。</p>
<h2 id="查詢裝進事件通道手工重建-rpc">查詢裝進事件通道：手工重建 RPC</h2>
<p>第二種錯位的方向相反：把對話裝進通知通道。同一個 App 的借閱功能要在建立借閱記錄前查書籍資訊，直接呼叫另一個 domain 的 <a href="/blog/ddd/knowledge-cards/repository/" data-link-title="Repository" data-link-desc="查詢方法該留在 repository、還是該抽成獨立讀模型時使用。repository 是 aggregate 的存取抽象——回傳的形狀是 aggregate 的形狀，不是讀的形狀。">repository</a> 違反依賴方向；修正方案選了事件驅動的 request/response——<code>BookInfoRequested</code> 發出、<code>BookInfoProvided</code> 回應。耦合確實解掉了，但設計規格裡跟著出現的每一項機制、都是同步呼叫免費附贈的東西：</p>
<table>
  <thead>
      <tr>
          <th>事件版要自己蓋的</th>
          <th>同步呼叫的對應物</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> 配對請求回應</td>
          <td>呼叫堆疊天然對應</td>
      </tr>
      <tr>
          <td>waitFor + timeout</td>
          <td>函式 return</td>
      </tr>
      <tr>
          <td>「查無資料」的錯誤事件</td>
          <td>回傳 null／拋例外</td>
      </tr>
      <tr>
          <td>超時與通道故障路徑</td>
          <td>不存在——同步呼叫沒有「沒人接」</td>
      </tr>
  </tbody>
</table>
<p>這張表的形狀有個名字：用事件通道手工重建 RPC。事件的天然語意是通知——fire and forget、任意多訂閱者、無所謂回應；一問一答的對話硬放上通知通道，RPC 的基礎設施就得自己蓋一遍、而且每個跨 domain 查詢點都要再蓋一遍。名字先洩漏了錯位：<code>BookInfoRequested</code> 的「Requested」記錄的是「有人想要」這個請求、不是業務世界發生的事實——事件命名的過去式測試在這裡當了偵測器，兩條判準在名字上交會。</p>
<p>事件真正買到、而更便宜的工具給不了的能力有兩樣：<strong>時間解耦</strong>（發布者不等回應、雙方可以不同時在線）與<strong>基數解耦</strong>（一個事件任意多訂閱者）。這個案例的查詢是同進程、一對一、呼叫端必須等到答案——三個特徵全部用不到事件的能力。解依賴方向有便宜一個量級的工具：消費端宣告自己需要的 <a href="/blog/ddd/knowledge-cards/port/" data-link-title="Port" data-link-desc="判斷介面該宣告在哪一層、依賴方向該朝哪時使用。port 是 domain 對外宣告的介面——需求用領域語言說完、技術細節留在實作端。">port</a>、對方提供實作、組裝層接線，依賴倒置同樣成立、呼叫還是一次普通的 await。完整比價（含 Port 版的介面設計）見 <a href="/blog/work-log/cross_domain_event_request_response_cost/" data-link-title="用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單" data-link-desc="domain 直接查另一個 domain 的 repository 違反依賴方向；改事件驅動的 request/response 解了耦、但帳單具體：correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」：前者用消費端 Port 就夠、後者才值得付事件的價。">用事件做同步查詢等於手工重建 RPC</a>。</p>
<p>判準收斂成兩行：</p>
<ul>
<li>要解的是<strong>依賴方向</strong>（同進程、一對一、要等答案）→ 消費端 port、付一張介面的價</li>
<li>要解的是<strong>時間、部署或基數</strong>（跨進程、離線佇列、多方反應）→ 事件、correlation 與 timeout 是這個量級的合理成本</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事件類別以動詞開頭（<code>ImportBook</code>）</td>
          <td>命令穿著事件的衣服——訂閱者會以為自己能影響流程</td>
      </tr>
      <tr>
          <td>事件成對出現 <code>XxxRequested</code>／<code>XxxProvided</code> + correlation id</td>
          <td>對話穿著事件的衣服——在通知通道上手工重建 RPC</td>
      </tr>
      <tr>
          <td>發布事件後 <code>waitFor</code>／await 回應才能往下走</td>
          <td>呼叫端語意是同步查詢、事件只是繞路</td>
      </tr>
      <tr>
          <td>訂閱者不只一個、或處理可以離線延後</td>
          <td>反向確認：這時事件才是本命、別退回 port</td>
      </tr>
  </tbody>
</table>
<p>前三個訊號的修法方向一致：回到責任結構重新分類——是意圖就改命令通道（或直接呼叫 use case）、是對話就改查詢（消費端 port）、確認是事實才留在事件。第四個訊號防反向矯枉：把所有跨 domain 通訊都收回同步呼叫、會把真正需要時間與基數解耦的場景也一起收掉。</p>
<h2 id="邊界">邊界</h2>
<p>本章處理事件與命令、查詢的訊息類型分界——三者都是離散訊息、差在責任結構。事件與 <a href="/blog/ddd/knowledge-cards/state-stream/" data-link-title="State Stream（狀態流）" data-link-desc="畫面刷新靠補償、或考慮拿既有事件當刷新訊號時使用。狀態流是持續發布「資料當前值」的觀測載體——新值蓋過舊值、錯過中間值無代價，回答「現在是什麼」。">狀態流</a>（連續觀測）的分界是另一個維度：那裡兩邊都不要求執行、差在「發生了什麼」與「現在是什麼」的時間語意，見 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">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>事件的另一條分界（對狀態流）在 <a href="/blog/ddd/domain-event-vs-state-stream/" data-link-title="domain event 與狀態流" data-link-desc="為了讓某個畫面刷新而補發事件、或監聽端掛著全事件過濾器時使用。事件記錄離散事實、狀態流發布連續觀測——判準是消費者問「發生了什麼」還是「現在是什麼」；載體借用的代價是涵蓋面靠枚舉維持。">domain event 與狀態流</a>——兩章合起來把 domain event 對三個鄰居（命令、查詢、狀態流）的邊界畫完。兩個 case 的完整記錄：<a href="/blog/work-log/domain_event_naming_past_tense/" data-link-title="BookImported 不能寫成 ImportBook — 事件命名的過去式是語意類別、不是風格" data-link-desc="domain event 用過去式命名（BookImported）因為事件是已發生的事實；動詞開頭（ImportBook）是命令的形狀、訂閱者會誤讀成指令。字尾清單的自動檢查抓不住不規則動詞——偵測可機械化、判定要看語意。附一次版本終止：工作日誌宣稱的命名問題實際早已不存在、執行前驗前提省下整輪流程。">事件命名的過去式</a>、<a href="/blog/work-log/cross_domain_event_request_response_cost/" data-link-title="用事件做同步查詢、等於手工重建 RPC — 跨 domain 解耦的完整帳單" data-link-desc="domain 直接查另一個 domain 的 repository 違反依賴方向；改事件驅動的 request/response 解了耦、但帳單具體：correlation id 配對並發、timeout 機制、三條錯誤路徑——都是同步呼叫免費附贈的東西。判準是需要解耦「依賴方向」還是「時間與部署」：前者用消費端 Port 就夠、後者才值得付事件的價。">用事件做同步查詢等於手工重建 RPC</a>；port 解依賴的實戰在 <a href="/blog/work-log/flutter_port_interface_mock_hell_isp/" data-link-title="mock 要配置 55 個方法、實際只用 5 個 — 測試痛是介面設計痛的探針" data-link-desc="service 測試的 mock 負擔正比於它依賴的介面寬度：依賴四個大介面共 55 個方法、實際呼叫 5 個，91% 的 mock 配置是純浪費、還會炸 MissingStubError。修法是介面隔離——抽出只含實際使用方法的 Port，讓 mock 縮到跟真實依賴一樣窄；為未來預留的方法用 TODO 標記啟用時機。">mock 55 個方法只用 5 個</a>。</p>
]]></content:encoded></item></channel></rss>