<?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>Code-Generation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/code-generation/</link><description>Recent content in Code-Generation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 11 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/code-generation/index.xml" rel="self" type="application/rss+xml"/><item><title>Freezed 的三層結構解剖：with、_$、以及更好懂的替代路徑</title><link>https://tarrragon.github.io/blog/work-log/freezed-%E7%9A%84%E4%B8%89%E5%B1%A4%E7%B5%90%E6%A7%8B%E8%A7%A3%E5%89%96with_%E4%BB%A5%E5%8F%8A%E6%9B%B4%E5%A5%BD%E6%87%82%E7%9A%84%E6%9B%BF%E4%BB%A3%E8%B7%AF%E5%BE%91/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/freezed-%E7%9A%84%E4%B8%89%E5%B1%A4%E7%B5%90%E6%A7%8B%E8%A7%A3%E5%89%96with_%E4%BB%A5%E5%8F%8A%E6%9B%B4%E5%A5%BD%E6%87%82%E7%9A%84%E6%9B%BF%E4%BB%A3%E8%B7%AF%E5%BE%91/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：實作營運端報表 API、寫了一個 freezed model
&lt;strong>疑問來源&lt;/strong>：&lt;code>abstract class PeriodReportRow with _$PeriodReportRow implements ReportAmountsView&lt;/code> 這一行包含太多陌生語法
&lt;strong>整理目的&lt;/strong>：把「為什麼長這樣」與「是否有更好懂做法」的脈絡記錄下來、避免下次又從零開始查
&lt;strong>本文邊界&lt;/strong>：這是一篇 work-log，目標是回溯一次具體實作中的理解成本；它不取代 freezed 官方文件，也不把某個專案的模型分層當成通用規則。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="事件起點">事件起點&lt;/h2>
&lt;p>今天在某個營運端 Flutter 專案新增週期彙總報表 API，這份報表和既有的單次作業報表共用呈現邏輯、各自有獨立的 DTO。為了讓兩個 DTO 共用 sections builder、抽了一個 &lt;code>ReportAmountsView&lt;/code> 介面、讓兩邊的 &lt;code>*Row&lt;/code> 都 &lt;code>implements&lt;/code> 它。&lt;/p>
&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="err">@&lt;/span>&lt;span class="n">freezed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">abstract&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">PeriodReportRow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">with&lt;/span> &lt;span class="n">_$PeriodReportRow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kd">implements&lt;/span> &lt;span class="n">ReportAmountsView&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="kd">const&lt;/span> &lt;span class="kd">factory&lt;/span> &lt;span class="n">PeriodReportRow&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kd">required&lt;/span> &lt;span class="kt">String&lt;/span> &lt;span class="n">date&lt;/span>&lt;span class="p">,&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">// ... 18 個欄位
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_PeriodReportRow&lt;/span>&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>abstract&lt;/code> 為什麼能配 &lt;code>factory&lt;/code>、&lt;code>with _$PeriodReportRow&lt;/code> 在做什麼、&lt;code>_$&lt;/code> 這個前綴代表什麼、&lt;code>= _PeriodReportRow&lt;/code> 如何接到生成類，以及為什麼要分成「我寫的 abstract」+「生成的 mixin」+「生成的具體類」三層。&lt;/p>
&lt;p>這篇筆記把那次停下來查證的路徑整理成可重讀的判斷脈絡。&lt;/p>
&lt;hr>
&lt;h2 id="第一層with-是什麼">第一層：&lt;code>with&lt;/code> 是什麼&lt;/h2>
&lt;p>&lt;code>with&lt;/code> 是 Dart 的 &lt;strong>mixin 語法&lt;/strong>、把另一個型別的成員「混入」當前 class。當前 class 會接上 mixin 提供的成員；如果 mixin 宣告了抽象成員，最後的具體類仍要提供實作。&lt;/p>
&lt;h3 id="三個關鍵字的差異">三個關鍵字的差異&lt;/h3>





&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">abstract&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">PeriodReportRow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">with&lt;/span> &lt;span class="n">_$PeriodReportRow&lt;/span> &lt;span class="c1">// ← mixin：接上生成 API surface
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">implements&lt;/span> &lt;span class="n">ReportAmountsView&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="err">←&lt;/span> &lt;span class="n">interface&lt;/span>&lt;span class="err">：拿到契約&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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;code>extends&lt;/code>&lt;/td>
 &lt;td>繼承父類別（單一）&lt;/td>
 &lt;td>可選擇覆寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>implements&lt;/code>&lt;/td>
 &lt;td>只拿型別契約&lt;/td>
 &lt;td>&lt;strong>要自己全部實作&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>with&lt;/code>&lt;/td>
 &lt;td>拿到 mixin 成員，可含實作或要求&lt;/td>
 &lt;td>取決於 mixin 內的成員是否已實作&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>extends&lt;/code> 佔據唯一父類別位置，適合真正的 is-a 關係；&lt;code>implements&lt;/code> 只拿契約，適合用型別描述能力；&lt;code>with&lt;/code> 在中間，適合把一組生成或共用的成員接到 class 上。&lt;/p>
&lt;h3 id="在-freezed-中的角色">在 freezed 中的角色&lt;/h3>
&lt;p>&lt;code>_$PeriodReportRow&lt;/code> 是 build_runner 跑完後在 &lt;code>period_report_dto.freezed.dart&lt;/code> 裡產出的 mixin，角色是把 Freezed 生成的 API surface 接到你宣告的 &lt;code>PeriodReportRow&lt;/code> 門面上。&lt;/p>
&lt;ul>
&lt;li>欄位 getter 的契約或 forwarding surface（&lt;code>date&lt;/code>、&lt;code>grossAmount&lt;/code>、&lt;code>channelA&lt;/code> 等）&lt;/li>
&lt;li>&lt;code>==&lt;/code> 和 &lt;code>hashCode&lt;/code> 相關生成邏輯&lt;/li>
&lt;li>&lt;code>copyWith&lt;/code>&lt;/li>
&lt;li>&lt;code>toString&lt;/code>&lt;/li>
&lt;li>JSON 相關的 generated function / method 接線（取決於是否搭配 &lt;code>json_serializable&lt;/code> 與 &lt;code>fromJson&lt;/code> factory）&lt;/li>
&lt;/ul>
&lt;p>所以 &lt;code>abstract class PeriodReportRow with _$PeriodReportRow&lt;/code> 在做的事是：&lt;/p>
&lt;blockquote>
&lt;p>「我這個 class 是抽象門面，Freezed 會把生成 API 放在 &lt;code>_$PeriodReportRow&lt;/code> mixin 與 &lt;code>_PeriodReportRow&lt;/code> 具體類裡；門面透過 &lt;code>with&lt;/code> 接上生成 surface，factory 再回傳真正持有欄位的生成類。」&lt;/p>&lt;/blockquote>
&lt;p>這裡最容易誤解的是「mixin 等於所有實作」。在 Freezed 的常見生成模式裡，mixin 會宣告或提供部分生成成員，真正持有 &lt;code>final&lt;/code> 欄位並滿足 getter 的通常是 factory 指向的 &lt;code>_PeriodReportRow&lt;/code> 具體類。&lt;code>with _$PeriodReportRow&lt;/code> 的價值是讓門面型別擁有一致的生成 API 形狀，而不是把每個欄位的儲存都塞進 mixin。&lt;/p>
&lt;h3 id="為什麼-freezed-用-mixin-而不是-extends">為什麼 freezed 用 mixin 而不是 extends&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>mixin 不佔「父類別」的獨生子位置&lt;/strong>：Dart 只允許單一 &lt;code>extends&lt;/code>、freezed 如果用 extends 強佔了、你就不能讓 model 繼承自己的 base class。&lt;code>with&lt;/code> 可以無限疊加、給你自由度&lt;/li>
&lt;li>&lt;strong>mixin 支援多個疊加&lt;/strong>：&lt;code>class Foo with A, B, C&lt;/code> 會把 A、B、C 的方法依序混入。Freezed 利用這個語法位置，把生成 API 接到使用者宣告的門面類&lt;/li>
&lt;li>&lt;strong>&lt;code>implements ReportAmountsView&lt;/code> 在這裡剛好成立&lt;/strong>：&lt;code>ReportAmountsView&lt;/code> 要求的是一組 getter 契約，而 Freezed 會讓生成的 &lt;code>_PeriodReportRow&lt;/code> 具體類依照 factory 參數產生對應欄位。門面類宣告 &lt;code>implements&lt;/code>，具體類回傳時提供欄位實作，所以不需要再手寫 18 個 forwarding getter&lt;/li>
&lt;/ul>
&lt;h3 id="簡化的等價心智模型">簡化的等價心智模型&lt;/h3>





