<?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>Factory on Tarragon</title><link>https://tarrragon.github.io/blog/tags/factory/</link><description>Recent content in Factory 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/tags/factory/index.xml" rel="self" type="application/rss+xml"/><item><title>建構路徑設計</title><link>https://tarrragon.github.io/blog/ddd/construction-path-design/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ddd/construction-path-design/</guid><description>&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>
&lt;h2 id="工廠的表達力與消費者需求">工廠的表達力與消費者需求&lt;/h2>
&lt;p>工廠（建構子、命名工廠、builder）是合法物件的唯一入口。它的職責有兩個：保證出生即合法（不變式檢查）、同時覆蓋消費者的正當需求（給得出消費者想要的合法組合）。兩個職責有張力——保證越嚴、接受的參數越窄；解法是讓工廠的表達力跟上需求、而不是放棄保證。&lt;/p>
&lt;p>一個書籍管理 App 的 &lt;code>createForTest&lt;/code> 工廠只接受 &lt;code>id&lt;/code> / &lt;code>title&lt;/code> / &lt;code>author&lt;/code> / &lt;code>isbn&lt;/code> 四個參數。測試想表達「預設 tags 再追加三個自訂 tag」——這是合法的需求、工廠的表達力沒覆蓋到。需求進不了工廠，就會從別的出口出去——在這個專案，出口是全欄位 copyWith（&lt;a href="https://tarrragon.github.io/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計&lt;/a>）。&lt;/p>
&lt;p>工廠參數列的設計判準：收的是「有語意的需求」（預設 tags 加自訂、指定初始狀態、帶入驗證過的關聯物件），不是「所有欄位的任意值」。後者退化成另一個全欄位建構器，保證也跟著消失——分寸在「覆蓋高頻的合法組合、不開放任意拼裝」——「高頻」的門檻見下一節：同一組合出現第二次就收進工廠。&lt;/p>
&lt;h2 id="逃生口吸收建構路徑的缺陷">逃生口吸收建構路徑的缺陷&lt;/h2>
&lt;p>逃生口吸收是建構路徑缺陷長期不被修好的核心機制：表達力缺口碰上萬能拼裝工具、上游缺陷被下游繞道消化、工廠永遠不會被迫改進。機制分三步。第一步、工廠表達不了需求：上述 &lt;code>createForTest&lt;/code> 不接受 &lt;code>bookTags&lt;/code>。第二步、逃生口接住需求：copyWith 總是能拼、它成為唯一出路。第三步、拼裝現場的最短路徑埋下語意錯誤：在用 copyWith 拼的當下、順手建個臨時物件撈預設值（&lt;code>Book.createForTest(id: 'tmp').bookTags&lt;/code>）是阻力最小的寫法——而 &lt;code>'tmp'&lt;/code> 不滿足 BookId 的長度約束。同型寫法幾天前才在另一個檔案修過一次。&lt;/p>
&lt;p>關鍵在第二步的因果方向：逃生口不只讓錯誤寫法變可能、它讓正確的修法變不必要。沒有逃生口時、工廠表達力不足會立刻擋住需求、逼出「幫工廠加參數」的修法；有逃生口時、每個被擋的需求都繞出去了、工廠的缺陷永遠不會痛。同族語意錯誤第二次出現就是結構在選擇行為的證據：兩個作者（或同作者在不同時刻）在同一個表達力缺口前面各自獨立走出同一條最短路徑（&lt;a href="https://tarrragon.github.io/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計&lt;/a>）。&lt;/p>
&lt;p>修法對準上游：讓工廠能表達需求（&lt;code>createForTest&lt;/code> 接受 &lt;code>bookTags&lt;/code>），拼裝的動機就消失了。修每一個拼裝點——把 &lt;code>'tmp'&lt;/code> 改成合法長度、把臨時物件改成取自己的欄位——只消掉當次症狀，下一個測試還是會在同一個表達力缺口前面選擇拼裝。門檻是兩次：第一次拼裝可以當個案修掉——只出現一次的特殊組合用拼裝是合理的、工廠也有參數列膨脹的反向代價。第二次出現就該停止修拼裝點、轉頭找共同面對的表達力缺口。工廠產物的 copyWith 呼叫在覆寫哪些欄位、就是一份「工廠該收而沒收的參數」的實證清單——繞道流量本身是免費的需求調查（&lt;a href="https://tarrragon.github.io/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223 逃生口吸收建構路徑的缺陷&lt;/a>）。&lt;/p>
&lt;h2 id="原始值的官方出口">原始值的官方出口&lt;/h2>
&lt;p>建構路徑的另一面是出口：value object 封裝了原始值之後、基礎設施層（快取 key、資料庫 column、序列化）要怎麼拿到它需要的表示。穩態是封裝任意操作、而非封裝取值本身——基礎設施邊界的需求是正當的、該給語意明確的出口。這個穩態從擺盪中浮出。&lt;/p>
&lt;p>同一個書籍管理 App 的 value object 封裝在三個版本間擺盪。v0.7.6 全移除（裸字串），不變式失去強制點、任何字串冒充任何 ID。隨後 v0.8.10 推向另一極——完全封裝、目標零個 &lt;code>.value&lt;/code> 外部存取——基礎設施層的 176 個編譯錯誤暴露這些消費是正當需求，把它們全逼到 &lt;code>toString()&lt;/code> 上是語意寄生、格式一改快取 key 靜默換一批。最終 v0.8.13 加回 &lt;code>.value&lt;/code> getter、被重新命名成「相容性介面」，實質是理想在依賴現實前退讓（&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_value_object_encapsulation_oscillation/" data-link-title="Value Object 的封裝擺盪：從全移除、完全封裝、到加回 .value getter" data-link-desc="VO 的封裝邊界在兩個極端之間來回——純字串（零封裝）跟完全封裝（禁止取原始值）各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口，而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。">VO 的封裝擺盪&lt;/a>）。&lt;/p>
&lt;p>穩態的操作化：出口的名字說明用途：&lt;code>toJsonString()&lt;/code>（序列化）、&lt;code>toDbValue()&lt;/code>（持久化）、&lt;code>displayValue&lt;/code>（UI）。「誰在拆封」就變成可 grep 的——一個 POS 專案的 Money 型別用 &lt;code>toDecimal()&lt;/code> 作為官方拆封口、搜尋 &lt;code>toDecimal(&lt;/code> 就是拆封清單。沒有官方出口的世界裡、下游用 &lt;code>toString()&lt;/code> 硬接或把 &lt;code>.value&lt;/code> getter 加回來、拆封處完全不可追蹤。&lt;/p>
&lt;p>擺盪的根治不在選對某一極、在於把邊界寫成決策記錄：哪些出口存在、各自給誰用、為什麼不多不少。出口增長到大多數消費者都有專屬方法時、封裝的值已不成立——此時直接暴露一個語意明確的拆封方法，比維護多個用途近似的出口便宜。沒有這份記錄、每一任重構者都會從自己撞到的那一面出發、再推向另一個極端。判讀徵兆：重構記錄出現「相容性介面」——檢查它是不是理想撤退的重新命名；決策記錄只記贏面——反方向的代價沒被記、下次擺回去的推力還在；封裝重構在基礎設施層爆量——訊號是「這些消費是正當的」、該給出口不是硬改。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>測試 Arrange 段出現「工廠 + copyWith 拼裝」——盤點拼裝在補什麼欄位、高頻欄位收進工廠參數列。工廠參數列長期不變、而它的產物被 copyWith 環繞——表達力已落後需求，原則見 &lt;a href="https://tarrragon.github.io/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223&lt;/a>。&lt;/li>
&lt;li>建一個立刻丟棄的物件、只為了拿它的一個欄位？這是語意錯誤的標記，先找它真正想表達的需求。&lt;/li>
&lt;li>同族語意錯誤第二次出現——停止修個案、找兩個案共同面對的建構路徑缺口。&lt;/li>
&lt;li>&lt;code>toString()&lt;/code> 被當成取值 API 用在快取 key / DB 值——語意寄生、格式一改靜默事故。出口要有語意明確的名字，語意封閉判準見 &lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a>。&lt;/li>
&lt;li>VO 封裝重構的編譯錯誤在基礎設施層爆量——基礎設施是正當消費者、該給出口。&lt;/li>
&lt;/ul>
&lt;h2 id="下一步">下一步&lt;/h2>
&lt;ul>
&lt;li>物件組好之後、整個系統怎麼被組起來：&lt;a href="https://tarrragon.github.io/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性&lt;/a>&lt;/li>
&lt;li>出生合法的機制：&lt;a href="https://tarrragon.github.io/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次&lt;/a>&lt;/li>
&lt;li>變更路徑收斂：&lt;a href="https://tarrragon.github.io/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡&lt;/a>&lt;/li>
&lt;li>型別類別的入口判準：&lt;a href="https://tarrragon.github.io/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a>&lt;/li>
&lt;li>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;/li>
&lt;li>Dart 的語言細節（copyWith 三態缺口、工廠表達力擴充、extension type 的拆封口）：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_value_object_encapsulation_oscillation/" data-link-title="Value Object 的封裝擺盪：從全移除、完全封裝、到加回 .value getter" data-link-desc="VO 的封裝邊界在兩個極端之間來回——純字串（零封裝）跟完全封裝（禁止取原始值）各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口，而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。">VO 的封裝擺盪&lt;/a>&lt;/li>
&lt;li>原則層：&lt;a href="https://tarrragon.github.io/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223 逃生口吸收建構路徑的缺陷&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p><a href="/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次</a> 展開了建構期不變式（物件建構時必須通過的業務規則檢查）——物件出生的那一刻就必須合法。本章接下來的問題是建構路徑本身的設計：工廠的表達力有沒有覆蓋消費者的正當需求、出口有沒有給原始值一個語意明確的位置。「讓違反規則的路徑走不通」在建構路徑有一個常被忽略的前提——「讓遵守規則的路徑走得通」：正當需求在工廠裡找不到出口時、逃生口就成了預設路徑、規則也跟著失效。</p>
<h2 id="工廠的表達力與消費者需求">工廠的表達力與消費者需求</h2>
<p>工廠（建構子、命名工廠、builder）是合法物件的唯一入口。它的職責有兩個：保證出生即合法（不變式檢查）、同時覆蓋消費者的正當需求（給得出消費者想要的合法組合）。兩個職責有張力——保證越嚴、接受的參數越窄；解法是讓工廠的表達力跟上需求、而不是放棄保證。</p>
<p>一個書籍管理 App 的 <code>createForTest</code> 工廠只接受 <code>id</code> / <code>title</code> / <code>author</code> / <code>isbn</code> 四個參數。測試想表達「預設 tags 再追加三個自訂 tag」——這是合法的需求、工廠的表達力沒覆蓋到。需求進不了工廠，就會從別的出口出去——在這個專案，出口是全欄位 copyWith（<a href="/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計</a>）。</p>
<p>工廠參數列的設計判準：收的是「有語意的需求」（預設 tags 加自訂、指定初始狀態、帶入驗證過的關聯物件），不是「所有欄位的任意值」。後者退化成另一個全欄位建構器，保證也跟著消失——分寸在「覆蓋高頻的合法組合、不開放任意拼裝」——「高頻」的門檻見下一節：同一組合出現第二次就收進工廠。</p>
<h2 id="逃生口吸收建構路徑的缺陷">逃生口吸收建構路徑的缺陷</h2>
<p>逃生口吸收是建構路徑缺陷長期不被修好的核心機制：表達力缺口碰上萬能拼裝工具、上游缺陷被下游繞道消化、工廠永遠不會被迫改進。機制分三步。第一步、工廠表達不了需求：上述 <code>createForTest</code> 不接受 <code>bookTags</code>。第二步、逃生口接住需求：copyWith 總是能拼、它成為唯一出路。第三步、拼裝現場的最短路徑埋下語意錯誤：在用 copyWith 拼的當下、順手建個臨時物件撈預設值（<code>Book.createForTest(id: 'tmp').bookTags</code>）是阻力最小的寫法——而 <code>'tmp'</code> 不滿足 BookId 的長度約束。同型寫法幾天前才在另一個檔案修過一次。</p>
<p>關鍵在第二步的因果方向：逃生口不只讓錯誤寫法變可能、它讓正確的修法變不必要。沒有逃生口時、工廠表達力不足會立刻擋住需求、逼出「幫工廠加參數」的修法；有逃生口時、每個被擋的需求都繞出去了、工廠的缺陷永遠不會痛。同族語意錯誤第二次出現就是結構在選擇行為的證據：兩個作者（或同作者在不同時刻）在同一個表達力缺口前面各自獨立走出同一條最短路徑（<a href="/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計</a>）。</p>
<p>修法對準上游：讓工廠能表達需求（<code>createForTest</code> 接受 <code>bookTags</code>），拼裝的動機就消失了。修每一個拼裝點——把 <code>'tmp'</code> 改成合法長度、把臨時物件改成取自己的欄位——只消掉當次症狀，下一個測試還是會在同一個表達力缺口前面選擇拼裝。門檻是兩次：第一次拼裝可以當個案修掉——只出現一次的特殊組合用拼裝是合理的、工廠也有參數列膨脹的反向代價。第二次出現就該停止修拼裝點、轉頭找共同面對的表達力缺口。工廠產物的 copyWith 呼叫在覆寫哪些欄位、就是一份「工廠該收而沒收的參數」的實證清單——繞道流量本身是免費的需求調查（<a href="/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223 逃生口吸收建構路徑的缺陷</a>）。</p>
<h2 id="原始值的官方出口">原始值的官方出口</h2>
<p>建構路徑的另一面是出口：value object 封裝了原始值之後、基礎設施層（快取 key、資料庫 column、序列化）要怎麼拿到它需要的表示。穩態是封裝任意操作、而非封裝取值本身——基礎設施邊界的需求是正當的、該給語意明確的出口。這個穩態從擺盪中浮出。</p>
<p>同一個書籍管理 App 的 value object 封裝在三個版本間擺盪。v0.7.6 全移除（裸字串），不變式失去強制點、任何字串冒充任何 ID。隨後 v0.8.10 推向另一極——完全封裝、目標零個 <code>.value</code> 外部存取——基礎設施層的 176 個編譯錯誤暴露這些消費是正當需求，把它們全逼到 <code>toString()</code> 上是語意寄生、格式一改快取 key 靜默換一批。最終 v0.8.13 加回 <code>.value</code> getter、被重新命名成「相容性介面」，實質是理想在依賴現實前退讓（<a href="/blog/work-log/flutter_value_object_encapsulation_oscillation/" data-link-title="Value Object 的封裝擺盪：從全移除、完全封裝、到加回 .value getter" data-link-desc="VO 的封裝邊界在兩個極端之間來回——純字串（零封裝）跟完全封裝（禁止取原始值）各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口，而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。">VO 的封裝擺盪</a>）。</p>
<p>穩態的操作化：出口的名字說明用途：<code>toJsonString()</code>（序列化）、<code>toDbValue()</code>（持久化）、<code>displayValue</code>（UI）。「誰在拆封」就變成可 grep 的——一個 POS 專案的 Money 型別用 <code>toDecimal()</code> 作為官方拆封口、搜尋 <code>toDecimal(</code> 就是拆封清單。沒有官方出口的世界裡、下游用 <code>toString()</code> 硬接或把 <code>.value</code> getter 加回來、拆封處完全不可追蹤。</p>
<p>擺盪的根治不在選對某一極、在於把邊界寫成決策記錄：哪些出口存在、各自給誰用、為什麼不多不少。出口增長到大多數消費者都有專屬方法時、封裝的值已不成立——此時直接暴露一個語意明確的拆封方法，比維護多個用途近似的出口便宜。沒有這份記錄、每一任重構者都會從自己撞到的那一面出發、再推向另一個極端。判讀徵兆：重構記錄出現「相容性介面」——檢查它是不是理想撤退的重新命名；決策記錄只記贏面——反方向的代價沒被記、下次擺回去的推力還在；封裝重構在基礎設施層爆量——訊號是「這些消費是正當的」、該給出口不是硬改。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>測試 Arrange 段出現「工廠 + copyWith 拼裝」——盤點拼裝在補什麼欄位、高頻欄位收進工廠參數列。工廠參數列長期不變、而它的產物被 copyWith 環繞——表達力已落後需求，原則見 <a href="/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223</a>。</li>
<li>建一個立刻丟棄的物件、只為了拿它的一個欄位？這是語意錯誤的標記，先找它真正想表達的需求。</li>
<li>同族語意錯誤第二次出現——停止修個案、找兩個案共同面對的建構路徑缺口。</li>
<li><code>toString()</code> 被當成取值 API 用在快取 key / DB 值——語意寄生、格式一改靜默事故。出口要有語意明確的名字，語意封閉判準見 <a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a>。</li>
<li>VO 封裝重構的編譯錯誤在基礎設施層爆量——基礎設施是正當消費者、該給出口。</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>物件組好之後、整個系統怎麼被組起來：<a href="/blog/ddd/composition-root-reachability/" data-link-title="組裝層的可達性" data-link-desc="行為測試全綠、功能在實機上沒有入口的失效形態出現時使用。mock 換掉的正是組裝，組裝完成與否在行為測試裡沒有證言；把可達性當成組裝層的不變式，在測試、發版與設計文件各給一個強制點。">組裝層的可達性</a></li>
<li>出生合法的機制：<a href="/blog/ddd/invariant-enforcement-layers/" data-link-title="不變式的強制層次" data-link-desc="業務約束落在文件層、型別層、執行層的差異與代價：違反時是靜默、編譯失敗還是當場拒絕。含不變式被撞時「需求違規 vs 約束錯形」的分辨、存在條件與輸入品質的分層邊界。">不變式的強制層次</a></li>
<li>變更路徑收斂：<a href="/blog/ddd/state-transition-and-audit-trail/" data-link-title="狀態轉換與稽核軌跡" data-link-desc="領域方法作為唯一變更路徑：判準是「變更有沒有需要一起完成的伴隨動作」。含唯一路徑與建議路徑的分界、稽核軌跡出洞的靜默機制與凍結作為稽核端點。">狀態轉換與稽核軌跡</a></li>
<li>型別類別的入口判準：<a href="/blog/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a></li>
<li>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></li>
<li>Dart 的語言細節（copyWith 三態缺口、工廠表達力擴充、extension type 的拆封口）：<a href="/blog/work-log/dart_copywith_entity_escape_hatch/" data-link-title="copyWith 是逃生口，不是設計 — 從一個測試 bug 追到 entity 稽核軌跡的洞" data-link-desc="copyWith 對純資料載體是正確工具，對有領域方法的 entity 是繞過不變式的逃生口。從一個 3 字元 ID 觸發的例外，追出同族語意錯誤、被繞過的領域方法、以及從未被強制的註解約束。">copyWith 是逃生口，不是設計</a>、<a href="/blog/work-log/flutter_value_object_encapsulation_oscillation/" data-link-title="Value Object 的封裝擺盪：從全移除、完全封裝、到加回 .value getter" data-link-desc="VO 的封裝邊界在兩個極端之間來回——純字串（零封裝）跟完全封裝（禁止取原始值）各有成立的理由、也各自撞牆。穩態是給原始值一個有語意的官方出口，而不是把「取原始值」本身當違規。含 176 個編譯錯誤的工作量低估、以及「相容性介面」作為理想撤退訊號的判讀。">VO 的封裝擺盪</a></li>
<li>原則層：<a href="/blog/report/escape-hatch-absorbs-construction-gap/" data-link-title="逃生口吸收建構路徑的缺陷：修工廠的表達力、不是修拼裝點" data-link-desc="同族語意錯誤重複出現、或測試 Arrange 段大量用萬能拼裝工具建物件時使用。全欄位 copyWith 這類逃生口總有辦法把物件拼出來，於是建構路徑的表達力缺陷永遠不被迫修好——需求被逃生口吸收、以語意錯誤的形式在別處復發。修上游的表達力、不是修每一個拼裝點。">#223 逃生口吸收建構路徑的缺陷</a></li>
</ul>
]]></content:encoded></item></channel></rss>