<?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>Decimal on Tarragon</title><link>https://tarrragon.github.io/blog/tags/decimal/</link><description>Recent content in Decimal 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/decimal/index.xml" rel="self" type="application/rss+xml"/><item><title>金額型別的三段遷移：double、Decimal、再到 Money extension type</title><link>https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/dart_money_extension_type_migration/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：整理 POS 專案的金額處理時，發現 git 歷史上金額型別換過兩次——&lt;code>double&lt;/code> 到 &lt;code>Decimal&lt;/code>、再從 &lt;code>Decimal&lt;/code> 到自訂的 &lt;code>Money&lt;/code>。第二次遷移乍看多餘：精度問題 &lt;code>Decimal&lt;/code> 已經解掉了
&lt;strong>疑問來源&lt;/strong>：&lt;code>Decimal&lt;/code> 哪裡不夠？第二次遷移買到的是什麼？
&lt;strong>整理目的&lt;/strong>：記下「精度」跟「語意」是金額型別的兩個獨立問題、以及 Dart extension type 在第二個問題上的實作手法
&lt;strong>本文邊界&lt;/strong>：以該專案的實際遷移軌跡為素材；extension type 是 Dart 3 的機制、其他語言的對應手法（newtype / value class）思路相同但細節不同&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="第一段double-的精度問題">第一段：double 的精度問題&lt;/h2>
&lt;p>最初所有金額欄位是 &lt;code>double&lt;/code>，JSON 轉換器把後端的 number 或 string 統一存成 &lt;code>double&lt;/code>。浮點數處理金額的問題是經典的：&lt;code>0.1 + 0.2 != 0.3&lt;/code>，累加訂單明細時誤差會累積到分位。&lt;/p>
&lt;p>第一次遷移（單一 PR 內兩個 commit）把所有 model 的金額欄位換成 &lt;code>Decimal&lt;/code>，API 接入層用 &lt;code>jsonToDecimal&lt;/code> 統一處理後端可能回 number 或 string 的格式差異。精度問題到此解決。&lt;/p>
&lt;h2 id="第二段decimal-解決了精度沒解決語意">第二段：Decimal 解決了精度、沒解決語意&lt;/h2>
&lt;p>換完 &lt;code>Decimal&lt;/code> 之後，金額仍然是一個&lt;strong>裸的通用數字型別&lt;/strong>。任何拿到 &lt;code>Decimal&lt;/code> 的程式碼都能對它做任意運算：兩個金額相乘（語意上不存在的運算）、金額跟折扣率直接相加、拿金額當數量用。型別系統對這些錯誤全部放行，因為它們在 &lt;code>Decimal&lt;/code> 的世界都是合法運算。&lt;/p>
&lt;p>這是 primitive obsession 的標準形態：值的表示對了、值的&lt;strong>語意邊界&lt;/strong>還是沒有。三個月後的第二次遷移把金額包進 &lt;code>Money&lt;/code>：&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="n">extension&lt;/span> &lt;span class="n">type&lt;/span> &lt;span class="kd">const&lt;/span> &lt;span class="n">Money&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Decimal&lt;/span> &lt;span class="n">_raw&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kd">implements&lt;/span> &lt;span class="kt">Object&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">Money&lt;/span> &lt;span class="kd">operator&lt;/span> &lt;span class="o">+&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Money&lt;/span> &lt;span class="n">other&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">Money&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_raw&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">other&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_raw&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">Money&lt;/span> &lt;span class="kd">operator&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Money&lt;/span> &lt;span class="n">other&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">Money&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">_raw&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="n">other&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_raw&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">Money&lt;/span> &lt;span class="kd">operator&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">Money&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">_&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">_raw&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">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="kd">operator&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&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">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="n">multiplyByRate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Decimal&lt;/span> &lt;span class="n">rate&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&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">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="n">clamp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Money&lt;/span> &lt;span class="n">min&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Money&lt;/span> &lt;span class="n">max&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="p">...;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>運算列表本身就是領域規則的宣告：金額加金額可以、金額乘整數（數量）可以、金額乘倍率（&lt;code>Decimal&lt;/code>，刻意跟數量分開簽名）可以——&lt;strong>金額乘金額不存在&lt;/strong>，因為介面沒開放。想對 &lt;code>Money&lt;/code> 做 &lt;code>Decimal&lt;/code> 的任意運算，得先顯式呼叫 &lt;code>toDecimal()&lt;/code> 拆封，那一行拆封程式碼就是 code review 的攔截點。&lt;/p>
&lt;h2 id="implements-object-的取捨要當-object不當-decimal">implements Object 的取捨：要當 Object、不當 Decimal&lt;/h2>
&lt;p>extension type 宣告 &lt;code>implements Object&lt;/code> 而只有這個，是一個精確的 subtype 決策：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>是 &lt;code>Object&lt;/code> 的 subtype&lt;/strong>：既有的格式化入口 &lt;code>formatAmount(Object)&lt;/code> 可以直接吃 &lt;code>Money&lt;/code>、不用改簽名&lt;/li>
&lt;li>&lt;strong>不是 &lt;code>Decimal&lt;/code> 的 subtype&lt;/strong>：如果宣告 &lt;code>implements Decimal&lt;/code>，&lt;code>Money&lt;/code> 就能被傳進任何收 &lt;code>Decimal&lt;/code> 的參數、所有裸運算又回來了——包裝等於白做&lt;/li>
&lt;/ul>
&lt;p>extension type 在 runtime 是零開銷的（編譯後就是底層的 &lt;code>Decimal&lt;/code>），所有約束都活在編譯期。這也意味著它的保護是編譯期的：反射或 dynamic 繞得過去，威脅模型是「防止無心的誤用」而不是「防止刻意拆封」。&lt;/p>
&lt;h2 id="遷移安全網characterization-test-先鎖行為">遷移安全網：characterization test 先鎖行為&lt;/h2>
&lt;p>第二次遷移動的是全專案的金額欄位，怎麼確認換型別沒改行為？這個專案在遷移前先寫了一批 characterization test，測試檔開頭直接註明用途：&lt;/p>
&lt;blockquote>
&lt;p>Characterization test —— 鎖住 CheckoutContext 結帳金額計算的現有行為。在 Money value object 遷移（階段 5）前建立。涵蓋應付金額 fold、現金找零（含負數歸零分支）、金額足夠判斷。&lt;/p>&lt;/blockquote>
&lt;p>characterization test 跟一般測試的差別在斷言的性質：它不驗證「行為正確」、驗證「行為不變」。遷移前對著舊實作寫、鎖住當前輸出（包含當前的邊界行為，例如找零算出負數時歸零），遷移後全綠就證明型別替換沒有帶入行為變化。正確性是另一個問題、留給另一批測試——把兩個問題混在同一批測試裡，遷移期間的紅燈就分不清是「換壞了」還是「本來就錯」。&lt;/p>
&lt;h2 id="收束兩個問題兩次遷移">收束：兩個問題、兩次遷移&lt;/h2>
&lt;p>金額型別有兩個獨立的問題，這個專案的軌跡恰好一段解一個：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題&lt;/th>
 &lt;th>症狀&lt;/th>
 &lt;th>解法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>精度&lt;/td>
 &lt;td>浮點誤差累積到分位&lt;/td>
 &lt;td>&lt;code>double&lt;/code> 換 &lt;code>Decimal&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語意&lt;/td>
 &lt;td>任何人都能對金額做任意運算&lt;/td>
 &lt;td>&lt;code>Decimal&lt;/code> 包成 &lt;code>Money&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一段遷移完成時「金額用 Decimal」看起來已經是終點，語意問題要等到夠多「拿金額亂算」的路徑存在後才顯形。判讀訊號是：&lt;strong>一個領域概念的合法運算集合、明顯小於它底層型別的運算集合&lt;/strong>時，包一層 domain type 的價值就成立——差集裡的每個運算都是一個等著被誤用的 API。&lt;/p>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>概念地基：&lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a> 的語意封閉段（本文是 primitive obsession 到 domain type 的實機案例）&lt;/li>