&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="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">abstract&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">PeriodReportRow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">with&lt;/span> &lt;span class="n">_$PeriodReportRow&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">implements&lt;/span> &lt;span class="n">ReportAmountsView&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&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>&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="kd">abstract&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">PeriodReportRow&lt;/span> &lt;span class="kd">implements&lt;/span> &lt;span class="n">ReportAmountsView&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="c1">// 門面接上 generated API surface：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">PeriodReportRow&lt;/span> &lt;span class="n">copyWith&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">_PeriodReportRow&lt;/span> &lt;span class="kd">implements&lt;/span> &lt;span class="n">PeriodReportRow&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 具體生成類持有欄位並滿足 interface getters：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="kt">String&lt;/span> &lt;span class="n">date&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">Decimal&lt;/span> &lt;span class="n">grossAmount&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="err">@&lt;/span>&lt;span class="n">override&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">Decimal&lt;/span> &lt;span class="n">channelA&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 等等所有 factory 參數對應的欄位
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是心智模型：&lt;code>with&lt;/code> 接上 generated surface，&lt;code>factory = _PeriodReportRow&lt;/code> 接到真正的資料承載類。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：實作營運端報表 API、寫了一個 freezed model
<strong>疑問來源</strong>：<code>abstract class PeriodReportRow with _$PeriodReportRow implements ReportAmountsView</code> 這一行包含太多陌生語法
<strong>整理目的</strong>：把「為什麼長這樣」與「是否有更好懂做法」的脈絡記錄下來、避免下次又從零開始查
<strong>本文邊界</strong>：這是一篇 work-log，目標是回溯一次具體實作中的理解成本；它不取代 freezed 官方文件，也不把某個專案的模型分層當成通用規則。</p></blockquote>
<hr>
<h2 id="事件起點">事件起點</h2>
<p>今天在某個營運端 Flutter 專案新增週期彙總報表 API，這份報表和既有的單次作業報表共用呈現邏輯、各自有獨立的 DTO。為了讓兩個 DTO 共用 sections builder、抽了一個 <code>ReportAmountsView</code> 介面、讓兩邊的 <code>*Row</code> 都 <code>implements</code> 它。</p>
<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="err">@</span><span class="n">freezed</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">PeriodReportRow</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">with</span> <span class="n">_$PeriodReportRow</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="kd">implements</span> <span class="n">ReportAmountsView</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="kd">const</span> <span class="kd">factory</span> <span class="n">PeriodReportRow</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="kd">required</span> <span class="kt">String</span> <span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="c1">// ... 18 個欄位
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="p">})</span> <span class="o">=</span> <span class="n">_PeriodReportRow</span><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>abstract</code> 為什麼能配 <code>factory</code>、<code>with _$PeriodReportRow</code> 在做什麼、<code>_$</code> 這個前綴代表什麼、<code>= _PeriodReportRow</code> 如何接到生成類，以及為什麼要分成「我寫的 abstract」+「生成的 mixin」+「生成的具體類」三層。</p>
<p>這篇筆記把那次停下來查證的路徑整理成可重讀的判斷脈絡。</p>
<hr>
<h2 id="第一層with-是什麼">第一層：<code>with</code> 是什麼</h2>
<p><code>with</code> 是 Dart 的 <strong>mixin 語法</strong>、把另一個型別的成員「混入」當前 class。當前 class 會接上 mixin 提供的成員；如果 mixin 宣告了抽象成員，最後的具體類仍要提供實作。</p>
<h3 id="三個關鍵字的差異">三個關鍵字的差異</h3>





<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">abstract</span> <span class="kd">class</span> <span class="nc">PeriodReportRow</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kd">with</span> <span class="n">_$PeriodReportRow</span>         <span class="c1">// ← mixin：接上生成 API surface
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>    <span class="kd">implements</span> <span class="n">ReportAmountsView</span>  <span class="o">//</span> <span class="err">←</span> <span class="n">interface</span><span class="err">：拿到契約</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>關鍵字</th>
          <th>拿到什麼</th>
          <th>是否要自己寫實作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>extends</code></td>
          <td>繼承父類別（單一）</td>
          <td>可選擇覆寫</td>
      </tr>
      <tr>
          <td><code>implements</code></td>
          <td>只拿型別契約</td>
          <td><strong>要自己全部實作</strong></td>
      </tr>
      <tr>
          <td><code>with</code></td>
          <td>拿到 mixin 成員，可含實作或要求</td>
          <td>取決於 mixin 內的成員是否已實作</td>
      </tr>
  </tbody>
</table>
<p><code>extends</code> 佔據唯一父類別位置，適合真正的 is-a 關係；<code>implements</code> 只拿契約，適合用型別描述能力；<code>with</code> 在中間，適合把一組生成或共用的成員接到 class 上。</p>
<h3 id="在-freezed-中的角色">在 freezed 中的角色</h3>
<p><code>_$PeriodReportRow</code> 是 build_runner 跑完後在 <code>period_report_dto.freezed.dart</code> 裡產出的 mixin，角色是把 Freezed 生成的 API surface 接到你宣告的 <code>PeriodReportRow</code> 門面上。</p>
<ul>
<li>欄位 getter 的契約或 forwarding surface（<code>date</code>、<code>grossAmount</code>、<code>channelA</code> 等）</li>
<li><code>==</code> 和 <code>hashCode</code> 相關生成邏輯</li>
<li><code>copyWith</code></li>
<li><code>toString</code></li>
<li>JSON 相關的 generated function / method 接線（取決於是否搭配 <code>json_serializable</code> 與 <code>fromJson</code> factory）</li>
</ul>
<p>所以 <code>abstract class PeriodReportRow with _$PeriodReportRow</code> 在做的事是：</p>
<blockquote>
<p>「我這個 class 是抽象門面，Freezed 會把生成 API 放在 <code>_$PeriodReportRow</code> mixin 與 <code>_PeriodReportRow</code> 具體類裡；門面透過 <code>with</code> 接上生成 surface，factory 再回傳真正持有欄位的生成類。」</p></blockquote>
<p>這裡最容易誤解的是「mixin 等於所有實作」。在 Freezed 的常見生成模式裡，mixin 會宣告或提供部分生成成員，真正持有 <code>final</code> 欄位並滿足 getter 的通常是 factory 指向的 <code>_PeriodReportRow</code> 具體類。<code>with _$PeriodReportRow</code> 的價值是讓門面型別擁有一致的生成 API 形狀，而不是把每個欄位的儲存都塞進 mixin。</p>
<h3 id="為什麼-freezed-用-mixin-而不是-extends">為什麼 freezed 用 mixin 而不是 extends</h3>
<ul>
<li><strong>mixin 不佔「父類別」的獨生子位置</strong>：Dart 只允許單一 <code>extends</code>、freezed 如果用 extends 強佔了、你就不能讓 model 繼承自己的 base class。<code>with</code> 可以無限疊加、給你自由度</li>
<li><strong>mixin 支援多個疊加</strong>：<code>class Foo with A, B, C</code> 會把 A、B、C 的方法依序混入。Freezed 利用這個語法位置，把生成 API 接到使用者宣告的門面類</li>
<li><strong><code>implements ReportAmountsView</code> 在這裡剛好成立</strong>：<code>ReportAmountsView</code> 要求的是一組 getter 契約，而 Freezed 會讓生成的 <code>_PeriodReportRow</code> 具體類依照 factory 參數產生對應欄位。門面類宣告 <code>implements</code>，具體類回傳時提供欄位實作，所以不需要再手寫 18 個 forwarding getter</li>
</ul>
<h3 id="簡化的等價心智模型">簡化的等價心智模型</h3>





<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="c1">// 你寫的：
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">PeriodReportRow</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">with</span> <span class="n">_$PeriodReportRow</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">implements</span> <span class="n">ReportAmountsView</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 大致等於（觀念上）：
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">PeriodReportRow</span> <span class="kd">implements</span> <span class="n">ReportAmountsView</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 門面接上 generated API surface：
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="n">PeriodReportRow</span> <span class="n">copyWith</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">class</span> <span class="nc">_PeriodReportRow</span> <span class="kd">implements</span> <span class="n">PeriodReportRow</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="c1">// 具體生成類持有欄位並滿足 interface getters：
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="err">@</span><span class="n">override</span> <span class="kd">final</span> <span class="kt">String</span> <span class="n">date</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="err">@</span><span class="n">override</span> <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="err">@</span><span class="n">override</span> <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">channelA</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="c1">// ... 等等所有 factory 參數對應的欄位
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>這是心智模型：<code>with</code> 接上 generated surface，<code>factory = _PeriodReportRow</code> 接到真正的資料承載類。</p>
<hr>
<h2 id="第二層_-命名約定">第二層：<code>_$</code> 命名約定</h2>
<p>第一次看到 <code>_$PeriodReportRow</code> 容易以為這是某個 framework 的特殊符號。實際上是<strong>兩個獨立慣例疊加</strong>的結果。</p>
<h3 id="_-和--各自的角色"><code>_</code> 和 <code>$</code> 各自的角色</h3>
<table>
  <thead>
      <tr>
          <th>符號</th>
          <th>來源</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_</code></td>
          <td><strong>Dart 語言本身</strong>的規則</td>
          <td>開頭底線 = library-private、只有同個 library 看得到</td>
      </tr>
      <tr>
          <td><code>$</code></td>
          <td><strong>codegen 工具的慣例</strong>（freezed、json_serializable、retrofit 都遵守）</td>
          <td>「這個名字是機器產的、請別自己取一樣的名字」</td>
      </tr>
  </tbody>
