<?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/flutter/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/flutter/knowledge-cards/index.xml" rel="self" type="application/rss+xml"/><item><title>copyWith</title><link>https://tarrragon.github.io/blog/flutter/knowledge-cards/copywith/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/flutter/knowledge-cards/copywith/</guid><description>&lt;p>copyWith 是 Dart 生態中逐欄位覆寫物件的慣用方法：呼叫時只傳要改的欄位、其餘保留原值、回傳一個新實例。&lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed&lt;/a> 自動為每個 model 生成 copyWith，IDE 補全第一個跳出來的也是它——它是 Dart 的預設路徑。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>copyWith 對&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>（DTO、API model、UI state）是正確工具——欄位組合全部合法、逐欄位覆寫語意清晰。但對有領域方法的 &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>、copyWith 是繞過&lt;a href="https://tarrragon.github.io/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式&lt;/a>的逃生口：領域方法從「唯一路徑」降級成「建議路徑」、稽核軌跡開始出洞。判準是型別有沒有「不允許任意組合的欄位」——有，copyWith 就不該讓那些欄位 public 可寫。多數專案的 copyWith 由 &lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed&lt;/a> 生成、不是手寫——這也是它對所有型別一視同仁套用的原因。&lt;/p>
&lt;h2 id="nullable-欄位的三態缺口">Nullable 欄位的三態缺口&lt;/h2>
&lt;p>copyWith 在 nullable 欄位上有一個 Dart 型別系統的缺口：&lt;code>String? isbn&lt;/code> 只有兩態（有值 / null），而 copyWith 需要三態——「不改這欄」「改成某值」「清空成 null」。前兩態沒問題，第三態表達不出來。通用的補償手法是哨兵物件（sentinel），freezed 生成的 copyWith 內部就是用同樣的技巧。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>收窄的方向分三層：value object / DTO / UI state 保留 copyWith；有領域方法的 entity 把 copyWith 改 private 或從參數列移除受約束欄位；測試建構需求不足時修工廠的表達力、不修每一個拼裝點。完整機制見 &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/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>copyWith 是 Dart 生態中逐欄位覆寫物件的慣用方法：呼叫時只傳要改的欄位、其餘保留原值、回傳一個新實例。<a href="/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed</a> 自動為每個 model 生成 copyWith，IDE 補全第一個跳出來的也是它——它是 Dart 的預設路徑。</p>
<h2 id="概念位置">概念位置</h2>
<p>copyWith 對<a href="/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋</a>（DTO、API model、UI state）是正確工具——欄位組合全部合法、逐欄位覆寫語意清晰。但對有領域方法的 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a>、copyWith 是繞過<a href="/blog/ddd/knowledge-cards/invariant/" data-link-title="Invariant" data-link-desc="領域模型的約束規則落在哪一層時使用。不變式是在物件整個生命週期都必須為真的業務規則——狀態只能沿流程轉換、被同一條規則綁住的欄位必須一起換。">不變式</a>的逃生口：領域方法從「唯一路徑」降級成「建議路徑」、稽核軌跡開始出洞。判準是型別有沒有「不允許任意組合的欄位」——有，copyWith 就不該讓那些欄位 public 可寫。多數專案的 copyWith 由 <a href="/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed</a> 生成、不是手寫——這也是它對所有型別一視同仁套用的原因。</p>
<h2 id="nullable-欄位的三態缺口">Nullable 欄位的三態缺口</h2>
<p>copyWith 在 nullable 欄位上有一個 Dart 型別系統的缺口：<code>String? isbn</code> 只有兩態（有值 / null），而 copyWith 需要三態——「不改這欄」「改成某值」「清空成 null」。前兩態沒問題，第三態表達不出來。通用的補償手法是哨兵物件（sentinel），freezed 生成的 copyWith 內部就是用同樣的技巧。</p>
<h2 id="設計責任">設計責任</h2>
<p>收窄的方向分三層：value object / DTO / UI state 保留 copyWith；有領域方法的 entity 把 copyWith 改 private 或從參數列移除受約束欄位；測試建構需求不足時修工廠的表達力、不修每一個拼裝點。完整機制見 <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/ddd/data-bag-vs-domain-model/" data-link-title="資料袋與領域模型" data-link-desc="判斷一個型別該是一袋欄位還是有行為的領域模型：判準是「有沒有不允許任意組合的欄位」。含判準用錯時規則退化成建議的機制、以及資料袋起步後升級的演化訊號。">資料袋與領域模型</a>。</p>
]]></content:encoded></item><item><title>freezed</title><link>https://tarrragon.github.io/blog/flutter/knowledge-cards/freezed/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/flutter/knowledge-cards/freezed/</guid><description>&lt;p>freezed 是 Dart 生態中最廣泛使用的 immutable data class 程式碼生成器。標記 &lt;code>@freezed&lt;/code> 後，它自動產生 &lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith&lt;/a>、&lt;code>==&lt;/code> / &lt;code>hashCode&lt;/code>、&lt;code>toString()&lt;/code>、以及 sealed union（多態分支的 exhaustive switch）。它把「寫一個 immutable class 需要的 boilerplate」壓到接近零。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>freezed 的預設路徑對所有型別一視同仁——它不區分&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;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;code>@freezed&lt;/code> 標記的 class 都會得到全欄位的 public &lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith&lt;/a>、包含狀態欄位與稽核欄位。這是生態推力：規範說「請走領域方法」、工具預設給全欄位 copyWith——規範和預設衝突時、預設會贏。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>在 entity 上使用 freezed 時、需要額外措施收窄 copyWith 的開放程度：把 copyWith 改 private、或從參數列移除受約束的欄位。freezed 的 sealed union 在枚舉分層上有正面價值——exhaustive switch 讓「忘記決定新成員歸哪類」在編譯期就走不通。結構細節見 &lt;a href="https://tarrragon.github.io/blog/work-log/dart_freezed_anatomy/" data-link-title="Freezed 的三層結構解剖：with、_$、以及更好懂的替代路徑" data-link-desc="freezed `class X with _$X implements Y` 的分層結構解剖：`with` 與 `_$` 各自的角色、沒有 freezed 怎麼手做、中間投影物件 vs DTO 直接 implements 的維護取捨。">Freezed 三層結構解剖&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>freezed 是 Dart 生態中最廣泛使用的 immutable data class 程式碼生成器。標記 <code>@freezed</code> 後，它自動產生 <a href="/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith</a>、<code>==</code> / <code>hashCode</code>、<code>toString()</code>、以及 sealed union（多態分支的 exhaustive switch）。它把「寫一個 immutable class 需要的 boilerplate」壓到接近零。</p>
<h2 id="概念位置">概念位置</h2>
<p>freezed 的預設路徑對所有型別一視同仁——它不區分<a href="/blog/ddd/knowledge-cards/data-bag/" data-link-title="Data Bag" data-link-desc="判斷一個型別要不要投資領域模型設計時使用。資料袋是欄位組合全部合法、沒有不變式要守的型別——DTO、API model、UI state 都屬於這一類。">資料袋</a>和有領域方法的 <a href="/blog/ddd/knowledge-cards/entity/" data-link-title="Entity" data-link-desc="判斷一個概念該建成 entity 還是 value object 時使用。entity 的同一性由身份定義——欄位全部改變、只要身份參照不變就是同一個。">entity</a>。每個被 <code>@freezed</code> 標記的 class 都會得到全欄位的 public <a href="/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith</a>、包含狀態欄位與稽核欄位。這是生態推力：規範說「請走領域方法」、工具預設給全欄位 copyWith——規範和預設衝突時、預設會贏。</p>
<h2 id="設計責任">設計責任</h2>
<p>在 entity 上使用 freezed 時、需要額外措施收窄 copyWith 的開放程度：把 copyWith 改 private、或從參數列移除受約束的欄位。freezed 的 sealed union 在枚舉分層上有正面價值——exhaustive switch 讓「忘記決定新成員歸哪類」在編譯期就走不通。結構細節見 <a href="/blog/work-log/dart_freezed_anatomy/" data-link-title="Freezed 的三層結構解剖：with、_$、以及更好懂的替代路徑" data-link-desc="freezed `class X with _$X implements Y` 的分層結構解剖：`with` 與 `_$` 各自的角色、沒有 freezed 怎麼手做、中間投影物件 vs DTO 直接 implements 的維護取捨。">Freezed 三層結構解剖</a>。</p>
]]></content:encoded></item><item><title>Extension Type</title><link>https://tarrragon.github.io/blog/flutter/knowledge-cards/extension-type/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/flutter/knowledge-cards/extension-type/</guid><description>&lt;p>Extension type 是 Dart 3 引入的語言特性：在編譯期把一個型別包裝成另一個名字、限縮可用的 API、而 runtime 不存在額外物件（零成本抽象）。它跟 class 的差異在「沒有 runtime 開銷、但也沒有 runtime 的型別檢查——&lt;code>is&lt;/code> 跟 &lt;code>as&lt;/code> 看到的是底層型別」。同樣是「限縮型別可用介面」的目標，&lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed&lt;/a> 生成的 class 走 runtime 路徑、extension type 走編譯期路徑，兩者是互斥的實作選擇。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Extension type 是實作 &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> 語意封閉的一種載體。金額用 extension type 包 Decimal：開放的運算限於領域有意義的集合（加減、乘數量、乘倍率），差集裡的誤用（金額乘金額）在編譯期就走不通。跟用 class 包裝的差異：class 有 runtime 身份（&lt;code>is Money&lt;/code> 為 true）但有 overhead；extension type 零 overhead 但 runtime 是透明的。這條分野也決定了誰適合搭 &lt;a href="https://tarrragon.github.io/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith&lt;/a>——extension type 沒有 runtime 物件可覆寫欄位，逐欄位覆寫語意只在 class 路徑上成立。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Extension type 的 subtype 決策要在設計時做好——它是底層型別的 subtype（寬鬆：可以隱式 upcast 回底層型別）還是獨立型別（嚴格：只能顯式拆封）。前者方便、但封裝邊界弱；後者安全、但每個需要底層型別的銜接點都要顯式拆封。顯式拆封的出口要有語意明確的名字（如 &lt;code>toDecimal()&lt;/code>），見 &lt;a href="https://tarrragon.github.io/blog/ddd/construction-path-design/" data-link-title="建構路徑設計" data-link-desc="工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。">建構路徑設計&lt;/a>。Migration 案例見 &lt;a href="https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Extension type 是 Dart 3 引入的語言特性：在編譯期把一個型別包裝成另一個名字、限縮可用的 API、而 runtime 不存在額外物件（零成本抽象）。它跟 class 的差異在「沒有 runtime 開銷、但也沒有 runtime 的型別檢查——<code>is</code> 跟 <code>as</code> 看到的是底層型別」。同樣是「限縮型別可用介面」的目標，<a href="/blog/flutter/knowledge-cards/freezed/" data-link-title="freezed" data-link-desc="Dart 的 immutable data class 程式碼生成器。freezed 自動產生 copyWith、equals、toString、sealed union——它是 Dart 生態把 copyWith 推成預設路徑的主要推力。">freezed</a> 生成的 class 走 runtime 路徑、extension type 走編譯期路徑，兩者是互斥的實作選擇。</p>
<h2 id="概念位置">概念位置</h2>
<p>Extension type 是實作 <a href="/blog/ddd/knowledge-cards/value-object/" data-link-title="Value Object" data-link-desc="判斷一個概念該用內容比對還是身份追蹤時使用。value object 的同一性由內容定義——內容相等就是同一個、替換實例對系統沒有影響。">value object</a> 語意封閉的一種載體。金額用 extension type 包 Decimal：開放的運算限於領域有意義的集合（加減、乘數量、乘倍率），差集裡的誤用（金額乘金額）在編譯期就走不通。跟用 class 包裝的差異：class 有 runtime 身份（<code>is Money</code> 為 true）但有 overhead；extension type 零 overhead 但 runtime 是透明的。這條分野也決定了誰適合搭 <a href="/blog/flutter/knowledge-cards/copywith/" data-link-title="copyWith" data-link-desc="物件的逐欄位覆寫方法在什麼時候是正確工具、什麼時候是逃生口時使用。copyWith 對資料袋語意清晰、對有領域方法的 entity 是繞過不變式的逃生口。">copyWith</a>——extension type 沒有 runtime 物件可覆寫欄位，逐欄位覆寫語意只在 class 路徑上成立。</p>
<h2 id="設計責任">設計責任</h2>
<p>Extension type 的 subtype 決策要在設計時做好——它是底層型別的 subtype（寬鬆：可以隱式 upcast 回底層型別）還是獨立型別（嚴格：只能顯式拆封）。前者方便、但封裝邊界弱；後者安全、但每個需要底層型別的銜接點都要顯式拆封。顯式拆封的出口要有語意明確的名字（如 <code>toDecimal()</code>），見 <a href="/blog/ddd/construction-path-design/" data-link-title="建構路徑設計" data-link-desc="工廠表達力不足時缺陷如何被逃生口吸收——逃生口讓正確的修法變不必要、以語意錯誤在下游復發。含原始值官方出口的穩態邊界、封裝擺盪的判讀。">建構路徑設計</a>。Migration 案例見 <a href="/blog/work-log/dart_money_extension_type_migration/" data-link-title="金額型別的三段遷移：double、Decimal、再到 Money extension type" data-link-desc="金額欄位從 double 換 Decimal 只解決精度、沒解決「任何人都能對它做無意義運算」；用 Dart extension type 包成 Money 之後，型別系統只開放領域有意義的運算。含 implements Object 的 subtype 設計、以及大規模型別遷移前先寫 characterization test 鎖行為的做法。">Money 三段遷移</a>。</p>
]]></content:encoded></item></channel></rss>