&lt;li>同族判準：&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;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：整理 POS 專案的金額處理時，發現 git 歷史上金額型別換過兩次——<code>double</code> 到 <code>Decimal</code>、再從 <code>Decimal</code> 到自訂的 <code>Money</code>。第二次遷移乍看多餘：精度問題 <code>Decimal</code> 已經解掉了
<strong>疑問來源</strong>：<code>Decimal</code> 哪裡不夠？第二次遷移買到的是什麼？
<strong>整理目的</strong>：記下「精度」跟「語意」是金額型別的兩個獨立問題、以及 Dart extension type 在第二個問題上的實作手法
<strong>本文邊界</strong>：以該專案的實際遷移軌跡為素材；extension type 是 Dart 3 的機制、其他語言的對應手法（newtype / value class）思路相同但細節不同</p></blockquote>
<hr>
<h2 id="第一段double-的精度問題">第一段：double 的精度問題</h2>
<p>最初所有金額欄位是 <code>double</code>，JSON 轉換器把後端的 number 或 string 統一存成 <code>double</code>。浮點數處理金額的問題是經典的：<code>0.1 + 0.2 != 0.3</code>，累加訂單明細時誤差會累積到分位。</p>
<p>第一次遷移（單一 PR 內兩個 commit）把所有 model 的金額欄位換成 <code>Decimal</code>，API 接入層用 <code>jsonToDecimal</code> 統一處理後端可能回 number 或 string 的格式差異。精度問題到此解決。</p>
<h2 id="第二段decimal-解決了精度沒解決語意">第二段：Decimal 解決了精度、沒解決語意</h2>
<p>換完 <code>Decimal</code> 之後，金額仍然是一個<strong>裸的通用數字型別</strong>。任何拿到 <code>Decimal</code> 的程式碼都能對它做任意運算：兩個金額相乘（語意上不存在的運算）、金額跟折扣率直接相加、拿金額當數量用。型別系統對這些錯誤全部放行，因為它們在 <code>Decimal</code> 的世界都是合法運算。</p>
<p>這是 primitive obsession 的標準形態：值的表示對了、值的<strong>語意邊界</strong>還是沒有。三個月後的第二次遷移把金額包進 <code>Money</code>：</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="n">extension</span> <span class="n">type</span> <span class="kd">const</span> <span class="n">Money</span><span class="p">.</span><span class="n">_</span><span class="p">(</span><span class="n">Decimal</span> <span class="n">_raw</span><span class="p">)</span> <span class="kd">implements</span> <span class="kt">Object</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">Money</span> <span class="kd">operator</span> <span class="o">+</span><span class="p">(</span><span class="n">Money</span> <span class="n">other</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">Money</span><span class="p">.</span><span class="n">_</span><span class="p">(</span><span class="n">_raw</span> <span class="o">+</span> <span class="n">other</span><span class="p">.</span><span class="n">_raw</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">Money</span> <span class="kd">operator</span> <span class="o">-</span><span class="p">(</span><span class="n">Money</span> <span class="n">other</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">Money</span><span class="p">.</span><span class="n">_</span><span class="p">(</span><span class="n">_raw</span> <span class="o">-</span> <span class="n">other</span><span class="p">.</span><span class="n">_raw</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">Money</span> <span class="kd">operator</span> <span class="o">-</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">Money</span><span class="p">.</span><span class="n">_</span><span class="p">(</span><span class="o">-</span><span class="n">_raw</span><span class="p">);</span>              <span class="c1">// 退款 / 折讓
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="n">Money</span> <span class="kd">operator</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">...;</span>             <span class="c1">// 金額 × 數量
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="n">Money</span> <span class="n">multiplyByRate</span><span class="p">(</span><span class="n">Decimal</span> <span class="n">rate</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">...;</span>         <span class="c1">// 會員價率、服務費率
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="n">Money</span> <span class="n">clamp</span><span class="p">(</span><span class="n">Money</span> <span class="n">min</span><span class="p">,</span> <span class="n">Money</span> <span class="n">max</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">...</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>運算列表本身就是領域規則的宣告：金額加金額可以、金額乘整數（數量）可以、金額乘倍率（<code>Decimal</code>，刻意跟數量分開簽名）可以——<strong>金額乘金額不存在</strong>，因為介面沒開放。想對 <code>Money</code> 做 <code>Decimal</code> 的任意運算，得先顯式呼叫 <code>toDecimal()</code> 拆封，那一行拆封程式碼就是 code review 的攔截點。</p>
<h2 id="implements-object-的取捨要當-object不當-decimal">implements Object 的取捨：要當 Object、不當 Decimal</h2>
<p>extension type 宣告 <code>implements Object</code> 而只有這個，是一個精確的 subtype 決策：</p>
<ul>
<li><strong>是 <code>Object</code> 的 subtype</strong>：既有的格式化入口 <code>formatAmount(Object)</code> 可以直接吃 <code>Money</code>、不用改簽名</li>
<li><strong>不是 <code>Decimal</code> 的 subtype</strong>：如果宣告 <code>implements Decimal</code>，<code>Money</code> 就能被傳進任何收 <code>Decimal</code> 的參數、所有裸運算又回來了——包裝等於白做</li>
</ul>
<p>extension type 在 runtime 是零開銷的（編譯後就是底層的 <code>Decimal</code>），所有約束都活在編譯期。這也意味著它的保護是編譯期的：反射或 dynamic 繞得過去，威脅模型是「防止無心的誤用」而不是「防止刻意拆封」。</p>
<h2 id="遷移安全網characterization-test-先鎖行為">遷移安全網：characterization test 先鎖行為</h2>
<p>第二次遷移動的是全專案的金額欄位，怎麼確認換型別沒改行為？這個專案在遷移前先寫了一批 characterization test，測試檔開頭直接註明用途：</p>
<blockquote>
<p>Characterization test —— 鎖住 CheckoutContext 結帳金額計算的現有行為。在 Money value object 遷移（階段 5）前建立。涵蓋應付金額 fold、現金找零（含負數歸零分支）、金額足夠判斷。</p></blockquote>
<p>characterization test 跟一般測試的差別在斷言的性質：它不驗證「行為正確」、驗證「行為不變」。遷移前對著舊實作寫、鎖住當前輸出（包含當前的邊界行為，例如找零算出負數時歸零），遷移後全綠就證明型別替換沒有帶入行為變化。正確性是另一個問題、留給另一批測試——把兩個問題混在同一批測試裡，遷移期間的紅燈就分不清是「換壞了」還是「本來就錯」。</p>
<h2 id="收束兩個問題兩次遷移">收束：兩個問題、兩次遷移</h2>
<p>金額型別有兩個獨立的問題，這個專案的軌跡恰好一段解一個：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>症狀</th>
          <th>解法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>精度</td>
          <td>浮點誤差累積到分位</td>
          <td><code>double</code> 換 <code>Decimal</code></td>
      </tr>
      <tr>
          <td>語意</td>
          <td>任何人都能對金額做任意運算</td>
          <td><code>Decimal</code> 包成 <code>Money</code></td>
      </tr>
  </tbody>
</table>
<p>第一段遷移完成時「金額用 Decimal」看起來已經是終點，語意問題要等到夠多「拿金額亂算」的路徑存在後才顯形。判讀訊號是：<strong>一個領域概念的合法運算集合、明顯小於它底層型別的運算集合</strong>時，包一層 domain type 的價值就成立——差集裡的每個運算都是一個等著被誤用的 API。</p>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>概念地基：<a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a> 的語意封閉段（本文是 primitive obsession 到 domain type 的實機案例）</li>
<li>同族判準：<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>——那篇的判準「有沒有不允許任意組合的欄位」在型別層的對應是「有沒有不允許的運算」，兩篇都是把約束從慣例層上移到型別層</li>
</ul>
]]></content:encoded></item></channel></rss>