</table>
<p>組合起來：</p>
<ul>
<li><code>_$PeriodReportRow</code> → 機器產的 + 只給內部用（你不該在外部檔案引用它）</li>
<li><code>$PeriodReportRowCopyWith</code> → 機器產的 + 公開介面（呼叫 <code>instance.copyWith(...)</code> 時要看得到型別）</li>
</ul>
<p>兩個前綴分別代表不同意圖——freezed 透過 <code>_</code> 的有無、區分「實作細節」跟「公開介面」。</p>
<h3 id="_foo-為什麼你的檔案看得到"><code>_$Foo</code> 為什麼你的檔案看得到</h3>
<p>Dart 的 library-private（<code>_</code> 前綴）並非「檔案私有」、是「<strong>library 私有</strong>」。預設一個 <code>.dart</code> 檔就是一個 library、但 <strong><code>part</code> 指令會把多個檔案併成同一個 library</strong>。</p>
<p>freezed model 檔案開頭那兩行：</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="k">part</span> <span class="s1">&#39;period_report_dto.freezed.dart&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">part</span> <span class="s1">&#39;period_report_dto.g.dart&#39;</span><span class="p">;</span></span></span></code></pre></div><p>就是在說：「這三個檔屬於同一個 library」。</p>
<p>結果：generated 檔裡的 <code>_$PeriodReportRow</code> 雖然 <code>_</code> 開頭、但因為 <code>part</code> 連通、你的主檔還是看得見、可以 <code>with</code> 它。其他 import 你檔案的人就看不到、正好符合「只給內部生成檔用」的意圖。</p>
<p>這也是為什麼<strong>忘記寫 <code>part 'xxx.freezed.dart';</code> 會編譯失敗</strong>——不是因為「找不到檔案」、是因為「<code>_$Foo</code> 不在同一個 library 內、外部不能引用」。</p>
<h3 id="一個快速辨認方式">一個快速辨認方式</h3>
<p>下次看 freezed / codegen 產出的名字、可以這樣判斷：</p>
<ul>
<li><code>_$Foo</code> → mixin / 實作類（內部用）</li>
<li><code>$Foo</code> → public 介面（給外部呼叫）</li>
<li><code>_Foo</code> → 純內部 class（如 <code>_PeriodReportRow</code> 是 freezed 為你的 factory 產的具體類）</li>
<li><code>Foo</code> → 你自己寫的 abstract class、是門面（facade）</li>
</ul>
<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">abstract</span> <span class="kd">class</span> <span class="nc">PeriodReportRow</span> <span class="kd">with</span> <span class="n">_$PeriodReportRow</span> <span class="kd">implements</span> <span class="n">ReportAmountsView</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">//</span>             <span class="err">↑</span> <span class="err">門面</span>            <span class="err">↑</span> <span class="err">內部</span> <span class="n">mixin</span>           <span class="err">↑</span> <span class="err">你定義的介面</span></span></span></code></pre></div><p>三層責任可以被辨認：你自己寫的門面類、機器產的實作、你自己定義的契約。它不是透明抽象，因為使用者仍要看懂 <code>part</code>、<code>with _$Foo</code> 與 factory redirect 這些接線。</p>
<hr>
<h2 id="第三層為什麼要這樣拆是設計不當嗎">第三層：為什麼要這樣拆——是設計不當嗎</h2>
<p><code>with _$Foo</code> 加 <code>part</code> 加 <code>abstract class</code> 加 <code>factory</code> 加 <code>_$ / $ / _ / 無前綴</code> 四種命名……理解到這裡會自然冒出一個問題：<strong>這個拆分本身、是不是 freezed 設計不當？</strong></p>
<p>我的看法：<strong>這個拆分不是 freezed 設計不當、但它確實暴露了 Dart 語言層的能力缺口</strong>。換個角度、「需要這樣拆」是症狀、不是病因——病因在語言本身。</p>
<h3 id="拆分到底解決了什麼問題">拆分到底解決了什麼問題</h3>
<p>把那幾個元素還原成「想做的事 vs 不得不這樣寫」：</p>
<table>
  <thead>
      <tr>
          <th>想做的事</th>
          <th>在 Dart 中需要的東西</th>
          <th>為什麼要拆</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不可變 class DTO + <code>copyWith</code></td>
          <td><code>==</code>、<code>hashCode</code>、<code>toString</code>、<code>copyWith</code></td>
          <td>Dart 有 records，但沒有能取代 class DTO 的 nominal data class</td>
      </tr>
      <tr>
          <td>JSON 序列化</td>
          <td><code>fromJson</code> / <code>toJson</code></td>
          <td>Dart 沒有 reflection（AOT 砍了）、只能 codegen</td>
      </tr>
      <tr>
          <td>Sum types（多個 constructor + pattern matching）</td>
          <td>sealed class + 多個 factory</td>
          <td>Dart 3 才有 sealed、pattern matching 也是 Dart 3</td>
      </tr>
      <tr>
          <td>把上面塞進<strong>一個</strong>讓人能寫的 class</td>
          <td>abstract class + mixin + factory</td>
          <td>這是「組裝零件」的膠水、不是真實功能</td>
      </tr>
  </tbody>
</table>
<p>前 3 行是真實需求；最後一行是「為了實現前 3 行、Dart 缺工具、所以要組裝」。</p>
<h3 id="對比其他語言處理同樣問題">對比其他語言處理同樣問題</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kotlin" data-lang="kotlin"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Kotlin —— 語言內建
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="k">class</span> <span class="nc">PeriodReport</span><span class="p">(</span><span class="k">val</span> <span class="py">date</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span> <span class="k">val</span> <span class="py">grossAmount</span><span class="p">:</span> <span class="n">BigDecimal</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// copy、equals、hashCode、toString 全部自動、0 行 codegen</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Rust —— derive macro 內建在語言
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="cp">#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">struct</span> <span class="nc">PeriodReport</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">date</span>: <span class="nb">String</span><span class="p">,</span><span class="w"> </span><span class="n">grossAmount</span>: <span class="nc">Decimal</span><span class="w"> </span><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// TypeScript —— 結構型別 + 解構即拷貝
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kr">type</span> <span class="nx">PeriodReport</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">date</span>: <span class="kt">string</span><span class="p">;</span> <span class="nx">grossAmount</span>: <span class="kt">Decimal</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">const</span> <span class="nx">next</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">prev</span><span class="p">,</span> <span class="nx">grossAmount</span>: <span class="kt">newAmount</span> <span class="p">};</span>  <span class="c1">// copyWith 不用存在
</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Swift —— struct 是值類型</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">struct</span> <span class="nc">PeriodReport</span><span class="p">:</span> <span class="n">Codable</span><span class="p">,</span> <span class="nb">Equatable</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">let</span> <span class="nv">date</span><span class="p">:</span> <span class="nb">String</span><span class="p">;</span> <span class="kd">let</span> <span class="nv">grossAmount</span><span class="p">:</span> <span class="n">Decimal</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<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="c1">// Dart 2 —— 你只能這樣寫（沒 freezed 的話）
</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">PeriodReport</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">date</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">const</span> <span class="n">PeriodReport</span><span class="p">({</span><span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">date</span><span class="p">,</span> <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">grossAmount</span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="n">PeriodReport</span> <span class="n">copyWith</span><span class="p">({</span><span class="kt">String</span><span class="o">?</span> <span class="n">date</span><span class="p">,</span> <span class="n">Decimal</span><span class="o">?</span> <span class="n">grossAmount</span><span class="p">})</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="n">PeriodReport</span><span class="p">(</span><span class="nl">date:</span> <span class="n">date</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">date</span><span class="p">,</span> <span class="nl">grossAmount:</span> <span class="n">grossAmount</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">grossAmount</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="err">@</span><span class="n">override</span> <span class="kt">bool</span> <span class="kd">operator</span> <span class="o">==</span><span class="p">(...)</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="err">@</span><span class="n">override</span> <span class="kt">int</span> <span class="kd">get</span> <span class="n">hashCode</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="err">@</span><span class="n">override</span> <span class="kt">String</span> <span class="n">toString</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kd">factory</span> <span class="n">PeriodReport</span><span class="p">.</span><span class="n">fromJson</span><span class="p">(</span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">json</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">toJson</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="o">//</span> <span class="m">18</span> <span class="err">個欄位</span> <span class="err">×</span> <span class="m">6</span> <span class="err">個樣板</span> <span class="err">≈</span> <span class="m">150</span> <span class="err">行手寫、每加一個欄位要改</span> <span class="m">5</span> <span class="err">個地方</span></span></span></code></pre></div><p>Freezed 是在這個現實下做的工程權衡：<strong>用一個外部工具、把這上百行壓回十幾行宣告</strong>。代價就是看到的「分三層」。</p>
<h3 id="freezed-自己有沒有設計可議的地方">Freezed 自己有沒有設計可議的地方</h3>
<p>Freezed 的設計可議之處集中在抽象洩漏，而不是功能是否成立：</p>
<ul>
<li><strong><code>part</code> directive 是漏出的實作細節</strong>：使用者必須知道 library / part 的概念才能寫對。Freezed 依賴 <code>part</code>，是因為生成檔需要和主檔落在同一個 library，讓 <code>_</code> 開頭的 generated member 可以被主檔看到</li>
<li><strong><code>with _$Foo</code> 暴露了 codegen 接線</strong>：理想上 <code>@freezed</code> 只描述資料形狀，使用者不用知道生成 mixin 的名字。現行 codegen surface 需要使用者把生成 mixin 接上去，這就是學習成本來源</li>
<li><strong><code>abstract class</code> + <code>factory</code> 需要語言模型支撐</strong>：abstract class 不能直接 <code>new</code>，但 <code>factory</code> 可以回傳具體子類。Freezed 產生 <code>_PeriodReportRow</code>，因此這個寫法在語言上成立；直覺成本來自「門面類」和「具體生成類」分離</li>
</ul>
<h3 id="那設計得不當的真正主體是誰">那「設計得不當」的真正主體是誰</h3>
<p>這個問題要拆成三層看：</p>
<ol>
<li><strong>你的 model 設計</strong>：宣告一個 immutable DTO 並實作金額視圖契約，這個方向成立</li>
<li><strong>Freezed 的設計</strong>：它用 codegen 換掉大量樣板，代價是 <code>part</code>、<code>with _$Foo</code>、factory redirect 這些接線露在使用者面前</li>
<li><strong>Dart 的語言能力</strong>：Dart 長期缺少穩定的 data class / static metaprogramming 能力，讓資料模型的重複樣板需要靠 build_runner 與外部 codegen 補齊</li>
</ol>
<h3 id="未來改善方向不是-macros-這條直線">未來改善方向不是 macros 這條直線</h3>
<p>Dart 官方在 2025-01-29 宣布停止 macros 工作，因此「等 Dart macros 穩定後，這層拆分自然消失」已經不是可靠判斷。更務實的觀察是：Dart 仍會改善資料建模與 codegen 體驗，但方向可能是更專門的 data language features、build_runner 改善或 augmentations，而不是通用 macros。</p>
<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="err">@</span><span class="n">Data</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">class</span> <span class="nc">PeriodReportRow</span> <span class="kd">implements</span> <span class="n">ReportAmountsView</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">date</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 18 個欄位
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="o">//</span> <span class="err">目標是讓資料形狀、序列化、</span><span class="n">value</span> <span class="n">equality</span><span class="err">、</span><span class="n">copyWith</span> <span class="err">更接近語言級宣告</span></span></span></code></pre></div><p>這段只能當作「期待中的語言表達能力」，不能當作 Dart 已承諾的 roadmap。對今天的專案來說，Freezed 仍然是把資料模型樣板壓低的成熟工具；它的成本是 build_runner、生成檔、以及本文拆解的三層心智模型。</p>
<hr>
<h2 id="第四層沒有-freezed-怎麼做">第四層：沒有 freezed 怎麼做</h2>
<p>如果規劃時就決定不裝 freezed、Dart 怎麼處理「immutable + JSON + copyWith + equality」這組需求？</p>
<h3 id="路線一純手寫">路線一：純手寫</h3>
<p>把 freezed 產的東西自己寫一遍：</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">PeriodReportRow</span> <span class="kd">implements</span> <span class="n">ReportAmountsView</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">date</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">primaryOrderCount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ... 其他 16 個欄位
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="kd">const</span> <span class="n">PeriodReportRow</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="kd">factory</span> <span class="n">PeriodReportRow</span><span class="p">.</span><span class="n">fromJson</span><span class="p">(</span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">json</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">PeriodReportRow</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nl">date:</span> <span class="n">json</span><span class="p">[</span><span class="s1">&#39;date&#39;</span><span class="p">]</span> <span class="o">as</span> <span class="kt">String</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="nl">primaryOrderCount:</span> <span class="n">json</span><span class="p">[</span><span class="s1">&#39;primary_order_count&#39;</span><span class="p">]</span> <span class="o">as</span> <span class="kt">int</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nl">grossAmount:</span> <span class="n">jsonToDecimal</span><span class="p">(</span><span class="n">json</span><span class="p">[</span><span class="s1">&#39;gross_amount&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="c1">// ... 重複 18 次
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span>    <span class="p">);</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">toJson</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="s1">&#39;date&#39;</span><span class="o">:</span> <span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="s1">&#39;primary_order_count&#39;</span><span class="o">:</span> <span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="s1">&#39;gross_amount&#39;</span><span class="o">:</span> <span class="n">grossAmount</span><span class="p">.</span><span class="n">toString</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="c1">// ... 重複 18 次
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"></span>      <span class="p">};</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="n">PeriodReportRow</span> <span class="n">copyWith</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="kt">String</span><span class="o">?</span> <span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="kt">int</span><span class="o">?</span> <span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="n">Decimal</span><span class="o">?</span> <span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">  <span class="p">})</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">      <span class="n">PeriodReportRow</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="nl">date:</span> <span class="n">date</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="nl">primaryOrderCount:</span> <span class="n">primaryOrderCount</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="nl">grossAmount:</span> <span class="n">grossAmount</span> <span class="o">??</span> <span class="k">this</span><span class="p">.</span><span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="c1">// ... 重複 18 次
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="c1"></span>      <span class="p">);</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">
</span></span><span class="line"><span class="ln">42</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">  <span class="kt">bool</span> <span class="kd">operator</span> <span class="o">==</span><span class="p">(</span><span class="kt">Object</span> <span class="n">other</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">      <span class="n">identical</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">other</span><span class="p">)</span> <span class="o">||</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">      <span class="n">other</span> <span class="k">is</span> <span class="n">PeriodReportRow</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">          <span class="n">other</span><span class="p">.</span><span class="n">date</span> <span class="o">==</span> <span class="n">date</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">          <span class="n">other</span><span class="p">.</span><span class="n">primaryOrderCount</span> <span class="o">==</span> <span class="n">primaryOrderCount</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">          <span class="n">other</span><span class="p">.</span><span class="n">grossAmount</span> <span class="o">==</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">          <span class="c1">// ... 重複 18 次
</span></span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">  <span class="kt">int</span> <span class="kd">get</span> <span class="n">hashCode</span> <span class="o">=&gt;</span> <span class="kt">Object</span><span class="p">.</span><span class="n">hash</span><span class="p">(</span><span class="n">date</span><span class="p">,</span> <span class="n">primaryOrderCount</span><span class="p">,</span> <span class="n">grossAmount</span> <span class="cm">/* 18 個 */</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl">  <span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">  <span class="kt">String</span> <span class="n">toString</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="s1">&#39;PeriodReportRow(date: </span><span class="si">$</span><span class="n">date</span><span class="s1">, ...)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>18 個欄位 × 6 個樣板 ≈ 150 行</strong>、每加一個欄位要改 5 處（constructor、fromJson、toJson、copyWith、==、hashCode）。漏改一處 → 隱性 bug。</p>
<h3 id="路線二只-codegen-序列化其他手寫">路線二：只 codegen 序列化、其他手寫</h3>
<p>只用 <code>json_serializable</code>（比 freezed 輕量很多）：</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="err">@</span><span class="n">JsonSerializable</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">class</span> <span class="nc">PeriodReportRow</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="kt">String</span> <span class="n">date</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="err">@</span><span class="n">JsonKey</span><span class="p">(</span><span class="nl">name:</span> <span class="s1">&#39;primary_order_count&#39;</span><span class="p">)</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">primaryOrderCount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="err">@</span><span class="n">DecimalConverter</span><span class="p">()</span> <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">const</span> <span class="n">PeriodReportRow</span><span class="p">({</span><span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">date</span><span class="p">,</span> <span class="p">...});</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">factory</span> <span class="n">PeriodReportRow</span><span class="p">.</span><span class="n">fromJson</span><span class="p">(</span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">json</span><span class="p">)</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="n">_$PeriodReportRowFromJson</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">toJson</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">_$PeriodReportRowToJson</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="c1">// 不寫 ==、hashCode、copyWith
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>省掉 fromJson / toJson 的樣板（最容易出錯的部分）、但仍要自己寫 <code>==</code> 和 <code>copyWith</code>（如果需要）。</p>
<h3 id="路線三dart-3-records">路線三：Dart 3 Records</h3>





<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">typedef</span> <span class="n">PeriodReportRow</span> <span class="o">=</span> <span class="p">({</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kt">String</span> <span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kt">int</span> <span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 建立
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">row</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nl">date:</span> <span class="s1">&#39;2026-05-11&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nl">primaryOrderCount:</span> <span class="m">0</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nl">grossAmount:</span> <span class="n">Decimal</span><span class="p">.</span><span class="n">zero</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">// 「copyWith」就是用解構重組
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">next</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nl">date:</span> <span class="n">row</span><span class="p">.</span><span class="n">date</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="nl">grossAmount:</span> <span class="n">newAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nl">primaryOrderCount:</span> <span class="n">row</span><span class="p">.</span><span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre></div><p>Record 是 Dart 3 內建的不可變值型別，適合短距離攜帶一組值：</p>
<ul>
<li>支援：自動 <code>==</code> / <code>hashCode</code> / <code>toString</code></li>
<li>支援：不可變</li>
<li>限制：無名 → 不能 <code>implements ReportAmountsView</code>、不能加方法、不能 <code>extends</code></li>
<li>限制：JSON 還是要手寫</li>
<li>限制：沒有 named constructor → 無法做「from raw API JSON」的轉換邏輯</li>
</ul>
<p>對「跨模組共享、需要實作介面、需要 fromJson」的 DTO，record 的語意承載力不足。對「函式內部短暫的多回傳值」，record 很合適。</p>
<hr>
<h2 id="真正該問的問題你需要的是哪幾項">真正該問的問題：你需要的是哪幾項</h2>
<p>回頭把「freezed 給你的功能」拆開看、對 DTO 真正用得到的有：</p>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>DTO 需求程度</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fromJson</code> / <code>toJson</code></td>
          <td>必要</td>
          <td>後端來的 raw JSON、必須轉成型別</td>
      </tr>
      <tr>
          <td>Immutable（<code>final</code>）</td>
          <td>必要</td>
          <td>DTO 被多處引用、可變會引入難追的 bug</td>
      </tr>
      <tr>
          <td><code>==</code> / <code>hashCode</code></td>
          <td>看用法</td>
          <td>若放進 <code>RxBool</code>、<code>Set</code>、<code>Map</code> 才需要；單純傳遞用不到</td>
      </tr>
      <tr>
          <td><code>copyWith</code></td>
          <td>通常不需要</td>
          <td>DTO 從 API 來就餵給 domain layer，修改通常發生在 domain model</td>
      </tr>
      <tr>
          <td>Sealed union</td>
          <td>不需要</td>
          <td>DTO 是固定形狀、不是「多種變體擇一」</td>
      </tr>
      <tr>
          <td><code>toString</code> 除錯</td>
          <td>看情境</td>
          <td>開發 / 除錯時方便、prod 用不到</td>
      </tr>
  </tbody>
</table>
<p>這個 DTO 情境的核心需求是 JSON 轉換與 immutable；其他能力是 Freezed 順手提供的附加價值，是否有用取決於後續資料流。</p>
<h3 id="過剩功能不是壞事但會誤導">過剩功能不是壞事、但會誤導</h3>
<p>用了 freezed 後會傾向「reach for <code>copyWith</code>」，因為它就在那。如果一開始只用 <code>json_serializable</code>，可能根本不會在 DTO 上做修改。較穩定的 DTO 用法是把 DTO 視為 API 邊界的快照；需要變更行為時，轉成 domain model 再承載狀態變化。</p>
<h3 id="這次-dto-只吃到-freezed-的部分價值">這次 DTO 只吃到 Freezed 的部分價值</h3>
<p>Freezed 在 DTO 上仍有價值，尤其是 immutable、JSON 轉換接線、欄位同步與 <code>toString</code> 除錯。這次報表 DTO 的資料流比較單純，主要吃到的是 JSON 轉換與 immutable；<code>copyWith</code>、sealed union、複雜狀態轉移這些能力比較像附加值。</p>
<p>Domain 物件（如 <code>ShoppingCart</code>、<code>Order</code>）常有「在現有狀態上做小修改」或「多種狀態擇一」的場景，這時 <code>copyWith</code> 與 sealed union 更容易回收那層拆分成本。比較精確的判斷不是「Freezed 不適合 DTO」，而是「不同 model 層吃到的 Freezed 價值不同」。</p>
<hr>
<h2 id="第五層更好懂的路徑是中間投影物件">第五層：更好懂的路徑是中間投影物件</h2>
<p>重新用 WARP 看這個設計時，決策錨點不是「怎樣讓 builder 少寫一次」，而是「下一個維護者能不能快速看懂資料怎麼從後端 row 變成報表 sections」。如果這個錨點成立，讓 DTO 直接 <code>implements ReportAmountsView</code> 的寫法就不一定是最佳答案。</p>
<p>目前的做法把共用點放在 DTO 型別上。兩種報表 row 都是後端 API row，卻為了共用 <code>_buildGeneralSections</code> / <code>_buildAccountSections</code>，一起實作一個 18 個 getter 的 <code>ReportAmountsView</code>。這在型別上可行，但讀者要同時理解 Freezed 生成類、mixin、interface、DTO 與報表 builder，才能知道為什麼這行能編譯。</p>
<h3 id="共用-builder-的三個局部方案">共用 builder 的三個局部方案</h3>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>核心做法</th>
          <th>讀者要理解什麼</th>
          <th>主要成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. DTO 直接實作共用介面</td>
          <td>兩個 row 都 <code>implements View</code></td>
          <td>Freezed + mixin + interface + builder</td>
          <td>抽象位置偏早，型別關係較難讀</td>
      </tr>
      <tr>
          <td>2. 直接重複兩份 builder</td>
          <td>兩種報表各自寫 sections builder</td>
          <td>每個 builder 自己讀自己的 row</td>
          <td>重複邏輯，後續欄位變動要改兩處</td>
      </tr>
      <tr>
          <td>3. 先投影成報表金額模型</td>
          <td>row 先轉 <code>ReportAmounts</code></td>
          <td>API row → 報表金額投影 → sections</td>
          <td>多一個 model 與兩份 mapping</td>
      </tr>
  </tbody>
</table>
<p>方案 1 是目前寫法。它的優點是 <code>_buildGeneralSections</code> / <code>_buildAccountSections</code> 可以直接共用，而且沒有額外 mapping；缺點是共用介面綁在 API DTO 上，讓「後端資料形狀」和「報表需要的共同金額視圖」混在同一層。這種寫法對熟悉 Freezed 的人不難，但對第一次接手的人，理解成本集中在一行 class 宣告上。</p>
<p>方案 2 是最直白的寫法。每種報表 row 用自己的 builder，讀者不用理解跨 DTO 介面；缺點是兩份 builder 很容易長得幾乎一樣。當報表欄位增加或文字調整時，維護者要記得同步兩邊，重複會變成一致性風險。</p>
<p>方案 3 把共用點移到更貼近需求的中間層。DTO 仍然只描述 API 回傳形狀，報表 builder 只吃 <code>ReportAmounts</code>，兩個 row 各自用 extension 或 mapper 明確轉成報表需要的共同資料。</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">ReportAmounts</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">const</span> <span class="n">ReportAmounts</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">primaryTurnover</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">required</span> <span class="k">this</span><span class="p">.</span><span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// ...其餘報表需要的金額欄位
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">primaryOrderCount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">primaryTurnover</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kd">final</span> <span class="n">Decimal</span> <span class="n">grossAmount</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">extension</span> <span class="n">SingleRunReportRowAmounts</span> <span class="n">on</span> <span class="n">SingleRunReportRow</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="n">ReportAmounts</span> <span class="n">toAmounts</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">ReportAmounts</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nl">primaryOrderCount:</span> <span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nl">primaryTurnover:</span> <span class="n">primaryTurnover</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nl">grossAmount:</span> <span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">extension</span> <span class="n">PeriodReportRowAmounts</span> <span class="n">on</span> <span class="n">PeriodReportRow</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="n">ReportAmounts</span> <span class="n">toAmounts</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">ReportAmounts</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="nl">primaryOrderCount:</span> <span class="n">primaryOrderCount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="nl">primaryTurnover:</span> <span class="n">primaryTurnover</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="nl">grossAmount:</span> <span class="n">grossAmount</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">      <span class="p">);</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個 mapping 看起來重複，但它是有價值的重複：它明確標出「哪些 API 欄位被投影成報表金額」。後端欄位名稱或語意改變時，維護者會在 mapper 裡看到轉換邊界，而不是在一個 18-getter interface 裡推理兩個 DTO 為什麼剛好長得一樣。</p>
<h3 id="重新判斷">重新判斷</h3>
<p>以好懂與好維護為核心，方案 3 比方案 1 更穩。它多寫一個 <code>ReportAmounts</code> 和兩份 mapping，但把複雜度放在比較合理的位置：DTO 層接 API，projection 層接報表語意，builder 層只處理畫面 / 呈現 sections。</p>
<p>方案 1 可以短期保留，因為它型別安全、改動小、和既有 Freezed 寫法一致。但若這段程式會長期被不同人維護，或未來還會增加其他 report row，應把 <code>ReportAmountsView</code> 換成明確的 <code>ReportAmounts</code> 投影模型。</p>
<p>實作落地時還有一個命名細節：如果已經從「共用介面」改成「中間投影模型」，檔名也應從 <code>report_amounts_view.dart</code> 改成 <code>report_amounts.dart</code>。否則程式碼雖然改成 projection，讀者仍會被舊的 View 命名帶回「DTO 實作介面」的心智模型。</p>
<h3 id="實作後驗證">實作後驗證</h3>
<p>這輪實作已經把 <code>ReportAmountsView</code> 移除，改成 <code>ReportAmounts</code> 投影模型與兩個 <code>toAmounts()</code> extension。局部 <code>flutter analyze</code> 對修改檔案通過，並補了 <code>report_amounts_test.dart</code> 驗證兩種報表 row 的共同金額欄位投影正確。</p>
<p>這個驗證證明 projection 邊界在型別與欄位對應上可行，但它還沒有驗證呈現版面或實際 API response 的完整結果。後續若報表內容有差異，應回到 sections builder 或 API 欄位語意，而不是回頭讓 DTO 重新實作共用介面。</p>
<hr>
<h2 id="規劃有沒有瑕疵">規劃有沒有瑕疵</h2>
<p>整體判斷：<strong>使用 Freezed 本身不是瑕疵，但共用 builder 的抽象位置值得調整</strong>。</p>
<h3 id="1-工具選擇是一致性-vs-適配度的取捨">1. 工具選擇是「一致性 vs 適配度」的取捨</h3>
<p>這類專案統一使用 freezed 的收益：</p>
<ul>
<li><strong>一致性</strong>：所有 model 一樣寫，接手者不用學兩套</li>
<li><strong>未雨綢繆</strong>：今天 DTO 不需要 <code>copyWith</code>、明天可能要（例如做 optimistic update 時要短暫修改 DTO）</li>
<li><strong>降低決策成本</strong>：不用每個 model 問「這個需要 copyWith 嗎？」</li>
</ul>
<p>成本：</p>
<ul>
<li><strong>DTO 上「邊際過剩」</strong>：用不到的功能也產出來、多花 build_runner 時間</li>
<li><strong>抽象洩漏</strong>：使用者必須懂 <code>_$</code> / <code>part</code> / mixin</li>
</ul>
<p>這個取捨<strong>沒標準答案</strong>、看團隊規模和維護週期。若系統長期維護、多人接手、既有專案已經採用 Freezed、而 build_runner 成本可接受，一致性的價值通常會高於 DTO 上的邊際過剩。</p>
<h3 id="2-dto-與-domain-model-兩層分離仍然合理">2. DTO 與 domain model 兩層分離仍然合理</h3>
<p>不在「用了 freezed」、而在於——<strong>是否需要 DTO 與 domain model 兩層分離</strong>？</p>
<p>這類專案結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">SingleRunReportRow（DTO、貼著 API）
</span></span><span class="line"><span class="ln">2</span><span class="cl">       ↓ service 轉換
</span></span><span class="line"><span class="ln">3</span><span class="cl">ReportSummary（domain、貼著 UI / 呈現）</span></span></code></pre></div><p>兩層是分開的。這個分層有成本：</p>
<ul>
<li>多寫一個 model</li>
<li>多寫一份轉換邏輯</li>
<li>多一份要維護</li>
</ul>
<p>但價值：</p>
<ul>
<li>後端改 API 欄位名 → 只動 DTO 層、domain 不受影響</li>
<li>UI 要新增顯示邏輯 → 只動 domain 層、DTO 不受影響</li>
<li>呈現報表的格式可以脫離 API 變化</li>
</ul>
<p>對長期維護、資料語意敏感的營運系統，這層分離通常值得；對短期 prototype，這層分離的維護成本可能高於收益。</p>
<h3 id="3-共用-builder-的抽象位置可能放太早">3. 共用 builder 的抽象位置可能放太早</h3>
<p><code>ReportAmountsView</code> 把報表需要的共同欄位直接壓到 API DTO 上，這是目前寫法最需要檢討的地方。更清楚的分層是：DTO 先完整接住後端 row，再由 mapper 投影成 <code>ReportAmounts</code>，最後由 sections builder 使用這個報表模型。</p>
<p>這個調整不會否定 Freezed，也不會否定 DTO / domain 分層。它只是把「共同報表金額」從 API DTO interface 移到報表投影層，讓型別關係更接近讀者真正要理解的資料流。</p>
<h3 id="一個反向思考">一個反向思考</h3>
<p>如果<strong>沒有 freezed</strong>、會怎麼做？</p>
<p>我猜會：</p>
<ol>
<li>DTO 只用 <code>json_serializable</code>（最輕量）</li>
<li>domain model 手寫（反正欄位通常比 DTO 少）</li>
<li>用 immutable 慣例但不強制（<code>final</code> 欄位 + 沒有 setter）</li>
</ol>
<p>這樣寫出來會比現在<strong>少一層拆分但多一些手寫樣板</strong>。誰好誰壞、看 trade-off 什麼：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>用 freezed</th>
          <th>不用 freezed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫起來</td>
          <td>短</td>
          <td>長</td>
      </tr>
      <tr>
          <td>讀起來</td>
          <td>多層、要懂 mixin</td>
          <td>直白</td>
      </tr>
      <tr>
          <td>改起來</td>
          <td>改一處</td>
          <td>改多處</td>
      </tr>
      <tr>
          <td>學習門檻</td>
          <td>高</td>
          <td>低</td>
      </tr>
      <tr>
          <td>出錯機率</td>
          <td>欄位同步漏改風險低，但有工具鏈風險</td>
          <td>手寫易漏改</td>
      </tr>
      <tr>
          <td>Build 時間</td>
          <td>增加 build_runner 成本</td>
          <td>沒影響</td>
      </tr>
      <tr>
          <td>Debug 體驗</td>
          <td>IDE 跳轉差</td>
          <td>直接看到</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="結論">結論</h2>
<ol>
<li><strong>「拆」是 Freezed 在 Dart 現有 codegen surface 下的工程妥協</strong>：它用三層結構換掉大量手寫樣板</li>
<li><strong><code>with _$Foo</code> 和 <code>part</code> 是漏出的實作細節</strong>：使用者需要理解 library、mixin、factory redirect，才能讀懂 Freezed 生成模型</li>
<li><strong>不同 model 層吃到的 Freezed 價值不同</strong>：DTO 常吃到 immutable / JSON / 欄位同步，domain model 更容易吃到 <code>copyWith</code> / union / 狀態轉移能力；統一用法換來的一致性，在長期維護的專案上可能值得</li>
<li><strong>Dart macros 不是可期待的解法路線</strong>：官方已停止 macros 工作，後續改善更可能來自 data features、build_runner 或 augmentations</li>
<li><strong>真正要檢討的是分層邊界</strong>：DTO 與 domain model 分離是否值得，比 <code>with _$Foo</code> 本身更接近架構決策</li>
<li><strong>目前 <code>implements ReportAmountsView</code> 可行但不一定最好懂</strong>：若核心目標是長期維護，<code>ReportAmounts</code> 投影模型通常比讓 API DTO 直接實作共用介面更清楚；落地時連檔名也要改成 projection 命名，避免舊抽象殘留</li>
</ol>
<p>換個角度說：當你寫 <code>with _$PeriodReportRow</code> 時，你是在接受一個 codegen 工具的心智模型，用它補上資料類型在手寫 Dart 裡會產生的大量樣板。</p>
<hr>
<h2 id="附錄今日實作中相關的設計決策">附錄：今日實作中相關的設計決策</h2>
<p>這次新增週期彙總報表 API 時，面對的關鍵設計選擇是「沿用既有 row、還是新增一個獨立 row」。</p>
<p>當下選擇了新增，然後抽 <code>ReportAmountsView</code> 介面共用 sections builder。這個決策當時在 A/B/C 三個選項裡合理，但重新用「好懂、好維護」作為錨點審查後，應該補上第四個選項：</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>優點</th>
          <th>缺點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A. 沿用既有 row、把獨有欄位改 optional</td>
          <td>共用一個 model、少寫 18 個欄位</td>
          <td>兩個語意完全不同的東西放在一起、型別會說謊</td>
      </tr>
      <tr>
          <td>B. 新增獨立 row、各自獨立</td>
          <td>語意清楚、各自演化</td>
          <td>報表 sections builder 可能重複</td>
      </tr>
      <tr>
          <td>C. 新增 + 抽 <code>ReportAmountsView</code> 介面共用 builder</td>
          <td>兼顧 A 的 DRY + B 的清楚</td>
          <td>多一個 interface 檔案、需理解 Freezed <code>implements</code> 用法</td>
      </tr>
      <tr>
          <td>D. 新增 + 投影成 <code>ReportAmounts</code></td>
          <td>DTO 與報表語意分層清楚</td>
          <td>多一個投影 model 與兩份 mapping</td>
      </tr>
  </tbody>
</table>
<p>選項 A 的主要問題是型別會說謊。既有 row 有單次作業、操作者、時間等語意，新的 row 是跨作業週期彙總；把兩種欄位塞進同一個 row，會讓 optional 欄位承擔太多語意分支。</p>
<p>選項 B 的主要問題是同步成本。它最容易讀，但如果兩種報表的 sections 幾乎一致，後續調整顯示項目時就要維護兩份相似邏輯。</p>
<p>選項 C 是當下採用的路徑。<code>ReportAmountsView</code> 只覆蓋「金額部分」、操作者 / 作業週期 / 日期等識別欄位刻意留給各自的 row 自管，避免介面變成 god interface；但它也讓 API DTO 直接承擔報表共用介面，讀者必須理解 Freezed 的門面類、generated mixin 與具體生成類。</p>
<p>選項 D 是重新審查後更好的候選。它保留兩種報表 row 各自獨立，也保留 sections builder 共用，但把共用點移到 <code>ReportAmounts</code> 這個報表投影模型。這樣多寫的 mapping 是刻意暴露資料轉換邊界，而不是無效樣板。</p>
<p>因此，本文更新後的判斷是：<strong>當下選 C 可以理解，但若要讓程式碼更好懂、更好維護，實作上應改成 D</strong>。</p>
<hr>
<h2 id="參考資料">參考資料</h2>
<ul>
<li><a href="https://pub.dev/packages/freezed">freezed 套件</a></li>
<li><a href="https://dart.dev/language/mixins">Dart language tour - Mixins</a></li>
<li><a href="https://dart.dev/language/libraries">Dart language tour - Libraries and imports</a></li>
<li><a href="https://dart.dev/blog/an-update-on-dart-macros-data-serialization">Dart Blog - An update on Dart macros &amp; data serialization</a></li>
<li><a href="https://dart.dev/language/records">Dart Records</a></li>
<li><a href="../freezed/">既有的 freezed 選型評估筆記</a></li>
</ul>
]]></content:encoded></item><item><title>Freezed 選型評估</title><link>https://tarrragon.github.io/blog/work-log/freezed-%E9%81%B8%E5%9E%8B%E8%A9%95%E4%BC%B0/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/freezed-%E9%81%B8%E5%9E%8B%E8%A9%95%E4%BC%B0/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>關聯 Ticket&lt;/strong>：0.2.0-W5-007
&lt;strong>決策結論&lt;/strong>：移除 freezed，採用 json_serializable + Equatable&lt;/p>&lt;/blockquote>
&lt;!-- 前言 -->
&lt;p>我設定了一個新的需求開了一個專案，我沒有專門指定開發的框架或者細節，我只有很簡單的先建立我需求的 spec 文件，這個文件當然並不完整，我是希望先讓AI做一個 原形，我會在 prototype 符合我的需求動起來之後再介入去調整設計。&lt;/p>
&lt;p>我的初始技術規範就只有我要用 flutter 去寫，所以AI就動了，但是在中間我發現 AI使用了 Freezed ，我並不喜歡在我 build 之外還要做一次
code generation 的動作，所以我就跟AI討論一次關於 Freezed 這種做法的必要性，至少在原形階段我覺得單純一點的 model 檔案沒有什麼不好，也不大會出錯，整體討論下來我選擇 捨棄 已有的 Freezed 程式碼，重構成更簡易的 版本，但是我覺得這個評估還是很有價值，所以讓AI重新整理了一次討論的內容作為備查。&lt;/p>
&lt;hr>
&lt;h2 id="1-freezed-是什麼">1. Freezed 是什麼&lt;/h2>
&lt;p>Freezed 是 Dart 的自動程式碼產生（code generation）套件，專門用來幫你自動生成資料類別（data class）裡那些重複的樣板程式碼（boilerplate），包括：&lt;/p>
&lt;ul>
&lt;li>&lt;code>copyWith&lt;/code>：複製一份物件，但可以只改其中幾個欄位，常用在狀態管理時產生新狀態&lt;/li>
&lt;li>&lt;code>==&lt;/code> / &lt;code>hashCode&lt;/code>：值相等比較（value equality），讓兩個內容相同的物件被判定為「相等」&lt;/li>
&lt;li>&lt;code>toString&lt;/code>：把物件轉成易讀的字串，方便除錯&lt;/li>
&lt;li>&lt;code>fromJson&lt;/code> / &lt;code>toJson&lt;/code>：JSON 序列化與反序列化，搭配 json_serializable 使用，處理前後端資料交換&lt;/li>
&lt;li>聯合型別（Union types）/ 密封類別（sealed class）：用 &lt;code>@freezed&lt;/code> 的多建構子語法，實現型別安全的多態模式&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>解決的核心問題&lt;/strong>：Dart 的類別預設是可變的（mutable），而且比較兩個物件時只看記憶體位址是否相同（identity equality），不會比較欄位內容。如果要手刻一個有 10 個欄位的不可變資料物件（immutable value object），大約需要 80-120 行程式碼，而且每次修改欄位都要同步更動 6 個地方（欄位宣告、建構子、&lt;code>copyWith&lt;/code>、&lt;code>==&lt;/code>、&lt;code>hashCode&lt;/code>、&lt;code>toJson&lt;/code>），非常容易漏改出錯。&lt;/p>
&lt;hr>
&lt;h2 id="2-優缺點分析">2. 優缺點分析&lt;/h2>
&lt;h3 id="優點">優點&lt;/h3>
&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>copyWith&lt;/td>
 &lt;td>建立修改後的新實例，State 管理必備&lt;/td>
 &lt;td>高（State 類別頻繁使用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>== / hashCode&lt;/td>
 &lt;td>Value equality，Riverpod 用於判斷狀態是否變更&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fromJson / toJson&lt;/td>
 &lt;td>JSON 序列化，WebSocket 通訊必備&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Immutability 保證&lt;/td>
 &lt;td>編譯期強制不可變&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Union types / sealed&lt;/td>
 &lt;td>型別安全的多態模式&lt;/td>
 &lt;td>視需求（本專案未使用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="缺點">缺點&lt;/h3>
&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>build_runner 依賴&lt;/td>
 &lt;td>每次改模型需執行 &lt;code>dart run build_runner build&lt;/code>&lt;/td>
 &lt;td>高（開發體驗）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>生成檔案膨脹&lt;/td>
 &lt;td>12 個類別產生約 20 個 &lt;code>.freezed.dart&lt;/code> / &lt;code>.g.dart&lt;/code> 檔案&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯時間&lt;/td>
 &lt;td>code generation 拖慢整體編譯&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>學習成本&lt;/td>
 &lt;td>需理解 &lt;code>part&lt;/code>、&lt;code>_$ClassName&lt;/code>、code generation 機制&lt;/td>
 &lt;td>中（新手門檻）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>版本耦合&lt;/td>
 &lt;td>freezed 3.x + json_serializable + build_runner 三者版本需相容&lt;/td>
 &lt;td>高（升級風險）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="3-適用場景判斷表">3. 適用場景判斷表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>適合 freezed&lt;/th>
 &lt;th>不需要 freezed&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>模型數量&lt;/td>
 &lt;td>50+ 個&lt;/td>
 &lt;td>&amp;lt; 20 個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>欄位變動頻率&lt;/td>
 &lt;td>頻繁新增/修改欄位&lt;/td>
 &lt;td>欄位穩定（如對應後端 struct）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Union types 需求&lt;/td>
 &lt;td>大量使用（BLoC State/Event）&lt;/td>
 &lt;td>無或極少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>巢狀 copyWith&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;/tr>
 &lt;tr>
 &lt;td>狀態管理&lt;/td>
 &lt;td>BLoC（State/Event union 是標配）&lt;/td>
 &lt;td>Riverpod（不依賴 union）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dart 版本&lt;/td>
 &lt;td>&amp;lt; 3.0（無原生 sealed class）&lt;/td>
 &lt;td>&amp;gt;= 3.0（原生 sealed class 可用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="4-替代方案比較">4. 替代方案比較&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>方案&lt;/th>
 &lt;th>描述&lt;/th>
 &lt;th>copyWith&lt;/th>
 &lt;th>== / hashCode&lt;/th>
 &lt;th>JSON&lt;/th>
 &lt;th>維護成本&lt;/th>
 &lt;th>code gen&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A：維持 freezed&lt;/td>
 &lt;td>現狀不變&lt;/td>
 &lt;td>自動&lt;/td>
 &lt;td>自動&lt;/td>
 &lt;td>自動&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>需要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B：json_serializable + Equatable&lt;/td>
 &lt;td>保留 JSON 生成，手寫 copyWith，Equatable 處理 equality&lt;/td>
 &lt;td>手寫（僅 2 個 State）&lt;/td>
 &lt;td>Equatable（零 code gen）&lt;/td>
 &lt;td>自動&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>僅 JSON&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C：完全手寫&lt;/td>
 &lt;td>移除所有 code generation&lt;/td>
 &lt;td>手寫&lt;/td>
 &lt;td>手寫&lt;/td>
 &lt;td>手寫&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>不需要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D：Dart 3 原生特性&lt;/td>
 &lt;td>使用 &lt;code>sealed class&lt;/code> + &lt;code>record&lt;/code> + &lt;code>final class&lt;/code>&lt;/td>
 &lt;td>手寫&lt;/td>
 &lt;td>record 自帶；class 需手寫或 Equatable&lt;/td>
 &lt;td>手寫或 json_serializable&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>可選&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="方案-b-詳細說明本專案推薦">方案 B 詳細說明（本專案推薦）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>JSON 序列化&lt;/strong>：保留 json_serializable（10 個模型仍需 &lt;code>fromJson&lt;/code> / &lt;code>toJson&lt;/code>），build_runner 僅用於 JSON&lt;/li>
&lt;li>&lt;strong>Value equality&lt;/strong>：使用 Equatable 套件，繼承 &lt;code>Equatable&lt;/code> 並宣告 &lt;code>props&lt;/code> 即可，零 code generation&lt;/li>
&lt;li>&lt;strong>copyWith&lt;/strong>：僅 2 個 State 類別（SessionListState、ConversationState）需要，手寫工作量極小&lt;/li>
&lt;li>&lt;strong>Immutability&lt;/strong>：使用 &lt;code>final&lt;/code> 欄位 + 命名建構子，Dart 語言層級保證&lt;/li>
&lt;/ul>
&lt;h3 id="方案-d-補充說明dart-3-原生特性">方案 D 補充說明（Dart 3 原生特性）&lt;/h3>
&lt;p>Dart 3.0+ 引入的原生特性可部分替代 freezed：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>關聯 Ticket</strong>：0.2.0-W5-007
<strong>決策結論</strong>：移除 freezed，採用 json_serializable + Equatable</p></blockquote>
<!-- 前言 -->
<p>我設定了一個新的需求開了一個專案，我沒有專門指定開發的框架或者細節，我只有很簡單的先建立我需求的 spec 文件，這個文件當然並不完整，我是希望先讓AI做一個 原形，我會在 prototype 符合我的需求動起來之後再介入去調整設計。</p>
<p>我的初始技術規範就只有我要用 flutter 去寫，所以AI就動了，但是在中間我發現 AI使用了 Freezed ，我並不喜歡在我 build 之外還要做一次
code generation 的動作，所以我就跟AI討論一次關於 Freezed 這種做法的必要性，至少在原形階段我覺得單純一點的 model 檔案沒有什麼不好，也不大會出錯，整體討論下來我選擇 捨棄 已有的 Freezed 程式碼，重構成更簡易的 版本，但是我覺得這個評估還是很有價值，所以讓AI重新整理了一次討論的內容作為備查。</p>
<hr>
<h2 id="1-freezed-是什麼">1. Freezed 是什麼</h2>
<p>Freezed 是 Dart 的自動程式碼產生（code generation）套件，專門用來幫你自動生成資料類別（data class）裡那些重複的樣板程式碼（boilerplate），包括：</p>
<ul>
<li><code>copyWith</code>：複製一份物件，但可以只改其中幾個欄位，常用在狀態管理時產生新狀態</li>
<li><code>==</code> / <code>hashCode</code>：值相等比較（value equality），讓兩個內容相同的物件被判定為「相等」</li>
<li><code>toString</code>：把物件轉成易讀的字串，方便除錯</li>
<li><code>fromJson</code> / <code>toJson</code>：JSON 序列化與反序列化，搭配 json_serializable 使用，處理前後端資料交換</li>
<li>聯合型別（Union types）/ 密封類別（sealed class）：用 <code>@freezed</code> 的多建構子語法，實現型別安全的多態模式</li>
</ul>
<p><strong>解決的核心問題</strong>：Dart 的類別預設是可變的（mutable），而且比較兩個物件時只看記憶體位址是否相同（identity equality），不會比較欄位內容。如果要手刻一個有 10 個欄位的不可變資料物件（immutable value object），大約需要 80-120 行程式碼，而且每次修改欄位都要同步更動 6 個地方（欄位宣告、建構子、<code>copyWith</code>、<code>==</code>、<code>hashCode</code>、<code>toJson</code>），非常容易漏改出錯。</p>
<hr>
<h2 id="2-優缺點分析">2. 優缺點分析</h2>
<h3 id="優點">優點</h3>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>說明</th>
          <th>受益程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>copyWith</td>
          <td>建立修改後的新實例，State 管理必備</td>
          <td>高（State 類別頻繁使用）</td>
      </tr>
      <tr>
          <td>== / hashCode</td>
          <td>Value equality，Riverpod 用於判斷狀態是否變更</td>
          <td>中</td>
      </tr>
      <tr>
          <td>fromJson / toJson</td>
          <td>JSON 序列化，WebSocket 通訊必備</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Immutability 保證</td>
          <td>編譯期強制不可變</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Union types / sealed</td>
          <td>型別安全的多態模式</td>
          <td>視需求（本專案未使用）</td>
      </tr>
  </tbody>
</table>
<h3 id="缺點">缺點</h3>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>說明</th>
          <th>影響程度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>build_runner 依賴</td>
          <td>每次改模型需執行 <code>dart run build_runner build</code></td>
          <td>高（開發體驗）</td>
      </tr>
      <tr>
          <td>生成檔案膨脹</td>
          <td>12 個類別產生約 20 個 <code>.freezed.dart</code> / <code>.g.dart</code> 檔案</td>
          <td>中</td>
      </tr>
      <tr>
          <td>編譯時間</td>
          <td>code generation 拖慢整體編譯</td>
          <td>中</td>
      </tr>
      <tr>
          <td>學習成本</td>
          <td>需理解 <code>part</code>、<code>_$ClassName</code>、code generation 機制</td>
          <td>中（新手門檻）</td>
      </tr>
      <tr>
          <td>版本耦合</td>
          <td>freezed 3.x + json_serializable + build_runner 三者版本需相容</td>
          <td>高（升級風險）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="3-適用場景判斷表">3. 適用場景判斷表</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>適合 freezed</th>
          <th>不需要 freezed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型數量</td>
          <td>50+ 個</td>
          <td>&lt; 20 個</td>
      </tr>
      <tr>
          <td>欄位變動頻率</td>
          <td>頻繁新增/修改欄位</td>
          <td>欄位穩定（如對應後端 struct）</td>
      </tr>
      <tr>
          <td>Union types 需求</td>
          <td>大量使用（BLoC State/Event）</td>
          <td>無或極少</td>
      </tr>
      <tr>
          <td>巢狀 copyWith</td>
          <td>深層巢狀物件需逐層複製</td>
          <td>結構扁平</td>
      </tr>
      <tr>
          <td>團隊規模</td>
          <td>多人協作，需統一生成減少出錯</td>
          <td>小團隊或個人</td>
      </tr>
      <tr>
          <td>狀態管理</td>
          <td>BLoC（State/Event union 是標配）</td>
          <td>Riverpod（不依賴 union）</td>
      </tr>
      <tr>
          <td>Dart 版本</td>
          <td>&lt; 3.0（無原生 sealed class）</td>
          <td>&gt;= 3.0（原生 sealed class 可用）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="4-替代方案比較">4. 替代方案比較</h2>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>描述</th>
          <th>copyWith</th>
          <th>== / hashCode</th>
          <th>JSON</th>
          <th>維護成本</th>
          <th>code gen</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A：維持 freezed</td>
          <td>現狀不變</td>
          <td>自動</td>
          <td>自動</td>
          <td>自動</td>
          <td>低</td>
          <td>需要</td>
      </tr>
      <tr>
          <td>B：json_serializable + Equatable</td>
          <td>保留 JSON 生成，手寫 copyWith，Equatable 處理 equality</td>
          <td>手寫（僅 2 個 State）</td>
          <td>Equatable（零 code gen）</td>
          <td>自動</td>
          <td>中</td>
          <td>僅 JSON</td>
      </tr>
      <tr>
          <td>C：完全手寫</td>
          <td>移除所有 code generation</td>
          <td>手寫</td>
          <td>手寫</td>
          <td>手寫</td>
          <td>高</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>D：Dart 3 原生特性</td>
          <td>使用 <code>sealed class</code> + <code>record</code> + <code>final class</code></td>
          <td>手寫</td>
          <td>record 自帶；class 需手寫或 Equatable</td>
          <td>手寫或 json_serializable</td>
          <td>中</td>
          <td>可選</td>
      </tr>
  </tbody>
</table>
<h3 id="方案-b-詳細說明本專案推薦">方案 B 詳細說明（本專案推薦）</h3>
<ul>
<li><strong>JSON 序列化</strong>：保留 json_serializable（10 個模型仍需 <code>fromJson</code> / <code>toJson</code>），build_runner 僅用於 JSON</li>
<li><strong>Value equality</strong>：使用 Equatable 套件，繼承 <code>Equatable</code> 並宣告 <code>props</code> 即可，零 code generation</li>
<li><strong>copyWith</strong>：僅 2 個 State 類別（SessionListState、ConversationState）需要，手寫工作量極小</li>
<li><strong>Immutability</strong>：使用 <code>final</code> 欄位 + 命名建構子，Dart 語言層級保證</li>
</ul>
<h3 id="方案-d-補充說明dart-3-原生特性">方案 D 補充說明（Dart 3 原生特性）</h3>
<p>Dart 3.0+ 引入的原生特性可部分替代 freezed：</p>
<table>
  <thead>
      <tr>
          <th>Dart 3 特性</th>
          <th>替代 freezed 功能</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sealed class</code></td>
          <td>Union types / when / switch</td>
          <td>不自動生成 copyWith、==</td>
      </tr>
      <tr>
          <td><code>final class</code></td>
          <td>Immutability 保證</td>
          <td>不自動生成 boilerplate</td>
      </tr>
      <tr>
          <td>Records <code>(int, String)</code></td>
          <td>輕量 value type（自帶 ==）</td>
          <td>無命名欄位語法糖有限</td>
      </tr>
      <tr>
          <td>Pattern matching</td>
          <td>exhaustive switch</td>
          <td>僅用於控制流，不生成程式碼</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="5-與狀態管理框架的關係">5. 與狀態管理框架的關係</h2>
<h3 id="riverpod-的-value-equality-機制">Riverpod 的 Value Equality 機制</h3>
<p><strong>常見誤解</strong>：「Riverpod 需要 freezed 才能正確判斷狀態變更」。</p>
<p><strong>事實釐清</strong>：</p>
<ol>
<li>Dart 預設是 <strong>identity equality</strong>（比較記憶體位址）。兩個欄位完全相同的新物件，<code>==</code> 仍為 <code>false</code></li>
<li>Riverpod 在 <code>state = newValue</code> 時使用 <code>==</code> 判斷是否通知 listener rebuild。相同則不通知</li>
<li>Riverpod <strong>本身不做任何額外 equality 優化</strong>，完全依賴物件自身的 <code>==</code> 運算子</li>
</ol>
<h3 id="此專案的實際影響">此專案的實際影響</h3>
<p>在本專案中，不使用 value equality 的影響極小：</p>
<table>
  <thead>
      <tr>
          <th>因素</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>狀態更新來源</td>
          <td>每次都是收到 WebSocket 新訊息才更新，值幾乎必然不同</td>
      </tr>
      <tr>
          <td>AsyncData 包裝</td>
          <td>Riverpod 的 <code>AsyncData</code> 每次都是新實例，外層已經不等</td>
      </tr>
      <tr>
          <td>UI rebuild 成本</td>
          <td>Flutter 本身的 Widget diff 機制已足夠高效，多餘 rebuild 不構成效能問題</td>
      </tr>
  </tbody>
</table>
<p><strong>結論</strong>：Equatable 零 code generation 即可解決 value equality 需求。在本專案場景下，甚至完全不處理也感受不到效能差異。</p>
<hr>
<h2 id="6-決策流程">6. 決策流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">是否需要 freezed?
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">模型數量 &gt; 50?
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    +-- 是 --&gt; 強烈建議使用 freezed
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    +-- 否 ↓
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">使用 union types / sealed class?
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    +-- 大量使用 --&gt; 建議使用 freezed（或 Dart 3 sealed class）
</span></span><span class="line"><span class="ln">10</span><span class="cl">    +-- 未使用 ↓
</span></span><span class="line"><span class="ln">11</span><span class="cl">    |
</span></span><span class="line"><span class="ln">12</span><span class="cl">欄位頻繁變動?
</span></span><span class="line"><span class="ln">13</span><span class="cl">    +-- 是 --&gt; 建議使用 freezed（減少同步維護）
</span></span><span class="line"><span class="ln">14</span><span class="cl">    +-- 否 ↓
</span></span><span class="line"><span class="ln">15</span><span class="cl">    |
</span></span><span class="line"><span class="ln">16</span><span class="cl">需要深層巢狀 copyWith?
</span></span><span class="line"><span class="ln">17</span><span class="cl">    +-- 是 --&gt; 建議使用 freezed
</span></span><span class="line"><span class="ln">18</span><span class="cl">    +-- 否 ↓
</span></span><span class="line"><span class="ln">19</span><span class="cl">    |
</span></span><span class="line"><span class="ln">20</span><span class="cl">需要 JSON 序列化?
</span></span><span class="line"><span class="ln">21</span><span class="cl">    +-- 是 --&gt; json_serializable 即可
</span></span><span class="line"><span class="ln">22</span><span class="cl">    +-- 否 ↓
</span></span><span class="line"><span class="ln">23</span><span class="cl">    |
</span></span><span class="line"><span class="ln">24</span><span class="cl">需要 value equality?
</span></span><span class="line"><span class="ln">25</span><span class="cl">    +-- 是 --&gt; Equatable 或手寫 ==
</span></span><span class="line"><span class="ln">26</span><span class="cl">    +-- 否 --&gt; 完全不需要 freezed</span></span></code></pre></div><hr>
<h2 id="7-本專案評估結論">7. 本專案評估結論</h2>
<h3 id="現況盤點">現況盤點</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>數量</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>@freezed</code> 類別</td>
          <td>12 個</td>
          <td>規模小</td>
      </tr>
      <tr>
          <td>資料模型（JSON）</td>
          <td>10 個</td>
          <td>SessionInfo, SessionEvent 等</td>
      </tr>
      <tr>
          <td>UI State</td>
          <td>2 個</td>
          <td>SessionListState, ConversationState</td>
      </tr>
      <tr>
          <td>Union types 使用</td>
          <td>0 個</td>
          <td>未使用 freezed 殺手功能</td>
      </tr>
      <tr>
          <td>巢狀 copyWith</td>
          <td>0 處</td>
          <td>結構扁平</td>
      </tr>
      <tr>
          <td>欄位變動頻率</td>
          <td>低</td>
          <td>對應 Go struct，後端穩定後前端不常改</td>
      </tr>
  </tbody>
</table>
<h3 id="評估對照">評估對照</h3>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>本專案狀況</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模型數量</td>
          <td>12 個（&lt; 20）</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>欄位穩定度</td>
          <td>對應 Go struct，穩定</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>Union types</td>
          <td>0 個</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>狀態管理</td>
          <td>Riverpod（非 BLoC）</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>巢狀 copyWith</td>
          <td>無</td>
          <td>不需要</td>
      </tr>
      <tr>
          <td>團隊規模</td>
          <td>小</td>
          <td>不需要</td>
      </tr>
  </tbody>
</table>
<h3 id="決策">決策</h3>
<p><strong>移除 freezed，採用方案 B</strong>：保留 json_serializable 處理 JSON 序列化，使用 Equatable 處理 value equality，手寫 copyWith（僅 2 個 State 類別）。</p>
<p><strong>理由</strong>：freezed 在本專案中只用到最基礎功能（copyWith、==、JSON），全部可被更輕量的方案替代。移除後減少 build_runner 依賴範圍、消除生成檔案膨脹、降低版本耦合風險。</p>
<hr>
<h2 id="8-遷移檢查清單">8. 遷移檢查清單</h2>
<h3 id="準備階段">準備階段</h3>
<ul>
<li><input disabled="" type="checkbox"> 確認所有 <code>@freezed</code> 類別清單（12 個）</li>
<li><input disabled="" type="checkbox"> 備份現有生成檔案</li>
<li><input disabled="" type="checkbox"> 確認 json_serializable 獨立使用的配置方式</li>
</ul>
<h3 id="資料模型遷移10-個">資料模型遷移（10 個）</h3>
<ul>
<li><input disabled="" type="checkbox"> 移除 <code>@freezed</code> 註解，改為 <code>@JsonSerializable</code> + <code>final class</code></li>
<li><input disabled="" type="checkbox"> 保留 <code>part '*.g.dart'</code>（json_serializable 仍需要）</li>
<li><input disabled="" type="checkbox"> 移除 <code>part '*.freezed.dart'</code></li>
<li><input disabled="" type="checkbox"> 繼承 <code>Equatable</code>，宣告 <code>props</code></li>
<li><input disabled="" type="checkbox"> 手寫建構子（<code>const</code> 建構子 + <code>final</code> 欄位）</li>
<li><input disabled="" type="checkbox"> 確認 <code>fromJson</code> / <code>toJson</code> 正常運作</li>
</ul>
<h3 id="ui-state-遷移2-個">UI State 遷移（2 個）</h3>
<ul>
<li><input disabled="" type="checkbox"> 同上資料模型遷移步驟</li>
<li><input disabled="" type="checkbox"> 手寫 <code>copyWith</code> 方法</li>
<li><input disabled="" type="checkbox"> 確認 Riverpod 狀態更新行為正確</li>
</ul>
<h3 id="清理階段">清理階段</h3>
<ul>
<li><input disabled="" type="checkbox"> 刪除所有 <code>.freezed.dart</code> 生成檔案</li>
<li><input disabled="" type="checkbox"> 從 <code>pubspec.yaml</code> 移除 <code>freezed</code> 和 <code>freezed_annotation</code> 依賴</li>
<li><input disabled="" type="checkbox"> 執行 <code>dart run build_runner build</code> 確認 json_serializable 正常</li>
<li><input disabled="" type="checkbox"> 執行全量測試確認無回歸</li>
<li><input disabled="" type="checkbox"> <code>dart analyze</code> 0 issues</li>
</ul>
<hr>
<h2 id="參考資源">參考資源</h2>
<ul>
<li><a href="https://pub.dev/packages/freezed">freezed 套件</a></li>
<li><a href="https://pub.dev/packages/json_serializable">json_serializable 套件</a></li>
<li><a href="https://pub.dev/packages/equatable">equatable 套件</a></li>
<li><a href="https://dart.dev/language/patterns">Dart 3 Patterns and Sealed Classes</a></li>
</ul>
<hr>
<p><strong>Last Updated</strong>: 2026-03-26
<strong>Version</strong>: 1.0.0</p>
]]></content:encoded></item></channel></rss>