<?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>Documentation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/documentation/</link><description>Recent content in Documentation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 05 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/documentation/index.xml" rel="self" type="application/rss+xml"/><item><title>Pattern：明示語意縮小（不承諾全集）</title><link>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 E。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援推進 query (A 不可行)&lt;/li>
&lt;li>不能控 build pipeline (C 不可行)&lt;/li>
&lt;li>Match 稀疏、自動續抓會拉爆 (B 不可行)&lt;/li>
&lt;li>工程量限制、做不了 #62 誠實 UX 的三數字&lt;/li>
&lt;li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 一次給完整 dataset（沒有 subset、不需要縮小）&lt;/li>
&lt;li>使用者預期 filter 是「全集」、無法接受縮小&lt;/li>
&lt;li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別&lt;/h2>
&lt;p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>D（誠實 UX）&lt;/th>
 &lt;th>E（明示語意縮小）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>範圍訊號&lt;/td>
 &lt;td>即時數字（已掃 N / 命中 K）&lt;/td>
 &lt;td>文字描述（一次性告知）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 顯眼度&lt;/td>
 &lt;td>高 — 每次都看得到&lt;/td>
 &lt;td>低 — 看一次就過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程量&lt;/td>
 &lt;td>中 — 要實作三數字&lt;/td>
 &lt;td>低 — 改文字 / 加 docstring&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者參與&lt;/td>
 &lt;td>點「再掃一批」續抓&lt;/td>
 &lt;td>不續抓、自己判斷要不要 load more&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合&lt;/td>
 &lt;td>filter 是主要互動模式&lt;/td>
 &lt;td>filter 是次要功能、原型期&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。&lt;/p>
&lt;hr>
&lt;h2 id="明示的具體做法">「明示」的具體做法&lt;/h2>
&lt;h3 id="ui-明示">UI 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span> &lt;span class="na">placeholder&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Filter loaded results...&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">small&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>只在已載入的結果裡篩選。要看更多請先載入更多。&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">small&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。&lt;/p>
&lt;h3 id="api-contract-明示">API contract 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Filter loaded results by predicate.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="cm"> * NOTE: Operates on currently loaded subset only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="cm"> * dataset, use {@link searchAll} instead.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">filterLoaded&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">predicate&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>: &lt;span class="kt">Item&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kr">boolean&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Item&lt;/span>&lt;span class="p">[];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。&lt;/p>
&lt;h3 id="docstring--readme-明示">Docstring / README 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## Filter behavior
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="sb">`filter()`&lt;/span> only operates on results currently loaded in client.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">If the source uses pagination, items not yet loaded are NOT included.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">For full-dataset filtering, the source must support server-side filter.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>文件級的明示、給開發者讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 E。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援推進 query (A 不可行)</li>
<li>不能控 build pipeline (C 不可行)</li>
<li>Match 稀疏、自動續抓會拉爆 (B 不可行)</li>
<li>工程量限制、做不了 #62 誠實 UX 的三數字</li>
<li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 一次給完整 dataset（沒有 subset、不需要縮小）</li>
<li>使用者預期 filter 是「全集」、無法接受縮小</li>
<li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）</li>
</ul>
<hr>
<h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別</h2>
<p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>D（誠實 UX）</th>
          <th>E（明示語意縮小）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍訊號</td>
          <td>即時數字（已掃 N / 命中 K）</td>
          <td>文字描述（一次性告知）</td>
      </tr>
      <tr>
          <td>UI 顯眼度</td>
          <td>高 — 每次都看得到</td>
          <td>低 — 看一次就過</td>
      </tr>
      <tr>
          <td>工程量</td>
          <td>中 — 要實作三數字</td>
          <td>低 — 改文字 / 加 docstring</td>
      </tr>
      <tr>
          <td>使用者參與</td>
          <td>點「再掃一批」續抓</td>
          <td>不續抓、自己判斷要不要 load more</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>filter 是主要互動模式</td>
          <td>filter 是次要功能、原型期</td>
      </tr>
  </tbody>
</table>
<p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。</p>
<hr>
<h2 id="明示的具體做法">「明示」的具體做法</h2>
<h3 id="ui-明示">UI 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter loaded results...&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">small</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;hint&#34;</span><span class="p">&gt;</span>只在已載入的結果裡篩選。要看更多請先載入更多。<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。</p>
<h3 id="api-contract-明示">API contract 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter loaded results by predicate.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> * NOTE: Operates on currently loaded subset only.
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="cm"> * dataset, use {@link searchAll} instead.
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kd">function</span> <span class="nx">filterLoaded</span><span class="p">(</span><span class="nx">predicate</span><span class="o">:</span> <span class="p">(</span><span class="nx">item</span>: <span class="kt">Item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">boolean</span><span class="p">)</span><span class="o">:</span> <span class="nx">Item</span><span class="p">[];</span></span></span></code></pre></div><p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。</p>
<h3 id="docstring--readme-明示">Docstring / README 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## Filter behavior
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sb">`filter()`</span> only operates on results currently loaded in client.
</span></span><span class="line"><span class="ln">4</span><span class="cl">If the source uses pagination, items not yet loaded are NOT included.
</span></span><span class="line"><span class="ln">5</span><span class="cl">For full-dataset filtering, the source must support server-side filter.</span></span></code></pre></div><p>文件級的明示、給開發者讀。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1silent-縮小不告訴">反例 1：Silent 縮小（不告訴）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter results...&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter results」沒指明「only loaded」 — 使用者預設是全集 filter、實際是 subset → 撞回 #55 <a href="../view-layer-filter-vs-source-layer/">層錯位</a> 的語意縫。</p>
<h3 id="反例-2明示位置使用者看不到">反例 2：明示位置使用者看不到</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter results.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> * Note: subset only.
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> */</span></span></span></code></pre></div><p>使用者只看 UI、不讀 docstring — 「明示」要在使用者會看到的位置（UI hint、tooltip、行為描述）。</p>
<h3 id="反例-3明示但不清楚">反例 3：明示但不清楚</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">small</span><span class="p">&gt;</span>限定範圍篩選<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「限定範圍」太抽象、沒說明是什麼範圍。要寫具體：「已載入的 N 筆內」「不包含尚未載入的」。</p>
<hr>
<h2 id="何時-e-升級到-d">何時 E 升級到 D</h2>
<p>當以下任一觸發、把 E 升級到 D（誠實 UX 三數字）：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者依然誤以為是全集 filter</td>
          <td>升 D — 文字明示不夠</td>
      </tr>
      <tr>
          <td>Filter 後 0 筆的情境變常見</td>
          <td>升 D — 三數字能 disambiguate</td>
      </tr>
      <tr>
          <td>Filter 變主要互動模式（不再是次要功能）</td>
          <td>升 D — 顯眼度需要拉高</td>
      </tr>
      <tr>
          <td>Match 密度高、續抓 ROI 變正</td>
          <td>升 B（自動續抓）</td>
      </tr>
  </tbody>
</table>
<p>E 是「成本低的退路」、不是長期解。當需求成熟、應該升級到 D / A / C。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>E 是策略順序 A → C → B → D 之外的「最後退路」</li>
<li>E 跟 D 都是「在 subset 上做」、差別在告知方式</li>
<li>E 跟 #55 silent 反模式的差別：<strong>E 是 explicit 縮小、silent 是 implicit 縮小</strong></li>
</ul>
<p>選擇順序（重申）：<strong>A 推進 → C 多 index → B 自動續抓 → D 誠實 UX → E 明示縮小 → silent（反模式）</strong></p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援、工程量做不了 D</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Filter 行為已決定是 subset、但 UI 沒寫</td>
          <td>補 UI hint</td>
      </tr>
      <tr>
          <td>API 沒 docstring 說明 filter 範圍</td>
          <td>補 docstring</td>
      </tr>
      <tr>
          <td>使用者反映「filter 結果跟我想的不一樣」</td>
          <td>E 沒成功、升級到 D 或 A</td>
      </tr>
      <tr>
          <td>內心 OS：「反正 subset 就是 subset、寫了也沒人看」</td>
          <td>停 — silent 縮小是 #55 反模式</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能接受語意縮小是可以、但必須明示。Silent 縮小（沒告知就 subset）等於 #55 層錯位、是反模式。E 的價值在「明示」這個動作、不在「subset」這個事實。</p>
]]></content:encoded></item><item><title>Commit message vs source code doc：兩份不同職責的文件</title><link>https://tarrragon.github.io/blog/record/commit-message-vs-source-code-doc%E5%85%A9%E4%BB%BD%E4%B8%8D%E5%90%8C%E8%81%B7%E8%B2%AC%E7%9A%84%E6%96%87%E4%BB%B6/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/commit-message-vs-source-code-doc%E5%85%A9%E4%BB%BD%E4%B8%8D%E5%90%8C%E8%81%B7%E8%B2%AC%E7%9A%84%E6%96%87%E4%BB%B6/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：source code doc 寫給「未來的讀者」，commit message 寫給「想了解過去發生什麼的考古者」。兩者是不同文件，內容該分開。
&lt;strong>設計原則&lt;/strong>：時序敏感的資訊（為什麼這次改動、考慮過什麼方案）放 commit；持續適用的資訊（當前契約、不變量）放 source。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 反模式 3「過去式 doc」的展開——把「source 跟 commit message 的時序職責邊界」拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點兩份文件的職責容易被混在一起">起點：兩份文件的職責容易被混在一起&lt;/h2>
&lt;p>Source code doc 的職責是「描述當前 code 的契約跟行為」、commit message 的職責是「描述某次改動做了什麼跟為什麼做」——兩者讀者不同、時序屬性不同、本該各歸各家。實務上這兩份文件的職責經常被混在 source code doc 一處：source 變成所有歷史的垃圾桶、commit message 反而沒人認真寫。&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="c1">/// 修了 issue #123 的 race condition
&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">/// 從 v2.3 開始改用 lock-free 結構
&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">/// TODO: @alice 之後可能要改用 SkipList
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">process&lt;/span>&lt;span class="p">()&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;/code>&lt;/pre>&lt;/div>&lt;p>這段 doc 混了三類資訊：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>過去發生什麼&lt;/strong>（修了 issue #123）→ 屬於 commit message&lt;/li>
&lt;li>&lt;strong>過去做過什麼決定&lt;/strong>（v2.3 開始改用 lock-free）→ 屬於 commit message / changelog&lt;/li>
&lt;li>&lt;strong>未來可能要改什麼&lt;/strong>（TODO @alice 改用 SkipList）→ 屬於 issue tracker / TODO 系統&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>沒有一條是「未來讀者讀這份 code 需要的資訊」&lt;/strong>——三條都凍結在過去某一刻、source 卻被當成歷史快照在用。要釐清這個問題、得先想清楚兩種文件各自的讀者與時間性。&lt;/p>
&lt;hr>
&lt;h2 id="時序差異當前狀態-vs-狀態轉移">時序差異：當前狀態 vs 狀態轉移&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文件&lt;/th>
 &lt;th>描述什麼&lt;/th>
 &lt;th>寫給誰讀&lt;/th>
 &lt;th>時間性&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source code doc&lt;/td>
 &lt;td>當前 code 的契約、行為、不變量&lt;/td>
 &lt;td>即將呼叫 / 修改 code 的人&lt;/td>
 &lt;td>&lt;strong>持續適用&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commit message&lt;/td>
 &lt;td>這次改動做了什麼、為什麼做&lt;/td>
 &lt;td>想了解某個變動的考古者&lt;/td>
 &lt;td>&lt;strong>特定時間點的決定&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵差別是&lt;strong>時間性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Source code doc 描述「&lt;strong>現在&lt;/strong>這份 code 在做什麼」——只要 code 不變，doc 就持續有效&lt;/li>
&lt;li>Commit message 描述「&lt;strong>那一刻&lt;/strong>為什麼要改 code」——commit 完成的那一秒就成為歷史&lt;/li>
&lt;/ul>
&lt;p>把過去式的內容塞進 source code doc，會讓 doc 變成「凍結在某個歷史時點的快照」，而不是描述當前狀態。&lt;/p>
&lt;hr>
&lt;h2 id="該寫在-commit-message-的內容">該寫在 commit message 的內容&lt;/h2>
&lt;p>Commit message 的核心職責是回答「&lt;strong>這次改動做了什麼、為什麼做&lt;/strong>」——所有「凍結在某次提交時點」的資訊都應該住在這裡、而不是被塞進 source 變成過時快照。下面四類是最常被誤放進 source 的內容：&lt;/p>
&lt;h3 id="1-改動的動機為什麼這次要動">1. 改動的動機（為什麼這次要動）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fix: prevent double-charge on payment retry
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Payment gateway 對同一個 transaction_id 會回傳 200 但實際扣款兩次
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">（incident #4521）。在 client 端加上 idempotency_key，gateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">看到重複的 key 直接回 cached response。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「為什麼動」幾乎永遠屬於 commit message。&lt;strong>source code 只需要描述「現在的行為是什麼」，不需要解釋「過去為什麼變成這樣」&lt;/strong>——除非那個「為什麼」對未來呼叫者仍是必須知道的限制（見後面段落）。&lt;/p>
&lt;h3 id="2-評估過的替代方案why-not-x">2. 評估過的替代方案（why not X）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">refactor: replace stream with reactive value
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">考慮過三個方案：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">- A. 改成 broadcast stream：最 minimal，但保留同樣的 payload 語義模糊問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">- B. 加新 broadcast stream 平行存在：兩條 stream 容易不同步
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">- C. 拆成 reactive value（採用）：與系統其他 service 一致、消除多訂閱問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">選 C 因為與 codebase 其他 service 風格對齊，雖然改動範圍最大。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「考慮過 A、B、C，選了 C」這類資訊對 reviewer 重要，對未來讀 code 的人多半不重要——他們看到的是 C 的結果，不關心你考慮過 A、B。&lt;strong>這類資訊屬於 commit message / PR description&lt;/strong>，不屬於 source code doc。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：source code doc 寫給「未來的讀者」，commit message 寫給「想了解過去發生什麼的考古者」。兩者是不同文件，內容該分開。
<strong>設計原則</strong>：時序敏感的資訊（為什麼這次改動、考慮過什麼方案）放 commit；持續適用的資訊（當前契約、不變量）放 source。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 反模式 3「過去式 doc」的展開——把「source 跟 commit message 的時序職責邊界」拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點兩份文件的職責容易被混在一起">起點：兩份文件的職責容易被混在一起</h2>
<p>Source code doc 的職責是「描述當前 code 的契約跟行為」、commit message 的職責是「描述某次改動做了什麼跟為什麼做」——兩者讀者不同、時序屬性不同、本該各歸各家。實務上這兩份文件的職責經常被混在 source code doc 一處：source 變成所有歷史的垃圾桶、commit message 反而沒人認真寫。</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="c1">/// 修了 issue #123 的 race condition
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 從 v2.3 開始改用 lock-free 結構
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// TODO: @alice 之後可能要改用 SkipList
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>這段 doc 混了三類資訊：</p>
<ol>
<li><strong>過去發生什麼</strong>（修了 issue #123）→ 屬於 commit message</li>
<li><strong>過去做過什麼決定</strong>（v2.3 開始改用 lock-free）→ 屬於 commit message / changelog</li>
<li><strong>未來可能要改什麼</strong>（TODO @alice 改用 SkipList）→ 屬於 issue tracker / TODO 系統</li>
</ol>
<p><strong>沒有一條是「未來讀者讀這份 code 需要的資訊」</strong>——三條都凍結在過去某一刻、source 卻被當成歷史快照在用。要釐清這個問題、得先想清楚兩種文件各自的讀者與時間性。</p>
<hr>
<h2 id="時序差異當前狀態-vs-狀態轉移">時序差異：當前狀態 vs 狀態轉移</h2>
<table>
  <thead>
      <tr>
          <th>文件</th>
          <th>描述什麼</th>
          <th>寫給誰讀</th>
          <th>時間性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source code doc</td>
          <td>當前 code 的契約、行為、不變量</td>
          <td>即將呼叫 / 修改 code 的人</td>
          <td><strong>持續適用</strong></td>
      </tr>
      <tr>
          <td>Commit message</td>
          <td>這次改動做了什麼、為什麼做</td>
          <td>想了解某個變動的考古者</td>
          <td><strong>特定時間點的決定</strong></td>
      </tr>
  </tbody>
</table>
<p>關鍵差別是<strong>時間性</strong>：</p>
<ul>
<li>Source code doc 描述「<strong>現在</strong>這份 code 在做什麼」——只要 code 不變，doc 就持續有效</li>
<li>Commit message 描述「<strong>那一刻</strong>為什麼要改 code」——commit 完成的那一秒就成為歷史</li>
</ul>
<p>把過去式的內容塞進 source code doc，會讓 doc 變成「凍結在某個歷史時點的快照」，而不是描述當前狀態。</p>
<hr>
<h2 id="該寫在-commit-message-的內容">該寫在 commit message 的內容</h2>
<p>Commit message 的核心職責是回答「<strong>這次改動做了什麼、為什麼做</strong>」——所有「凍結在某次提交時點」的資訊都應該住在這裡、而不是被塞進 source 變成過時快照。下面四類是最常被誤放進 source 的內容：</p>
<h3 id="1-改動的動機為什麼這次要動">1. 改動的動機（為什麼這次要動）</h3>





<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">fix: prevent double-charge on payment retry
</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">Payment gateway 對同一個 transaction_id 會回傳 200 但實際扣款兩次
</span></span><span class="line"><span class="ln">4</span><span class="cl">（incident #4521）。在 client 端加上 idempotency_key，gateway
</span></span><span class="line"><span class="ln">5</span><span class="cl">看到重複的 key 直接回 cached response。</span></span></code></pre></div><p>「為什麼動」幾乎永遠屬於 commit message。<strong>source code 只需要描述「現在的行為是什麼」，不需要解釋「過去為什麼變成這樣」</strong>——除非那個「為什麼」對未來呼叫者仍是必須知道的限制（見後面段落）。</p>
<h3 id="2-評估過的替代方案why-not-x">2. 評估過的替代方案（why not X）</h3>





<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">refactor: replace stream with reactive value
</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">考慮過三個方案：
</span></span><span class="line"><span class="ln">4</span><span class="cl">- A. 改成 broadcast stream：最 minimal，但保留同樣的 payload 語義模糊問題
</span></span><span class="line"><span class="ln">5</span><span class="cl">- B. 加新 broadcast stream 平行存在：兩條 stream 容易不同步
</span></span><span class="line"><span class="ln">6</span><span class="cl">- C. 拆成 reactive value（採用）：與系統其他 service 一致、消除多訂閱問題
</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">選 C 因為與 codebase 其他 service 風格對齊，雖然改動範圍最大。</span></span></code></pre></div><p>「考慮過 A、B、C，選了 C」這類資訊對 reviewer 重要，對未來讀 code 的人多半不重要——他們看到的是 C 的結果，不關心你考慮過 A、B。<strong>這類資訊屬於 commit message / PR description</strong>，不屬於 source code doc。</p>
<h3 id="3-migration--部署相關步驟">3. Migration / 部署相關步驟</h3>





<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">feat: migrate user_profile from int_id to uuid
</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">注意：
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 跑 migration 0042 之前先確認所有 client 已升到 v3.2 以上
</span></span><span class="line"><span class="ln">5</span><span class="cl">- migration 預估 2 小時（10M rows），建議週末執行
</span></span><span class="line"><span class="ln">6</span><span class="cl">- rollback：reverse migration 0042 然後 redeploy v3.1</span></span></code></pre></div><p>部署時序與步驟是當下發布動作的一部分，commit / release notes 該寫；source code 不該背這個負擔。</p>
<h3 id="4-bug-號ticket-連結incident-紀錄">4. Bug 號、ticket 連結、incident 紀錄</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fix: handle empty cart in checkout button visibility
</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">Closes <span class="c1">#1234</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">Related: incident-2026-04-12 <span class="o">(</span>button stuck enabled<span class="o">)</span></span></span></code></pre></div><p>把 ticket 號 / issue 連結寫在 commit message，git blame 出來的 commit 直接帶你去原始討論。寫在 source code 反而會 outdated（issue 關了、tracker 換了、URL 改了）。</p>
<hr>
<h2 id="該寫在-source-code-doc-的內容">該寫在 source code doc 的內容</h2>
<p>Source code doc 的核心職責是描述「<strong>當前 code 的契約跟行為</strong>」——只要 code 不變、doc 就持續有效。下面四類是「持續適用」的資訊類別、屬於 source 的家：</p>
<h3 id="1-當前對外契約">1. 當前對外契約</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></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 找不到對應品項時不做事；不會拋例外。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">);</span></span></span></code></pre></div><p>這是「現在這個 function 對 caller 承諾什麼」——持續適用，跟「上週為什麼加這個 function」無關。</p>
<h3 id="2-隱性需求--必要的呼叫順序">2. 隱性需求 / 必要的呼叫順序</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">/// 必須在 [init] 之後呼叫；否則 throw `StateError`。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「呼叫順序」是當前 code 的契約限制，未來呼叫者必須遵守。屬於 source code doc。</p>
<h3 id="3-對未來讀者仍然重要的過去原因">3. 對未來讀者仍然重要的「過去原因」</h3>
<p>少數情況下，「為什麼以前這樣決定」對未來讀者<strong>仍是必要資訊</strong>——典型是「這個寫法看起來怪，但有非顯然的原因」：</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="kt">void</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">Payment</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 刻意不 retry —— payment gateway 是非冪等，retry 會造成重複扣款
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// （見 incident-2026-04-12）。失敗一律拋給上層人工處理。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="n">_gateway</span><span class="p">.</span><span class="n">charge</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這條註解兼具「歷史原因」和「持續適用的限制」——未來維護者看到這段 code 會想「為什麼沒 retry？」，這條註解防止他「順手加上」。<strong>這類兼具兩種性質的內容是少數該留在 source 的歷史相關 doc</strong>。</p>
<p>判斷標準：「未來讀者<strong>不知道這條歷史會做錯決定</strong>嗎？」</p>
<ul>
<li>是 → 留 source</li>
<li>不是 → 留 commit</li>
</ul>
<h3 id="4-不變量--invariant">4. 不變量 / invariant</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">class</span> <span class="nc">CircularBuffer</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">/// 元素數量永遠在 [0, capacity] 之間
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kt">int</span> <span class="kd">get</span> <span class="n">length</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不變量是「這個型別永遠成立的事實」，是契約的一部分，屬於 source。</p>
<hr>
<h2 id="反模式">反模式</h2>
<h3 id="反模式-1把-commit-message-內容塞進-source">反模式 1：把 commit message 內容塞進 source</h3>
<p><strong>正向概念</strong>：source code doc 描述「現在的行為」、git log 才是「歷史演進」的家。兩者各自有對應的工具（IDE 看 doc、<code>git log</code> 看演進）、各司其職就能讓兩邊都精準。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 反：寫成歷史紀錄
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 2024-01-15 加上 retry 邏輯
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 2024-03-22 改用 exponential backoff
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">/// 2024-07-08 加上 jitter 避免 thundering herd
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span> <span class="n">fetch</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</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 class="c1">// 正：source 只寫當前行為
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">/// 自動 retry 失敗的請求，使用 exponential backoff + jitter
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span> <span class="n">fetch</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="o">//</span> <span class="err">演進歷史在</span> <span class="n">git</span> <span class="n">log</span> <span class="err">看</span></span></span></code></pre></div><p>把所有歷史塞進 source 等於在 source code 重做一份 git log——但 git log 已經存在、且結構化、可搜尋、有 author / timestamp。重做一份在 source 只會 outdated（下次再加邏輯時忘了補日期就破功）、而 git log 永遠是同步的。</p>
<h3 id="反模式-2commit-message-只寫-update--fix">反模式 2：commit message 只寫 &ldquo;update&rdquo; / &ldquo;fix&rdquo;</h3>
<p><strong>正向概念</strong>：commit message 是給未來考古者的線索——<code>git blame</code> 跳到一個 commit 時、message 是讀者拿到的第一份資訊。寫得清楚、考古路徑就短；寫得模糊、考古者得繼續挖 PR / 找原作者問。</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">- update
</span></span><span class="line"><span class="ln">2</span><span class="cl">- fix
</span></span><span class="line"><span class="ln">3</span><span class="cl">- wip
</span></span><span class="line"><span class="ln">4</span><span class="cl">- final
</span></span><span class="line"><span class="ln">5</span><span class="cl">- final v2
</span></span><span class="line"><span class="ln">6</span><span class="cl">- final v2 真的</span></span></code></pre></div><p>這類 commit message 當下就沒人看得懂、半年後 <code>git blame</code> 把人帶到 message 寫 &ldquo;update&rdquo; 的 commit、等於把讀者帶到死巷。合理 commit message 的最小單位是 <code>&lt;type&gt;: &lt;one-line summary&gt;</code>、例如 <code>fix: handle empty cart in checkout</code>——一行就好、但要說清楚做了什麼。</p>
<h3 id="反模式-3source-code-doc-寫滿-todo--fixme">反模式 3：source code doc 寫滿 TODO / FIXME</h3>
<p><strong>正向概念</strong>：「想未來改但還沒改」屬於 issue tracker——issue tracker 有優先序、有 owner、有 due date、能被排程。source code 的 TODO 沒有這些屬性、會被慢慢遺忘。</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="c1">/// TODO: refactor to use streams
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// FIXME: handle null case
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// HACK: temporary workaround for issue #234
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// XXX: this is broken under high load
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">doSomething</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>這些都是「想未來改但還沒改」的事——把它們留在 source 有三個問題：</p>
<ul>
<li>TODO 在 source 不會被 prioritize（產品 / 專案管理工具看不到 source 內的 TODO）</li>
<li>FIXME 在 source 容易被忽略（讀的人會想「不是我寫的不是我的問題」）</li>
<li>HACK / XXX 警告<strong>只在第一次讀時有效</strong>、第二次讀的人會麻木</li>
</ul>
<p>問題嚴重需要立刻處理 → 開 ticket、commit fix；不嚴重可以等 → 開 backlog ticket、source 別寫。把待辦項從 source 搬到 issue tracker、會被真正當成「待辦」處理。</p>
<h3 id="反模式-4把-pr-description-抄一份進-source">反模式 4：把 PR description 抄一份進 source</h3>
<p><strong>正向概念</strong>：PR description 是「這次提交的時空快照」、source code doc 是「持續適用的當前契約」。兩者描述的是同一段 code 在不同時序下的不同切面、各自有對應的家。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 這個 function 是為了支援新的 multi-currency 結帳流程。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 詳細需求見 PR #4521 與設計文件 https://wiki.../...
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 業務需求：客戶可以混合多幣別商品結帳，結帳當下統一換算成 settlement currency。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// QA 已驗證 5 種主要幣別組合 + 邊界 case。
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">multiCurrencyCheckout</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>PR description 該寫的內容（業務脈絡、設計連結、QA 範圍）抄進 source、會讓 source 凍結在「<strong>這次新增時的時空狀態</strong>」——半年後 PR 已經是歷史、連結可能失效、QA 範圍可能擴展、但 source 還停在那一刻。PR description 留在 PR、source 只寫 function 當前的對外契約。</p>
<hr>
<h2 id="git-blame-archaeology-workflow">Git blame archaeology workflow</h2>
<p>當 source code doc 跟 commit message 各司其職時，<strong>考古工作流</strong>會變得清晰：</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">讀者看到一段 code 不懂為什麼這樣寫
</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">先看 source code doc
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">不夠 → 跑 git blame
</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">找到引入這段 code 的 commit
</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">讀 commit message
</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">不夠 → 點進去看完整 PR / issue</span></span></code></pre></div><p>這個工作流要能順利跑，前提是：</p>
<ol>
<li><strong>commit 顆粒度合理</strong>——一個 commit 一個邏輯改動，不要「fix typo + refactor + add feature」混在一起，否則 blame 出來看到一個改 50 個檔案的 commit，message 寫 &ldquo;stuff&rdquo;，等於沒線索</li>
<li><strong>commit message 寫清楚動機</strong>——不是「changed X」（git diff 看得出來），而是「changed X <strong>because Y</strong>」</li>
<li><strong>重大決定用 PR 描述補充</strong>——commit message 太長不適合塞長文，PR description 是放長文的地方</li>
</ol>
<p>如果這三點做到，未來讀 code 的人有一條清楚的考古路徑，不必逼 source code doc 背所有歷史。</p>
<hr>
<h2 id="一個分配工具">一個分配工具</h2>
<p>決定一條資訊放哪時，問三個問題：</p>
<ol>
<li><strong>「未來讀者不知道這條會做錯決定嗎？」</strong>
<ul>
<li>是 → source code doc</li>
<li>不是 → commit message</li>
</ul>
</li>
<li><strong>「這條描述的是當前的行為，還是某次轉移？」</strong>
<ul>
<li>當前行為 → source code doc</li>
<li>某次轉移 → commit message</li>
</ul>
</li>
<li><strong>「Code 改了，這條會不會 outdated？」</strong>
<ul>
<li>不會（描述當前狀態）→ source code doc</li>
<li>會（描述特定時間點）→ commit message</li>
</ul>
</li>
</ol>
<p>三個問題收斂到同一個直覺：<strong>「凍結在過去」屬於 commit、「持續適用」屬於 source</strong>。</p>
<hr>
<h2 id="邊界什麼時候-source-還是該帶歷史脈絡">邊界：什麼時候 source 還是該帶歷史脈絡</h2>
<p>「歷史進 commit、契約進 source」是預設、<strong>但有些情境 source 還是該保留歷史脈絡</strong>——共通特徵是「未來讀者不知道這段歷史會做錯決定」：</p>
<ul>
<li><strong>看似怪、但有非顯然原因的寫法</strong>：「刻意不 retry、payment gateway 是非冪等」——下個維護者順手加 retry 會出事</li>
<li><strong>跟非預期外部行為對齊的 workaround</strong>：「拆兩步 query 避開 SQLite 32-bit Android 的 integer overflow（issue #1234）」——讀者重構時會想「為什麼不一次查」</li>
<li><strong>保留某段 code 的合規 / 法務原因</strong>：「依 GDPR 留 30 天可恢復、不是直接刪」——縮短到 7 天會違反法規</li>
<li><strong>效能調優的非顯然參數</strong>：「batch size = 32 是 production 跑出來的甜蜜點、改大會 OOM」——下次 review 看到「為什麼不開大」時得知道過去的實驗結果</li>
</ul>
<p>判斷標準：「未來讀者<strong>不知道這條歷史就會做錯決定</strong>嗎？」答「是」就留在 source、答「不是」就留在 commit。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>Source code doc 寫給「<strong>正要動這段 code 的人</strong>」、commit message 寫給「<strong>想知道為什麼當初這樣寫的人</strong>」。</p></blockquote>
<p>寫東西之前先問：我寫這段，是要幫<strong>正要動 code 的人</strong>做對決定，還是要幫<strong>回顧歷史的人</strong>理解某次改動？兩個讀者要找的資訊不同，分成兩處寫，雙方都受惠。</p>
<hr>
<h2 id="收束兩份文件協同源頭就要分清楚">收束：兩份文件協同，源頭就要分清楚</h2>
<p>很多團隊抱怨「source code doc 太亂、commit message 沒人寫」，本質是這兩份文件的職責沒分清楚。Source 想包辦所有事就會充滿過時內容；commit message 沒人寫是因為「反正歷史會寫進 source」變成預設。</p>
<p>把兩者的職責分清楚，兩份文件都會變健康：</p>
<ul>
<li><strong>source 變短、變精準</strong>：只寫當前契約，doc 不會 outdated</li>
<li><strong>commit message 被認真寫</strong>：因為它是某些資訊的唯一家</li>
<li><strong>考古路徑清楚</strong>：blame → commit → PR 是可預期的回溯路徑</li>
</ul>
<p>寫 doc / 寫 commit 是同一個技能的兩面。不要把任何一邊當成另一邊的替代品。</p>
]]></content:encoded></item><item><title>函式文件分層設計：型別、介面、實作各自該寫什麼</title><link>https://tarrragon.github.io/blog/record/%E5%87%BD%E5%BC%8F%E6%96%87%E4%BB%B6%E5%88%86%E5%B1%A4%E8%A8%AD%E8%A8%88%E5%9E%8B%E5%88%A5%E4%BB%8B%E9%9D%A2%E5%AF%A6%E4%BD%9C%E5%90%84%E8%87%AA%E8%A9%B2%E5%AF%AB%E4%BB%80%E9%BA%BC/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%87%BD%E5%BC%8F%E6%96%87%E4%BB%B6%E5%88%86%E5%B1%A4%E8%A8%AD%E8%A8%88%E5%9E%8B%E5%88%A5%E4%BB%8B%E9%9D%A2%E5%AF%A6%E4%BD%9C%E5%90%84%E8%87%AA%E8%A9%B2%E5%AF%AB%E4%BB%80%E9%BA%BC/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：doc 是塑造使用者決策的工具——寫不好的 doc 會反向誤導使用者選錯路。
&lt;strong>設計原則&lt;/strong>：把資訊放在能表達它的最低層次（名稱 / 型別 / 介面 doc / 實作 doc / 範例與測試）、上層留給「下層表達不了的剩餘」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點doc-是塑造使用者決策的工具">起點：doc 是塑造使用者決策的工具&lt;/h2>
&lt;p>API 設計者常忽略一件事：&lt;strong>文件本身會塑造使用者的決策&lt;/strong>——讀者依照 doc 給的資訊選預設值、選呼叫方式、選用途，所以 doc 寫不好就會反向誤導使用者選錯路。&lt;/p>
&lt;p>幾種常見的誤導模式：&lt;/p>
&lt;ul>
&lt;li>把「需要明確選擇」的東西做成「最少打字的預設」（例如某些 stream / channel API 預設是單訂閱、多數 SQL column 預設 nullable）——使用者讀不到「該選什麼」的資訊，跟著預設走就出包&lt;/li>
&lt;li>註解重複型別已說明的事，反而讓讀者懷疑「型別是不是不夠精確」&lt;/li>
&lt;li>介面 doc 描述「目前實作怎麼做」而非「契約承諾什麼」——讓未來新實作以為要照抄&lt;/li>
&lt;li>用憑想像的業務動機補完，後人讀了當真，反向影響其他相關決策&lt;/li>
&lt;/ul>
&lt;p>這些問題不是「沒寫 doc」，而是「&lt;strong>寫了誤導的 doc&lt;/strong>」。要寫出不誤導的 doc，得先想清楚每個位置該放什麼資訊。&lt;/p>
&lt;hr>
&lt;h2 id="設計原則資訊應該存在最低能表達它的層次">設計原則：資訊應該存在最低能表達它的層次&lt;/h2>
&lt;p>讀者讀一個 function 的閱讀順序：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>看簽章&lt;/strong>（名稱、參數、回傳型別）&lt;/li>
&lt;li>&lt;strong>讀 doc comment&lt;/strong>&lt;/li>
&lt;li>&lt;strong>跳進實作&lt;/strong>&lt;/li>
&lt;li>&lt;strong>找範例 / 測試&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>每往下一層，閱讀成本就高一級。設計 doc 的原則：&lt;/p>
&lt;blockquote>
&lt;p>能用上層表達的資訊，就不要往下層放。&lt;/p>&lt;/blockquote>
&lt;p>對應的職責劃分：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>該裝什麼&lt;/th>
 &lt;th>反例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>名稱&lt;/td>
 &lt;td>動詞 / 動作意圖&lt;/td>
 &lt;td>&lt;code>getData()&lt;/code>、&lt;code>process()&lt;/code>、&lt;code>handle()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>型別簽章&lt;/td>
 &lt;td>輸入合法範圍、回傳保證&lt;/td>
 &lt;td>&lt;code>int qty&lt;/code>（允許負數）、&lt;code>String?&lt;/code> 沒指明何時為 null&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>介面 doc&lt;/td>
 &lt;td>契約承諾、所有實作都要遵守的行為&lt;/td>
 &lt;td>描述當前實作流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實作 doc&lt;/td>
 &lt;td>實作特有的 invariant、bug workaround&lt;/td>
 &lt;td>重複介面契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範例 / 測試&lt;/td>
 &lt;td>抽象描述失敗的複雜用法&lt;/td>
 &lt;td>取代正常 doc&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把資訊放在能表達它的最低層次，能讓上層 doc 更精簡、更精準。&lt;/p>
&lt;hr>
&lt;h2 id="layer-1名稱與型別簽章">Layer 1：名稱與型別簽章&lt;/h2>
&lt;p>&lt;strong>強型別語言下，型別是文件的一部分&lt;/strong>。很多 doc 內容本來就該由型別承擔。&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="c1">// 弱：依賴 doc 警告
&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">/// [quantity] 必須為正整數
&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="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="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">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：型別本身就限制
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PositiveInt&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&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;/code>&lt;/pre>&lt;/div>




&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">// 弱：String flag，靠 doc 說明可選值
&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">/// [mode] 可選值：&amp;#39;manual&amp;#39;, &amp;#39;auto&amp;#39;, &amp;#39;hybrid&amp;#39;
&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="kt">void&lt;/span> &lt;span class="n">setMode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&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">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：用 enum
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">enum&lt;/span> &lt;span class="n">Mode&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">manual&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">auto&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">hybrid&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="kt">void&lt;/span> &lt;span class="n">setMode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Mode&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&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;/code>&lt;/pre>&lt;/div>&lt;p>當型別能表達約束時，&lt;strong>不要用 doc 重複表達&lt;/strong>——doc 是約束的弱形式（編譯不檢查、IDE 補全不提示），把 doc 當主要 enforcement 等於放棄型別系統的力氣。&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="c1">// 弱：positional argument，靠 doc 解釋
&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">/// [a] 是基準值，[b] 是新值
&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="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&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">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：named argument 自說明
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="kd">required&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">from&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">required&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">to&lt;/span>&lt;span class="p">})&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>update(from: 5, to: 10)&lt;/code> 的呼叫端比 &lt;code>update(5, 10)&lt;/code> 清楚得多，且&lt;strong>不需要任何 doc&lt;/strong>。&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="c1">// 弱：可能失敗，靠 doc 說「失敗時回傳 null」
&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">/// 找不到時回傳 null
&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="n">User&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&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">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：型別本身表達 optionality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&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">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 更強：分清 null 跟 error
&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">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">NotFoundError&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&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;/code>&lt;/pre>&lt;/div>&lt;p>簽章已經表達清楚的事，doc 不必再寫。&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="c1">// 弱：implementation-leaking 命名
&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="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Item&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getCachedItems&lt;/span>&lt;span class="p">()&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">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：意圖命名
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Item&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getItems&lt;/span>&lt;span class="p">()&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;/code>&lt;/pre>&lt;/div>&lt;p>「Cached」這個字洩漏實作（用了 cache）。如果之後改成不 cache，名字就要改、所有 caller 也要改——但&lt;strong>業務語義並沒變&lt;/strong>。命名應該反映「呼叫者想要什麼」，不是「實作怎麼做」。&lt;/p>
&lt;blockquote>
&lt;p>展開閱讀：&lt;a href="../types-replacing-docs/">型別取代 doc 的收益曲線&lt;/a>——整理 null safety / enum / wrapper / Result / typestate 各自能消除哪類 doc、以及型別表達不了的剩餘部分（業務動機、性能、副作用、時序契約）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：doc 是塑造使用者決策的工具——寫不好的 doc 會反向誤導使用者選錯路。
<strong>設計原則</strong>：把資訊放在能表達它的最低層次（名稱 / 型別 / 介面 doc / 實作 doc / 範例與測試）、上層留給「下層表達不了的剩餘」。</p></blockquote>
<hr>
<h2 id="起點doc-是塑造使用者決策的工具">起點：doc 是塑造使用者決策的工具</h2>
<p>API 設計者常忽略一件事：<strong>文件本身會塑造使用者的決策</strong>——讀者依照 doc 給的資訊選預設值、選呼叫方式、選用途，所以 doc 寫不好就會反向誤導使用者選錯路。</p>
<p>幾種常見的誤導模式：</p>
<ul>
<li>把「需要明確選擇」的東西做成「最少打字的預設」（例如某些 stream / channel API 預設是單訂閱、多數 SQL column 預設 nullable）——使用者讀不到「該選什麼」的資訊，跟著預設走就出包</li>
<li>註解重複型別已說明的事，反而讓讀者懷疑「型別是不是不夠精確」</li>
<li>介面 doc 描述「目前實作怎麼做」而非「契約承諾什麼」——讓未來新實作以為要照抄</li>
<li>用憑想像的業務動機補完，後人讀了當真，反向影響其他相關決策</li>
</ul>
<p>這些問題不是「沒寫 doc」，而是「<strong>寫了誤導的 doc</strong>」。要寫出不誤導的 doc，得先想清楚每個位置該放什麼資訊。</p>
<hr>
<h2 id="設計原則資訊應該存在最低能表達它的層次">設計原則：資訊應該存在最低能表達它的層次</h2>
<p>讀者讀一個 function 的閱讀順序：</p>
<ol>
<li><strong>看簽章</strong>（名稱、參數、回傳型別）</li>
<li><strong>讀 doc comment</strong></li>
<li><strong>跳進實作</strong></li>
<li><strong>找範例 / 測試</strong></li>
</ol>
<p>每往下一層，閱讀成本就高一級。設計 doc 的原則：</p>
<blockquote>
<p>能用上層表達的資訊，就不要往下層放。</p></blockquote>
<p>對應的職責劃分：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>該裝什麼</th>
          <th>反例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>名稱</td>
          <td>動詞 / 動作意圖</td>
          <td><code>getData()</code>、<code>process()</code>、<code>handle()</code></td>
      </tr>
      <tr>
          <td>型別簽章</td>
          <td>輸入合法範圍、回傳保證</td>
          <td><code>int qty</code>（允許負數）、<code>String?</code> 沒指明何時為 null</td>
      </tr>
      <tr>
          <td>介面 doc</td>
          <td>契約承諾、所有實作都要遵守的行為</td>
          <td>描述當前實作流程</td>
      </tr>
      <tr>
          <td>實作 doc</td>
          <td>實作特有的 invariant、bug workaround</td>
          <td>重複介面契約</td>
      </tr>
      <tr>
          <td>範例 / 測試</td>
          <td>抽象描述失敗的複雜用法</td>
          <td>取代正常 doc</td>
      </tr>
  </tbody>
</table>
<p>把資訊放在能表達它的最低層次，能讓上層 doc 更精簡、更精準。</p>
<hr>
<h2 id="layer-1名稱與型別簽章">Layer 1：名稱與型別簽章</h2>
<p><strong>強型別語言下，型別是文件的一部分</strong>。很多 doc 內容本來就該由型別承擔。</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="c1">// 弱：依賴 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [quantity] 必須為正整數
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <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">// 弱：String flag，靠 doc 說明可選值
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [mode] 可選值：&#39;manual&#39;, &#39;auto&#39;, &#39;hybrid&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="kt">String</span> <span class="n">mode</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：用 enum
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">Mode</span> <span class="p">{</span> <span class="n">manual</span><span class="p">,</span> <span class="n">auto</span><span class="p">,</span> <span class="n">hybrid</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="n">Mode</span> <span class="n">mode</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>當型別能表達約束時，<strong>不要用 doc 重複表達</strong>——doc 是約束的弱形式（編譯不檢查、IDE 補全不提示），把 doc 當主要 enforcement 等於放棄型別系統的力氣。</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="c1">// 弱：positional argument，靠 doc 解釋
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [a] 是基準值，[b] 是新值
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：named argument 自說明
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">({</span><span class="kd">required</span> <span class="kt">int</span> <span class="n">from</span><span class="p">,</span> <span class="kd">required</span> <span class="kt">int</span> <span class="n">to</span><span class="p">})</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p><code>update(from: 5, to: 10)</code> 的呼叫端比 <code>update(5, 10)</code> 清楚得多，且<strong>不需要任何 doc</strong>。</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="c1">// 弱：可能失敗，靠 doc 說「失敗時回傳 null」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：型別本身表達 optionality
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</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">// 更強：分清 null 跟 error
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="n">Result</span><span class="o">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">NotFoundError</span><span class="o">&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>簽章已經表達清楚的事，doc 不必再寫。</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="c1">// 弱：implementation-leaking 命名
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getCachedItems</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 強：意圖命名
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「Cached」這個字洩漏實作（用了 cache）。如果之後改成不 cache，名字就要改、所有 caller 也要改——但<strong>業務語義並沒變</strong>。命名應該反映「呼叫者想要什麼」，不是「實作怎麼做」。</p>
<blockquote>
<p>展開閱讀：<a href="../types-replacing-docs/">型別取代 doc 的收益曲線</a>——整理 null safety / enum / wrapper / Result / typestate 各自能消除哪類 doc、以及型別表達不了的剩餘部分（業務動機、性能、副作用、時序契約）。</p></blockquote>
<hr>
<h2 id="layer-2介面-doc">Layer 2：介面 doc</h2>
<p>介面 doc 是<strong>契約</strong>（contract）——對所有實作的承諾。它的讀者有兩類：</p>
<ol>
<li><strong>使用者</strong>：「我呼叫這個會發生什麼？需要注意什麼？」</li>
<li><strong>實作者</strong>（包括寫 mock、寫新版實作的人）：「我必須遵守哪些規則？」</li>
</ol>
<p>兩類讀者都不該為了讀懂契約而去讀任何單一實作。</p>
<h3 id="該寫的契約承諾行為保證隱性需求">該寫的：契約承諾、行為保證、隱性需求</h3>
<ul>
<li><strong>何時 throw / 回傳特殊值</strong>：「找不到時 throw <code>NotFoundException</code>」</li>
<li><strong>副作用</strong>：「呼叫後 <code>currentUser</code> 會被清空」</li>
<li><strong>同步 / 非同步保證</strong>：「呼叫後資料庫立即一致；快取要等下一次 refresh」</li>
<li><strong>執行順序保證</strong>：「listener 觸發順序不保證」</li>
<li><strong>業務規則</strong>（<strong>只在有實際業務需求時寫，且要有來源</strong>）：「會員價只能用 wallet 付款」</li>
</ul>
<h3 id="容易誤入介面-doc-的內容屬於型別實作或他處">容易誤入介面 doc 的內容（屬於型別、實作或他處）</h3>
<p>介面 doc 的職責是<strong>契約描述</strong>——所以「型別簽章已說的事」「特定實作怎麼做」「沒來源的業務動機」分屬其他層次（型別、實作 doc、issue tracker）、寫進介面 doc 反而稀釋契約本身的能見度。三個典型誤入：</p>
<h4 id="1-型別已表達的內容屬於型別簽章">1. 型別已表達的內容（屬於型別簽章）</h4>





<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">/// 回傳 User，找不到時為 null
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 簡：型別已說明，doc 留白或寫業務動機
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><h4 id="2-當前實作的細節屬於實作-doc">2. 當前實作的細節（屬於實作 doc）</h4>





<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">/// 內部用 HashMap 存儲，O(1) 查詢
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><p>實作細節寫在介面 doc 會誤導實作者「這個契約規定要用 HashMap」。如果未來有人寫一個用 B-tree 的實作，是合法的，但讀 doc 會以為違反契約。</p>
<h4 id="3-憑想像補完的業務動機屬於-issue-tracker--不寫">3. 憑想像補完的業務動機（屬於 issue tracker / 不寫）</h4>





<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">/// 為了符合 PCI-DSS 規範，這裡不能 log 完整 cardNumber
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">String</span> <span class="n">maskCardNumber</span><span class="p">(</span><span class="kt">String</span> <span class="n">cardNumber</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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">/// 回傳遮罩後字串，僅保留尾 4 碼
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kt">String</span> <span class="n">maskCardNumber</span><span class="p">(</span><span class="kt">String</span> <span class="n">cardNumber</span><span class="p">);</span></span></span></code></pre></div><p>業務動機要有來源（規範文件、PM 決策、incident 紀錄）才寫；猜的不要寫。猜的動機被當真會反向影響後續決策——讀者拿這條沒來源的猜測當依據、推到「既然是因為 PCI-DSS、那 X 也要這樣處理」、就把錯誤論述擴散到下游。</p>
<h3 id="介面-doc-越精簡越能被讀完">介面 doc 越精簡越能被讀完</h3>
<p>很多人覺得「寫得詳細才負責任」，結果介面 doc 三段五行，讀完也記不住。<strong>好的介面 doc 通常只有 2-4 行</strong>：</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="c1">/// 從本地購物車移除指定商品
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 找不到對應品項時不做事；不會拋例外。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">);</span></span></span></code></pre></div><p>第一行說 what、第二行說 edge case。寫到這就停。「指定商品」怎麼比對？無關契約，去看實作。</p>
<hr>
<h2 id="layer-3實作-doc">Layer 3：實作 doc</h2>
<p>實作 doc 的職責跟介面 doc<strong>完全不同</strong>：</p>
<ul>
<li><strong>介面 doc</strong>：對外契約，所有實作共通</li>
<li><strong>實作 doc</strong>：這個實作特有的細節</li>
</ul>
<h3 id="該寫的實作特有的-invariantworkaroundtradeoff">該寫的：實作特有的 invariant、workaround、tradeoff</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">// 該寫：實作特有的 invariant
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kt">void</span> <span class="n">increaseItemQuantity</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="c1">// 順序關鍵：先 set lastChangedItem 再動 list，
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="c1">// 因為訂閱 localCartItems 的 worker 會在 list 變動時讀 lastChangedItem
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="n">lastChangedItem</span><span class="p">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">item</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="n">localCartItems</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 該寫：bug workaround
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// Workaround for SQLite issue #1234: integer overflow on 32-bit Android,
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// 拆成兩步 query 避開
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">ids</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">db</span><span class="p">.</span><span class="n">rawQuery</span><span class="p">(</span><span class="s1">&#39;SELECT id FROM ...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">return</span> <span class="kd">await</span> <span class="n">db</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s1">&#39;items&#39;</span><span class="p">,</span> <span class="nl">where:</span> <span class="s1">&#39;id IN (</span><span class="si">${</span><span class="n">ids</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span><span class="si">}</span><span class="s1">)&#39;</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">// 該寫：性能 tradeoff
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 用 LinkedHashMap 而非普通 Map：插入 1k 次後查詢效能差 3-5 倍
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">cache</span> <span class="o">=</span> <span class="n">LinkedHashMap</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="n">Item</span><span class="o">&gt;</span><span class="p">();</span></span></span></code></pre></div><p>這些都是**讀實作 code 也看不出「為什麼要這樣」**的決定，需要 doc 解釋。</p>
<h3 id="契約只寫一處實作不重複介面已寫的規則">契約只寫一處：實作不重複介面已寫的規則</h3>
<p>實作 doc 的職責跟介面 doc 互補——契約描述歸介面層、實作層只補「該實作的特殊性」。同一條契約規則寫第二次（在實作層複述介面已寫的承諾）會破壞「契約只寫一次」原則：規則改的時候要同步兩處、少改一處就出現自相矛盾的文件、讀者看到也分不清以哪份為準。</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="c1">// 不該寫：介面 doc 已寫的規則，實作不再重複
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 移除不視為「最後變更」，不更新 lastChangedItem
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="n">localCartItems</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「移除不更新 lastChangedItem」是契約、介面層已寫。</p>
<p>如果擔心未來維護者誤以為「作者忘了寫」，留一個<strong>指向介面</strong>的最小提示比複述整條規則更安全：</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">override</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 行為見 ICartService.removeFromLocalCart
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">localCartItems</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不重複規則，只指向真相來源。</p>
<h3 id="negative-space-documentation">Negative-space documentation</h3>
<p>實作 doc 偶爾要寫「<strong>為什麼這裡刻意沒寫某段程式</strong>」。這類 doc 防的是「未來維護者順手補上」：</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="kt">void</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">Payment</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// NOTE: 這裡刻意不 retry —— payment gateway 是非冪等，
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// retry 會造成重複扣款。失敗一律拋給上層人工處理。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="n">_gateway</span><span class="p">.</span><span class="n">charge</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>沒這條註解，下個維護者看到網路 retry 是常見做法，可能會「順手加上」造成事故。</p>
<p>negative-space doc 用得好可以避免事故；用得多會變成處處防禦性註解，閱讀體驗變差。原則：<strong>這個「刻意沒做」的決定，是不是違反讀者的合理直覺？</strong> 違反才寫。</p>
<hr>
<h2 id="layer-4範例與測試">Layer 4：範例與測試</h2>
<p>複雜 API 的最後一層 doc 是<strong>可執行範例</strong>。</p>
<p>何時用 example：</p>
<ul>
<li>API 有多個正交參數，組合起來很多種用法</li>
<li>抽象描述比看程式碼難懂</li>
<li>邊界 case 用文字描述模糊（「如果 collection 是空、且 timeout 為 zero、且 retries 為 0…」）</li>
</ul>
<p>何時不用 example：</p>
<ul>
<li>API 用法只有一種、簽章已說清</li>
<li>用法跟名稱字面意義一致</li>
</ul>
<p><strong>測試也是 doc</strong>。命名好的測試比 example 更有價值——不會 outdated（測試會跑、example 不會），且涵蓋 edge case。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns null when item not in cart&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases quantity when item exists with quantity &gt; 1&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes item when quantity reaches 0&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>讀者看 function 不確定行為時，<strong>跳到對應 test file 比讀冗長 doc 快</strong>——測試案例的命名直接告訴你支援哪些 case，並且每個案例都有可執行的具體輸入輸出。</p>
<blockquote>
<p>展開閱讀：<a href="../test-naming-as-documentation/">測試命名作為文件</a>——測試是少數會自我驗證的文件、把命名寫成可執行 spec 條目就能取代不少 doc 的職責。</p></blockquote>
<hr>
<h2 id="常見反模式">常見反模式</h2>
<h3 id="反模式-1用-doc-取代不好的命名">反模式 1：用 doc 取代不好的命名</h3>
<p><strong>正向概念</strong>：命名是契約的最強形式、doc 是命名表達不了的剩餘部分的家。命名先到位、doc 才有空間寫真正重要的事。</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="c1">// 反：靠 doc 補救命名
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 處理訂單，但只在訂單狀態為 pending 時做事
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">(</span><span class="n">Order</span> <span class="n">o</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="kt">void</span> <span class="n">handlePendingOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">o</span><span class="p">);</span></span></span></code></pre></div><p>把 doc 當成命名失敗的補丁有兩個問題：(1)「需要讀 doc 才能用對」的 function 在 IDE 自動補全 / 快速瀏覽時看不到 doc、誤用機率高；(2) 命名其實沒變、別人改 code 時 doc 會跟不上、補丁本身又 outdated。「需要 doc 才能用對」通常是命名沒到位的訊號。</p>
<h3 id="反模式-2過度註解">反模式 2：過度註解</h3>
<p><strong>正向概念</strong>：doc 是稀缺資源——讀者注意力的預算有限、把 doc 留給「值得花注意力讀」的事項。</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="c1">// 反：句句都是 noise
</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">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">/// User 的 ID
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">/// User 的名字
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">/// User 的 email
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</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 class="c1">// 正：欄位名清楚就不寫
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kt">String</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「<code>User.name</code> 是 User 的名字」屬於命名已表達的訊息、寫進 doc 只是 redundant noise。整份 code 充斥這類 doc 會稀釋訊號——讀者習慣性 skip 所有 doc 之後、連真正重要的 invariant 跟 edge case 也會被一起跳過。</p>
<h3 id="反模式-3過去式-doc">反模式 3：過去式 doc</h3>
<p><strong>正向概念</strong>：source code doc 描述「<strong>現在</strong>這份 code 在做什麼」、commit message 描述「<strong>那一刻</strong>為什麼要改」。兩種讀者要找的資訊不同、各歸各的家。</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="c1">// 反：寫給歷史
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 修了 issue #123 的 race condition
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：寫給未來讀者（保留 fix 的關鍵 invariant 即可）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="c1">// 必須在持有 lock 內 call observer，避免 observer 看到中間狀態
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></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>「修了什麼 bug」凍結在過去某一刻、屬於 commit message / changelog；「目前必須持有 lock」是契約限制、屬於 source code doc。把過去式直接塞進 source 等於用 source 重做一份 git log——但 git log 已經存在、且結構化、可搜尋、有 author / timestamp。</p>
<blockquote>
<p>展開閱讀：<a href="../commit-message-vs-source-doc/">Commit message vs source code doc</a>——時序敏感的資訊（為什麼這次改、考慮過什麼方案）放 commit、持續適用的契約放 source、配合 git blame 工作流讓考古路徑清楚。</p></blockquote>
<h3 id="反模式-4同一條規則多處寫">反模式 4：同一條規則多處寫</h3>
<p><strong>正向概念</strong>：契約由介面層獨家承載、其他層引用即可。規則只有一個 SSoT（Single Source of Truth）、修改成本才可控。</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="c1">// 反：規則寫三處
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 介面：「取消訂單後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 實作：「取消後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 測試：「驗證取消後 3 天內不能重新下單」
</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></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 介面：「取消訂單後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">// 實作：（無 doc）
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="o">//</span> <span class="err">測試：</span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;cannot reorder within 3 days of cancellation&#39;</span><span class="p">)</span></span></span></code></pre></div><p>一條規則複製到三處看起來保險、但會在改規則時暴露代價：要同步修三處、漏改一處就出現自相矛盾的 doc、讀者讀到不一致的版本反而會懷疑「以哪份為準」。把規則收斂到單一介面、其他層指向（測試命名 / 實作註解 <code>// 行為見 ...</code>）就夠了。</p>
<h3 id="反模式-5把語法選擇當成-doc-內容">反模式 5：把語法選擇當成 doc 內容</h3>
<p><strong>正向概念</strong>：doc 描述業務目的跟行為契約——讀者要的是「這個 function 做什麼」、不是「為什麼用這個語法寫」。</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="c1">// 反：寫實作層次的選擇細節
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 用 Dart 3 的 record pattern destructure，比 .$1 / .$2 可讀
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">((</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="n">event</span><span class="p">)</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="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">=</span> <span class="n">event</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">...</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><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">/// 處理 (timestamp, value) 對的批次更新
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">((</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="n">event</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「為什麼用某語法」屬於 commit message / PR review 的討論記錄、不屬於 source code doc——換個語法寫法、業務行為沒變、但 doc 卻會 outdated。語法選擇的 why 在 git log / PR description 找得到、不需要 source 背這份歷史。</p>
<h3 id="反模式-6用-doc-警告使用者請別這樣用">反模式 6：用 doc 警告使用者「請別這樣用」</h3>
<p><strong>正向概念</strong>：能用型別 / API 設計禁掉的誤用、把它編進型別系統；doc 警告留給型別表達不了的使用情境（時序、跨方法 invariant、執行環境）。</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="c1">// 反：靠 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// **不要**直接修改回傳的 list，會造成內部狀態不一致
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：型別 / API 設計阻止誤用
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">List</span><span class="p">.</span><span class="n">unmodifiable</span><span class="p">(</span><span class="n">_items</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">Iterable</span> <span class="o">/</span> <span class="n">immutable</span> <span class="err">集合型別</span></span></span></code></pre></div><p>doc 警告的執行力靠使用者「願意讀並且記住」、型別約束則是編譯期強制——當失敗成本高（內部狀態被破壞）、保護機制就值得從 doc 升到型別。型別表達不了的使用情境（例如「必須在 main isolate 呼叫」）才是 doc 警告該守的範圍。</p>
<hr>
<h2 id="api-設計層面doc-之外的塑造工具">API 設計層面：doc 之外的塑造工具</h2>
<p>doc 寫得再好，<strong>API 設計本身</strong>會更直接塑造使用者行為。要讓使用者選對，從設計層下手比寫 doc 有效。</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="c1">// 預設導向受限選項：使用者忘了選通用版本就出錯
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">StreamController</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">ctrl</span> <span class="o">=</span> <span class="n">StreamController</span><span class="p">();</span>  <span class="c1">// single
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 預設導向通用選項：忘了選受限版本不會出錯
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">StreamController</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">ctrl</span> <span class="o">=</span> <span class="n">StreamController</span><span class="p">.</span><span class="n">broadcast</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="o">//</span> <span class="err">受限版本要顯式選</span> <span class="p">.</span><span class="n">singleSubscription</span><span class="p">()</span></span></span></code></pre></div><p>當預設造成的失敗成本高、失敗模式又不易察覺、把多數人實際需要的選項變成預設、能消除整類「忘了選」的事故。doc 警告的執行力靠「使用者讀到並記住」、規模一大就守不住——把保護從約定升到結構。</p>
<h3 id="把選擇從-default-取消用型別禁掉">把選擇從 default 取消（用型別禁掉）</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">// 弱：靠 doc 說「不該直接呼叫，請用 X」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">protected</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kt">void</span> <span class="n">internalMethod</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="kd">class</span> <span class="nc">_InternalImpl</span> <span class="p">{</span> <span class="kt">void</span> <span class="n">method</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">}</span></span></span></code></pre></div><p>能用 visibility / sealed / private 收掉的「請別這樣用」、把它收進型別系統——比起 doc 提示、語言層級的禁用是無條件強制的、且不會在大型重構時被遺漏。</p>
<h3 id="builder--fluent-api-取代多參數">Builder / fluent API 取代多參數</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">// 弱：positional / named 多參數，靠 doc 解釋
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">Request</span> <span class="n">build</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">,</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">String</span><span class="o">&gt;?</span> <span class="n">headers</span><span class="p">,</span> <span class="n">Body</span><span class="o">?</span> <span class="n">body</span><span class="p">,</span> <span class="kt">int</span> <span class="n">timeout</span> <span class="o">=</span> <span class="m">30</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 強：fluent API 自說明
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">Request</span><span class="p">.</span><span class="n">builder</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">.</span><span class="n">header</span><span class="p">(</span><span class="s1">&#39;Accept&#39;</span><span class="p">,</span> <span class="s1">&#39;json&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">.</span><span class="n">body</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">.</span><span class="n">timeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">(</span><span class="nl">seconds:</span> <span class="m">30</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">  <span class="p">.</span><span class="n">build</span><span class="p">();</span></span></span></code></pre></div><p>fluent API 的 method 名直接表達意圖，不需要 doc 解釋每個參數做什麼。</p>
<hr>
<h2 id="寫-function-doc-的-checklist">寫 function doc 的 checklist</h2>
<p>寫一個 function doc 前，跑這個 checklist：</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>這條資訊型別能不能表達？</strong> 能 → 改 type，不寫 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條資訊命名能不能表達？</strong> 能 → 改名，不寫 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條資訊是契約還是實作細節？</strong> 契約 → 介面 doc / 實作 → 實作 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條規則是不是已經寫在介面 doc？</strong> 是 → 實作不重複</li>
<li><input disabled="" type="checkbox"> <strong>這個業務動機有沒有來源？</strong> 沒有 → 不寫，只寫可觀察事實</li>
<li><input disabled="" type="checkbox"> <strong>這個 doc 在描述什麼時候出問題？</strong> 是 → 寫得明確（throw / null / edge case）</li>
<li><input disabled="" type="checkbox"> <strong>沒有這條 doc，讀者會誤判嗎？</strong> 不會 → 不寫</li>
<li><input disabled="" type="checkbox"> <strong>同一條規則我寫了第二次嗎？</strong> 是 → 砍一處，留一處</li>
</ul>
<p>過完 checklist 留下的 doc 通常很短——<strong>這是好現象</strong>。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>doc 是「<strong>型別、簽章、命名、結構都表達不了的剩餘資訊</strong>」的家。</p></blockquote>
<p>寫 doc 之前先問：</p>
<ul>
<li>能用型別表達嗎？</li>
<li>能用命名表達嗎？</li>
<li>能用結構（fluent API、enum、sealed class）表達嗎？</li>
</ul>
<p>三題都答「不能」、<strong>而且</strong>使用者不知道會出錯——這時才需要 doc。</p>
<p>這個原則的 corollary：<strong>型別系統越強的語言、function doc 也越能寫得短</strong>。如果發現 Dart / TypeScript / Rust 的 function doc 寫得跟 Python 一樣長、多半有東西可以下移到型別。</p>
<h3 id="何時-doc-還是該寫得詳細">何時 doc 還是該寫得詳細</h3>
<p>「能少寫就少寫」是預設、<strong>但有些情境 doc 必須寫得詳細</strong>——這些是型別跟結構覆蓋不到的場景：</p>
<ul>
<li><strong>跨方法 protocol</strong>：「呼叫 <code>reserve</code> 之後必須在 X 內呼叫 <code>commit</code> 或 <code>release</code>」——typestate 能部分表達但寫法繁瑣、多數情況靠 doc 是合理的</li>
<li><strong>時序契約</strong>：「寫入後最多 1 秒內 read replica 可見」「retry 5 次後放棄」——跨呼叫、跨時間的契約、型別表達不了</li>
<li><strong>副作用 / 對外部系統的影響</strong>：「會寫入 audit log」「會發 webhook」——caller 需要知道才能規劃整體流程</li>
<li><strong>業務規則 + 有來源</strong>：「會員價只能用 wallet 付款（業務需求 #1234）」——有出處的業務動機要寫、避免後人誤刪</li>
<li><strong>效能契約</strong>：「O(log n) 查詢；不適合在熱迴圈呼叫」——caller 要根據這個資訊選用法</li>
</ul>
<p>「短」不是目標、「精準」才是。把該下移的下移到型別、剩下的就值得詳細寫。</p>
<hr>
<h2 id="收束doc-設計就是-api-設計">收束：doc 設計就是 API 設計</h2>
<p>回到開頭——doc 寫不好會誤導使用者。但更深一層的觀察是：<strong>「需要寫很多 doc 才能用對」本身就是 API 設計的紅旗</strong>。</p>
<p>好的 API 用最少的 doc 就能讓使用者用對：</p>
<ul>
<li>命名直接表達意圖</li>
<li>型別表達合法輸入與失敗模式</li>
<li>結構（enum、sealed、builder）防止誤用</li>
<li>預設值導向多數情況下正確的選擇</li>
<li>殘餘的契約與 edge case 用簡短介面 doc 說明</li>
<li>實作特有的 invariant 用簡短實作註解說明</li>
</ul>
<p>寫 doc 的時候同時問「<strong>這條 doc 想說的事，是不是該由 API 設計本身承擔？</strong>」——這個問題能讓你的 doc 跟 API 同時變更好。</p>
]]></content:encoded></item><item><title>型別取代 doc 的收益曲線：強型別語言的 doc 該有多短</title><link>https://tarrragon.github.io/blog/record/%E5%9E%8B%E5%88%A5%E5%8F%96%E4%BB%A3-doc-%E7%9A%84%E6%94%B6%E7%9B%8A%E6%9B%B2%E7%B7%9A%E5%BC%B7%E5%9E%8B%E5%88%A5%E8%AA%9E%E8%A8%80%E7%9A%84-doc-%E8%A9%B2%E6%9C%89%E5%A4%9A%E7%9F%AD/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%9E%8B%E5%88%A5%E5%8F%96%E4%BB%A3-doc-%E7%9A%84%E6%94%B6%E7%9B%8A%E6%9B%B2%E7%B7%9A%E5%BC%B7%E5%9E%8B%E5%88%A5%E8%AA%9E%E8%A8%80%E7%9A%84-doc-%E8%A9%B2%E6%9C%89%E5%A4%9A%E7%9F%AD/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：型別系統強化等於 doc 表達力轉移——很多 doc 內容應該下移到型別。
&lt;strong>設計原則&lt;/strong>：能用型別表達的限制，不要用 doc 表達；doc 是型別表達不了的剩餘資訊的家。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 的 Layer 1（名稱與型別簽章）展開——把「型別承擔哪些原本寫在 doc 的內容」拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點型別越強doc-的職責範圍就越窄">起點：型別越強、doc 的職責範圍就越窄&lt;/h2>
&lt;p>「型別系統越強、function doc 也越能寫得短」——這是個普遍但不被刻意利用的現象。&lt;/p>
&lt;p>當你看到一個 Dart / TypeScript / Rust 的 function doc 寫得跟 Python / JavaScript 一樣長、多半有東西可以下移到型別。把可下移的內容下移、doc 表面變短、實質上的好處更深：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>編譯期被檢查&lt;/strong>——型別說的事不會 outdated（doc 會）&lt;/li>
&lt;li>&lt;strong>IDE 補全提示&lt;/strong>——使用者看到型別就懂、不用切到文件頁&lt;/li>
&lt;li>&lt;strong>重構時連動&lt;/strong>——改型別會逼所有 caller 跟著改、doc 改了沒人逼你檢查&lt;/li>
&lt;/ul>
&lt;p>這篇整理：哪些常見的 doc 內容能被型別取代、哪些下移了會破壞別的東西、以及型別越加越強時要怎麼平衡 ergonomic 跟表達力。&lt;/p>
&lt;hr>
&lt;h2 id="可被型別取代的常見-doc-內容">可被型別取代的常見 doc 內容&lt;/h2>
&lt;p>下面 8 類 doc 內容、共通特徵是「可以從 doc 約定升級成型別約束」——升級之後、保護從「靠使用者讀並記住」變成「靠編譯器強制」、執行力跟一致性都比 doc 強。每類列出弱（doc 約定）vs 強（型別約束）的對比。&lt;/p>
&lt;h3 id="1-必須是正整數必須非空必須在範圍內">1. 「必須是正整數」「必須非空」「必須在範圍內」&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">// 弱：依賴 doc 警告
&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">/// [quantity] 必須為正整數（&amp;gt;= 1）
&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="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">quantity&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="n">ArgumentError&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：refinement type / value object
&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="kd">class&lt;/span> &lt;span class="nc">PositiveInt&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="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">value&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="n">PositiveInt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&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">11&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="n">ArgumentError&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PositiveInt&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&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">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1">// 最強（語言支援的話）：refinement types
&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">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">where&lt;/span> &lt;span class="n">quantity&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="m">0&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;/code>&lt;/pre>&lt;/div>&lt;p>Dart 沒有 native refinement type，但用 wrapper class 一樣能達到「&lt;strong>呼叫端要顯式建構合法值才能呼叫&lt;/strong>」的效果。validation 從「呼叫進入 function 後才檢查」前移到「建構 value object 時檢查」，contract 變成型別系統的一部分。&lt;/p>
&lt;h3 id="2-可能為-null找不到時回傳-null">2. 「可能為 null」「找不到時回傳 null」&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">// 弱（前 null safety 時代）：
&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">/// [name] 可為 null，[email] 不可為 null
&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">class&lt;/span> &lt;span class="nc">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kt">String&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">name&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="kt">String&lt;/span> &lt;span class="n">email&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="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">/// 找不到時回傳 null
&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="n">User&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強（null safety）：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kt">String&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 型別已說可為 null
&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">&lt;/span> &lt;span class="kt">String&lt;/span> &lt;span class="n">email&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 型別已說不可為 null
&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="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="n">User&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="err">型別已說可能找不到&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dart / TypeScript / Kotlin / Swift 的 sound null safety 把「可為 null」從 doc 約定升級成型別約定——升級之後、「[X] 可為 null」這類 doc 變成 redundant noise（型別已經精準說了、重複寫只是稀釋訊號、改型別時忘了同步 doc 還會誤導讀者）。&lt;/p>
&lt;h3 id="3-會-throw-某-exception">3. 「會 throw 某 exception」&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">// 弱：靠 doc
&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">/// 找不到時 throw [NotFoundException]
&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">/// 網路錯誤時 throw [NetworkException]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Future&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&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">// 強：用 Result / Either / sealed class
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Future&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">GetUserError&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n">sealed&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">GetUserError&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="kd">class&lt;/span> &lt;span class="nc">NotFoundError&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">GetUserError&lt;/span> &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 class="kd">class&lt;/span> &lt;span class="nc">NetworkError&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">GetUserError&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">statusCode&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Result / Either pattern 把 error 從「invisible exception」升級成「型別簽章可見的回傳值」。Caller 必須處理（編譯不過 if not handled），不會漏掉 error path。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：型別系統強化等於 doc 表達力轉移——很多 doc 內容應該下移到型別。
<strong>設計原則</strong>：能用型別表達的限制，不要用 doc 表達；doc 是型別表達不了的剩餘資訊的家。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 的 Layer 1（名稱與型別簽章）展開——把「型別承擔哪些原本寫在 doc 的內容」拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點型別越強doc-的職責範圍就越窄">起點：型別越強、doc 的職責範圍就越窄</h2>
<p>「型別系統越強、function doc 也越能寫得短」——這是個普遍但不被刻意利用的現象。</p>
<p>當你看到一個 Dart / TypeScript / Rust 的 function doc 寫得跟 Python / JavaScript 一樣長、多半有東西可以下移到型別。把可下移的內容下移、doc 表面變短、實質上的好處更深：</p>
<ul>
<li><strong>編譯期被檢查</strong>——型別說的事不會 outdated（doc 會）</li>
<li><strong>IDE 補全提示</strong>——使用者看到型別就懂、不用切到文件頁</li>
<li><strong>重構時連動</strong>——改型別會逼所有 caller 跟著改、doc 改了沒人逼你檢查</li>
</ul>
<p>這篇整理：哪些常見的 doc 內容能被型別取代、哪些下移了會破壞別的東西、以及型別越加越強時要怎麼平衡 ergonomic 跟表達力。</p>
<hr>
<h2 id="可被型別取代的常見-doc-內容">可被型別取代的常見 doc 內容</h2>
<p>下面 8 類 doc 內容、共通特徵是「可以從 doc 約定升級成型別約束」——升級之後、保護從「靠使用者讀並記住」變成「靠編譯器強制」、執行力跟一致性都比 doc 強。每類列出弱（doc 約定）vs 強（型別約束）的對比。</p>
<h3 id="1-必須是正整數必須非空必須在範圍內">1. 「必須是正整數」「必須非空」「必須在範圍內」</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">// 弱：依賴 doc 警告
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [quantity] 必須為正整數（&gt;= 1）
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">quantity</span> <span class="o">&lt;</span> <span class="m">1</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</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 class="c1">// 強：refinement type / value object
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">PositiveInt</span> <span class="p">{</span>
</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">value</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">PositiveInt</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">&lt;</span> <span class="m">1</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</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 class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</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">// 最強（語言支援的話）：refinement types
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="n">where</span> <span class="n">quantity</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>Dart 沒有 native refinement type，但用 wrapper class 一樣能達到「<strong>呼叫端要顯式建構合法值才能呼叫</strong>」的效果。validation 從「呼叫進入 function 後才檢查」前移到「建構 value object 時檢查」，contract 變成型別系統的一部分。</p>
<h3 id="2-可能為-null找不到時回傳-null">2. 「可能為 null」「找不到時回傳 null」</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">// 弱（前 null safety 時代）：
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [name] 可為 null，[email] 不可為 null
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</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="c1">// 強（null safety）：
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>       <span class="c1">// 型別已說可為 null
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>       <span class="c1">// 型別已說不可為 null
</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 class="n">User</span><span class="o">?</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="o">//</span> <span class="err">型別已說可能找不到</span></span></span></code></pre></div><p>Dart / TypeScript / Kotlin / Swift 的 sound null safety 把「可為 null」從 doc 約定升級成型別約定——升級之後、「[X] 可為 null」這類 doc 變成 redundant noise（型別已經精準說了、重複寫只是稀釋訊號、改型別時忘了同步 doc 還會誤導讀者）。</p>
<h3 id="3-會-throw-某-exception">3. 「會 throw 某 exception」</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">// 弱：靠 doc
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 找不到時 throw [NotFoundException]
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 網路錯誤時 throw [NetworkException]
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">User</span><span class="o">&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</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">// 強：用 Result / Either / sealed class
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Result</span><span class="o">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">GetUserError</span><span class="o">&gt;&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</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="n">sealed</span> <span class="kd">class</span> <span class="nc">GetUserError</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">class</span> <span class="nc">NotFoundError</span> <span class="kd">extends</span> <span class="n">GetUserError</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kd">class</span> <span class="nc">NetworkError</span> <span class="kd">extends</span> <span class="n">GetUserError</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">statusCode</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Result / Either pattern 把 error 從「invisible exception」升級成「型別簽章可見的回傳值」。Caller 必須處理（編譯不過 if not handled），不會漏掉 error path。</p>
<p>代價：寫法比 throw 多一些；不是所有 codebase 都採用這個 pattern。但對核心 service 介面值得。</p>
<h3 id="4-合法值是-ab-或-c">4. 「合法值是 A、B 或 C」</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">// 弱：String flag + doc
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [mode] 可選值：&#39;manual&#39;、&#39;auto&#39;、&#39;hybrid&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="kt">String</span> <span class="n">mode</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：enum
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">Mode</span> <span class="p">{</span> <span class="n">manual</span><span class="p">,</span> <span class="n">auto</span><span class="p">,</span> <span class="n">hybrid</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="n">Mode</span> <span class="n">mode</span><span class="p">);</span></span></span></code></pre></div><p>String flag 是「<strong>doc 約束代替型別約束</strong>」的最常見例子。改用 enum 之後：</p>
<ul>
<li>IDE 自動補全</li>
<li>拼錯立刻編譯錯</li>
<li>新增 / 刪除 mode 時所有 caller 編譯出錯（迫使你檢查每個地方該怎麼處理）</li>
</ul>
<h3 id="5-狀態-x-才能呼叫">5. 「狀態 X 才能呼叫」</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">// 弱：靠 doc + 執行期檢查
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 必須在 [open] 之後、[close] 之前呼叫；否則 throw [StateError]
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">write</span><span class="p">(</span><span class="kt">String</span> <span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 強：typestate / phantom types（Rust 友善，Dart 較吃力）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">OpenConnection</span> <span class="p">{</span> <span class="kt">void</span> <span class="n">write</span><span class="p">(</span><span class="kt">String</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">class</span> <span class="nc">ClosedConnection</span> <span class="p">{</span> <span class="cm">/* no write method */</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="n">OpenConnection</span> <span class="n">open</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">ClosedConnection</span> <span class="n">close</span><span class="p">(</span><span class="n">OpenConnection</span> <span class="n">conn</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>typestate 把「必須在某狀態下才能呼叫」變成「<strong>那個狀態才存在那個方法</strong>」。Rust / Haskell 寫起來最自然；Dart / Java 可以用建構子分流模擬，但 ergonomic 較差。</p>
<p>對核心 lifecycle（connection、transaction、stream subscription）值得用；一般 service 不必。</p>
<h3 id="6-兩個參數互斥某參數有時必填">6. 「兩個參數互斥」「某參數有時必填」</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">// 弱：positional args + doc
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 同時提供 [token] 和 [credentials] 會 throw
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 至少要提供一個
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">auth</span><span class="p">(</span><span class="kt">String</span><span class="o">?</span> <span class="n">token</span><span class="p">,</span> <span class="n">Credentials</span><span class="o">?</span> <span class="n">credentials</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">// 強：sealed class 表達互斥
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="n">sealed</span> <span class="kd">class</span> <span class="nc">AuthMethod</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">class</span> <span class="nc">TokenAuth</span> <span class="kd">extends</span> <span class="n">AuthMethod</span> <span class="p">{</span> <span class="kd">final</span> <span class="kt">String</span> <span class="n">token</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">class</span> <span class="nc">CredentialsAuth</span> <span class="kd">extends</span> <span class="n">AuthMethod</span> <span class="p">{</span> <span class="kd">final</span> <span class="n">Credentials</span> <span class="n">creds</span><span class="p">;</span> <span class="p">}</span>
</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 class="n">User</span> <span class="n">auth</span><span class="p">(</span><span class="n">AuthMethod</span> <span class="n">method</span><span class="p">);</span></span></span></code></pre></div><p>「至少一個 / 至多一個 / 互斥」這類條件用 sealed class / discriminated union 表達。caller 看到型別就知道兩條路擇一，不需要 doc 說明組合規則。</p>
<h3 id="7-這個-collection-是-read-only--不要修改">7. 「這個 collection 是 read-only / 不要修改」</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">// 弱：靠 doc 約定
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 不要修改回傳的 list
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 強：immutable collection 型別
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">List</span><span class="p">.</span><span class="n">unmodifiable</span><span class="p">(</span><span class="n">_items</span><span class="p">);</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="c1"></span><span class="n">Iterable</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">_items</span><span class="p">;</span>  <span class="c1">// Iterable 不暴露 mutation
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 或（用 built_collection）：
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="n">BuiltList</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span></span></span></code></pre></div><p>「請別修改」doc 警告靠的是「使用者願意讀且記住」，型別約束是強制的。</p>
<h3 id="8-測量單位公里-vs-英里秒-vs-毫秒">8. 「測量單位」（公里 vs 英里、秒 vs 毫秒）</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">// 弱：靠 doc 標單位
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [timeout] 單位：毫秒
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setTimeout</span><span class="p">(</span><span class="kt">int</span> <span class="n">timeout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="kt">void</span> <span class="n">setTimeout</span><span class="p">(</span><span class="n">Duration</span> <span class="n">timeout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">setTimeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">(</span><span class="nl">seconds:</span> <span class="m">30</span><span class="p">));</span>  <span class="o">//</span> <span class="err">不需要記得是哪個單位</span></span></span></code></pre></div><p>混淆單位是真實事故來源（Mars Climate Orbiter 級別的）。<code>Duration</code> / <code>Money</code> / <code>Distance</code> 等領域 wrapper 型別把單位編進型別系統，呼叫端不會傳錯。</p>
<hr>
<h2 id="型別表達不了的部分doc-仍是該寫的家">型別表達不了的部分（doc 仍是該寫的家）</h2>
<p>把可下移的下移之後，doc 還剩什麼？這些是型別表達不了的：</p>
<h3 id="1-業務動機--為什麼這個契約存在">1. 業務動機 / 為什麼這個契約存在</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">/// 會員價只能用 wallet 付款
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// （業務規則：會員價是 wallet 餘額的折扣回饋）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">chargeMemberPrice</span><span class="p">(</span><span class="n">Member</span> <span class="n">m</span><span class="p">);</span></span></span></code></pre></div><p>「為什麼只能用 wallet」是業務規則，不在型別系統的射程內。這類<strong>有來源的業務動機</strong>仍然要寫 doc——但要有來源，不是憑想像。</p>
<h3 id="2-性能特性">2. 性能特性</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">/// O(log n) 查詢；插入 O(n)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">T</span> <span class="n">find</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><p>Big-O / 延遲特性 / 記憶體 footprint 等性能契約，型別表達不了。如果這個性能特性是 caller 需要知道才能正確選用（例如「這個 method 不適合在迴圈裡呼叫」），就要寫進 doc。</p>
<h3 id="3-對外部系統的副作用">3. 對外部系統的副作用</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">/// 寫入 audit log（第三方系統，可能延遲到資料庫）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">recordTransaction</span><span class="p">(</span><span class="n">Tx</span> <span class="n">tx</span><span class="p">);</span></span></span></code></pre></div><p>跟外部系統的互動（log、analytics、cache invalidation、cloud sync）是型別表達不了的副作用。caller 需要知道這些副作用才能規劃整體流程。</p>
<h3 id="4-時序契約eventually-consistentretry-行為">4. 時序契約（eventually consistent、retry 行為）</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">/// 寫入後最多 1 秒內所有 read replica 會看到新值
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">updateProfile</span><span class="p">(</span><span class="n">Profile</span> <span class="n">p</span><span class="p">);</span></span></span></code></pre></div><p>「最多多久內 consistent」「失敗多少次後放棄 retry」「某事件多久觸發一次」——這類<strong>跨呼叫、跨時間的契約</strong>，型別系統無法表達。</p>
<h3 id="5-使用情境的限制threading--isolation">5. 使用情境的限制（threading / isolation）</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">/// 必須在 main isolate 呼叫；否則 throw `IsolateError`
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">registerPlatformChannel</span><span class="p">(</span><span class="kt">String</span> <span class="n">name</span><span class="p">);</span></span></span></code></pre></div><p>「哪個 thread / isolate / context 才能呼叫」這類資訊，多數型別系統無法強制（Rust 的 Send/Sync 是少數例外）。</p>
<h3 id="6-跨方法-invariant">6. 跨方法 invariant</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">/// 跟 [withdraw] 配對使用：每次 [reserve] 之後必須對應一次
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [withdraw] 或 [release]，否則餘額會被 reserved 卡住
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">reserve</span><span class="p">(</span><span class="n">Decimal</span> <span class="n">amount</span><span class="p">);</span></span></span></code></pre></div><p>「呼叫了 X 之後必須在 Y 時間內呼叫 Z」這類<strong>跨方法的 protocol</strong>，typestate 能部分表達但寫法繁瑣，多數情況靠 doc 是合理的。</p>
<hr>
<h2 id="各語言實際範例">各語言實際範例</h2>
<h3 id="dartnull-safety-的影響">Dart：null safety 的影響</h3>
<p>Dart 2.12 引入 sound null safety 後，<strong>至少消除了 30% 的 doc 內容</strong>——不再需要寫「可為 null」「不可為 null」「null 時的行為」。</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="c1">// 前（Dart 2.10）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [name] 可為 null
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>  <span class="c1">// 實際可能為 null，doc 提醒
</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="n">User</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="c1">// 實際可能為 null
</span></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">// 後（Dart 3.x）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>  <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 class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="o">//</span> <span class="err">型別說明</span></span></span></code></pre></div><p>如果你的 Dart codebase 升了 null safety 但 doc 還在寫「可為 null」之類字句，說明還沒充分利用型別系統的成果。</p>
<h3 id="rustownership-與-borrow-消除一整類-doc">Rust：ownership 與 borrow 消除一整類 doc</h3>





<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">// C 風格：靠 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="sd">/// 注意：caller 必須在 buffer 釋放前完成讀取
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">/// 不要把 buffer 傳給其他 thread
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd"></span><span class="k">fn</span> <span class="nf">process</span><span class="p">(</span><span class="n">buffer</span>: <span class="o">*</span><span class="k">const</span><span class="w"> </span><span class="kt">u8</span><span class="p">,</span><span class="w"> </span><span class="n">len</span>: <span class="kt">usize</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">// Rust：型別表達
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process</span><span class="p">(</span><span class="n">buffer</span>: <span class="kp">&amp;</span><span class="p">[</span><span class="kt">u8</span><span class="p">]);</span><span class="w">  </span><span class="c1">// borrow，編譯期保證 lifetime
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process_owned</span><span class="p">(</span><span class="n">buffer</span>: <span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u8</span><span class="o">&gt;</span><span class="p">);</span><span class="w">  </span><span class="c1">// own，move 後 caller 不能再用
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process_shared</span><span class="p">(</span><span class="n">buffer</span>: <span class="nc">Arc</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">u8</span><span class="p">]</span><span class="o">&gt;</span><span class="p">);</span><span class="w">  </span><span class="c1">// 跨 thread 安全共享
</span></span></span></code></pre></div><p>Rust 的 ownership / borrow 系統把記憶體管理 / 並發安全相關的 doc 幾乎完全變成型別。寫 Rust 的 function doc 多半短得驚人——大部分 contract 已經編進簽章。</p>
<h3 id="typescriptdiscriminated-union-取代條件-flag-doc">TypeScript：discriminated union 取代條件 flag doc</h3>





<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">// 弱：靠 doc 解釋 flag 之間的關係
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="cm">/**
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> * @param type &#39;success&#39; or &#39;error&#39;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> * @param data 當 type=&#39;success&#39; 時必填，否則為 null
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="cm"> * @param error 當 type=&#39;error&#39; 時必填，否則為 null
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kr">interface</span> <span class="nx">Response</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kr">type</span><span class="o">:</span> <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">data?</span>: <span class="kt">any</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">error?</span>: <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</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"><span class="c1">// 強：discriminated union
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="kr">type</span> <span class="nx">Response</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="o">|</span> <span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="s1">&#39;success&#39;</span><span class="p">;</span> <span class="nx">data</span>: <span class="kt">ResponseData</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="o">|</span> <span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="s1">&#39;error&#39;</span><span class="p">;</span> <span class="nx">error</span>: <span class="kt">string</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1">// 使用時 TypeScript narrowing：
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="s1">&#39;success&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>  <span class="c1">// 型別已知是 ResponseData
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span>  <span class="c1">// 型別已知是 string
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>discriminated union 把「flag 跟其他欄位的關聯」編進型別。這比 doc 警告強多了。</p>
<hr>
<h2 id="收益曲線什麼時候強型別開始邊際遞減">收益曲線：什麼時候強型別開始邊際遞減</h2>
<p>把所有可下移的 doc 都下移，是不是型別越強越好？不是。<strong>型別強化有邊際成本</strong>：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>型別強化</th>
          <th>收益</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 加 null safety</td>
          <td>高</td>
          <td>消除大量 null 相關 doc + 防 NPE</td>
          <td>低（語言原生支援）</td>
      </tr>
      <tr>
          <td>2. 加 enum 取代 string flag</td>
          <td>高</td>
          <td>消除「合法值列表」doc + 編譯期檢查</td>
          <td>低</td>
      </tr>
      <tr>
          <td>3. 加 wrapper value object（PositiveInt 等）</td>
          <td>中</td>
          <td>消除範圍檢查 doc + 前移 validation</td>
          <td>中（多寫 class）</td>
      </tr>
      <tr>
          <td>4. 加 Result / Either</td>
          <td>中</td>
          <td>消除 throw doc + 強迫處理 error</td>
          <td>中（API 寫法改變、要套件 / 自寫）</td>
      </tr>
      <tr>
          <td>5. 加 typestate / phantom types</td>
          <td>低</td>
          <td>消除「狀態相關呼叫順序」doc</td>
          <td>高（程式碼變複雜、學習曲線陡）</td>
      </tr>
      <tr>
          <td>6. 加 dependent types / refinement types</td>
          <td>低</td>
          <td>編譯期完整契約</td>
          <td>極高（需要特殊語言支援）</td>
      </tr>
  </tbody>
</table>
<p>實務 sweet spot 通常落在 1-4 之間。5-6 在 systems / safety-critical 程式碼有意義，一般 app 加進去 ergonomic 變差，回收不到。</p>
<hr>
<h2 id="一個-review-的問題這條-doc-能變型別嗎">一個 review 的問題：「這條 doc 能變型別嗎？」</h2>
<p>review code 看到 doc 時，問三個問題：</p>
<ol>
<li><strong>這條 doc 描述的是輸入合法範圍嗎？</strong>
<ul>
<li>是 → 能不能用 wrapper type / refinement / enum 表達？</li>
</ul>
</li>
<li><strong>這條 doc 描述的是回傳的可能性（null、error、特殊值）嗎？</strong>
<ul>
<li>是 → 能不能用 nullable / Result / sealed class 表達？</li>
</ul>
</li>
<li><strong>這條 doc 描述的是「這時候才能呼叫」嗎？</strong>
<ul>
<li>是 → 能不能用 typestate / 不同型別的方法分流表達？</li>
</ul>
</li>
</ol>
<p>任一答案是「能」、先試型別。如果型別寫起來 ergonomic 不好（例如 wrapper class 太多、call site 變難讀）、再退回 doc——「先試型別」比「預設寫 doc」更能逼出可下移的部分。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>doc 是「<strong>型別表達不了的剩餘資訊</strong>」的家——型別越強、剩餘越少。</p></blockquote>
<p>寫 doc 之前先問「能用型別表達嗎」。能 → 改型別。不能 → 寫 doc，但只寫那條型別表達不了的部分（業務動機、性能、副作用、時序契約、跨方法 protocol）。</p>
<hr>
<h2 id="收束型別系統升級是文件設計升級的契機">收束：型別系統升級是文件設計升級的契機</h2>
<p>每一次語言升級（Dart 2 → 3、TypeScript 加新型別功能、Rust 穩定新 lifetime feature），都是<strong>重新檢視既有 doc</strong> 的機會：</p>
<ul>
<li>哪些 doc 可以下移到新引入的型別功能？</li>
<li>下移之後，剩下的 doc 是不是更精準了？</li>
<li>是不是有新的型別組合能表達以前只能靠 doc 的契約？</li>
</ul>
<p>把語言升級當成 doc 整理的契機，不只是「換個編譯器」。<strong>程式碼品質的關鍵改善往往來自把約定升級為約束</strong>——doc 是約定，型別是約束。約定靠人記住，約束靠工具強制。每次升級都是一次「把約定變約束」的機會窗口。</p>
<p>寫到「三行 doc 解釋一個 function 的合法輸入範圍」這個訊號時、自問：<strong>「這三行能不能變成型別簽章？」</strong>——多半可以。</p>
]]></content:encoded></item><item><title>測試命名作為文件：可執行的規格說明</title><link>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%91%BD%E5%90%8D%E4%BD%9C%E7%82%BA%E6%96%87%E4%BB%B6%E5%8F%AF%E5%9F%B7%E8%A1%8C%E7%9A%84%E8%A6%8F%E6%A0%BC%E8%AA%AA%E6%98%8E/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%91%BD%E5%90%8D%E4%BD%9C%E7%82%BA%E6%96%87%E4%BB%B6%E5%8F%AF%E5%9F%B7%E8%A1%8C%E7%9A%84%E8%A6%8F%E6%A0%BC%E8%AA%AA%E6%98%8E/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：測試是少數&lt;strong>會自我驗證&lt;/strong>的文件——名稱說的事如果跟實際行為不符，CI 會炸。
&lt;strong>設計原則&lt;/strong>：測試命名應該讓「跳到測試檔讀名字」就能取代讀 doc。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 的 Layer 4（範例與測試）展開——把「測試命名作為可執行 spec」這個職責拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點被-ci-強制同步的-doc">起點：被 CI 強制同步的 doc&lt;/h2>
&lt;p>source code 的 doc comment 有個結構性缺陷：&lt;strong>寫得再好，code 改了 doc 沒改，doc 就在說謊&lt;/strong>。沒有任何工具強制 doc 跟 code 同步。&lt;/p>
&lt;p>測試是少數例外。一個命名為 &lt;code>removes_item_when_quantity_reaches_zero&lt;/code> 的測試，如果實際上 quantity 到 0 時沒移除，&lt;strong>測試會失敗、CI 會擋下 commit&lt;/strong>。測試名稱跟實際行為的一致性是被 CI 強制的——這讓測試成為&lt;strong>會自我驗證的文件&lt;/strong>。&lt;/p>
&lt;p>當你把這個性質有意識地利用起來，測試就不只是 regression 工具，而是&lt;strong>可執行的 API 規格&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="測試命名的三種主要模式">測試命名的三種主要模式&lt;/h2>
&lt;p>被測單元的契約大致分三類：「&lt;strong>在某狀態下回傳什麼&lt;/strong>」「&lt;strong>某操作會做什麼&lt;/strong>」「&lt;strong>何時 throw / 失敗&lt;/strong>」——對應到測試命名也分三類 pattern。每類 pattern 的命名格式不同、負責驗證契約的不同切面。&lt;/p>
&lt;h3 id="模式-1state-based狀態描述">模式 1：state-based（狀態描述）&lt;/h3>
&lt;p>「在某個狀態下，呼叫 X 會回傳 / 變成什麼」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_null_when_user_not_found&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_empty_list_when_no_items_match&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_cached_value_on_second_call&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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;/code>&lt;/pre>&lt;/div>&lt;p>適合：query / read-only 操作。&lt;/p>
&lt;h3 id="模式-2scenario-based情境描述">模式 2：scenario-based（情境描述）&lt;/h3>
&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="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_item_when_quantity_reaches_zero&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreases_quantity_when_item_exists_with_quantity_above_one&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;updates_lastChangedItem_on_addItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">4&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_not_update_lastChangedItem_on_removeItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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;/code>&lt;/pre>&lt;/div>&lt;p>適合：command / mutation 操作。注意 &lt;code>does_not_X&lt;/code> 形式——&lt;strong>negative assertion 也該寫進名字&lt;/strong>，這正是契約的一部分。&lt;/p>
&lt;h3 id="模式-3failure-mode失敗模式描述">模式 3：failure-mode（失敗模式描述）&lt;/h3>
&lt;p>「在某輸入 / 狀態下，會 throw / error / 失敗」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;throws_NotFoundException_when_id_does_not_exist&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;throws_StateError_when_called_after_dispose&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_error_when_network_unavailable&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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;/code>&lt;/pre>&lt;/div>&lt;p>適合：error path、edge case。&lt;strong>失敗模式是 doc 最容易漏寫的部分&lt;/strong>，但對 caller 最關鍵。&lt;/p>
&lt;hr>
&lt;h2 id="group-結構作為命名空間">Group 結構作為命名空間&lt;/h2>
&lt;p>巢狀 group 提供了「主題 → 操作 → 情境」的階層命名空間，比扁平命名更易讀：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;CartService&amp;#39;&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;addItem&amp;#39;&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"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;appends_when_item_not_in_cart&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;increments_quantity_when_same_item_exists&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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 class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;updates_lastChangedItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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"> 6&lt;/span>&lt;span class="cl"> &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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removeItem&amp;#39;&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_when_item_exists&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">10&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_nothing_when_item_not_found&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">11&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_not_update_lastChangedItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">12&lt;/span>&lt;span class="cl"> &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>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreaseQuantity&amp;#39;&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">15&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreases_when_quantity_above_one&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">16&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_item_when_quantity_reaches_zero&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&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">17&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀者掃過 group 結構，立刻知道 &lt;code>CartService&lt;/code> 對外提供哪些操作、每個操作有哪些行為承諾——&lt;strong>這是這個 service 的 readable spec&lt;/strong>。&lt;/p>
&lt;p>工具支援：好的 IDE / test runner 會把 group 結構顯示為樹狀，跑測試時的輸出也帶階層。把這個視覺結構利用好，測試 console 本身就是 doc 瀏覽器。&lt;/p>
&lt;hr>
&lt;h2 id="把-tests-當-readable-spec-的閱讀流程">把 tests 當 readable spec 的閱讀流程&lt;/h2>
&lt;p>當你不確定一個 function 的行為時，閱讀順序通常是：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>看簽章&lt;/strong> → 知道 what / takes / returns&lt;/li>
&lt;li>&lt;strong>讀 doc&lt;/strong> → 知道契約、edge case&lt;/li>
&lt;li>&lt;strong>看實作&lt;/strong> → 知道 how&lt;/li>
&lt;li>&lt;strong>找測試&lt;/strong> → 看具體 case&lt;/li>
&lt;/ol>
&lt;p>但如果測試命名做得好，&lt;strong>順序可以對調&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>看簽章&lt;/li>
&lt;li>&lt;strong>跳到對應 test file，掃 group + test names&lt;/strong> → 看 API 支援哪些 case、各 case 的承諾&lt;/li>
&lt;li>不夠才回去讀 doc / 實作&lt;/li>
&lt;/ol>
&lt;p>這個順序的優勢：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：測試是少數<strong>會自我驗證</strong>的文件——名稱說的事如果跟實際行為不符，CI 會炸。
<strong>設計原則</strong>：測試命名應該讓「跳到測試檔讀名字」就能取代讀 doc。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 的 Layer 4（範例與測試）展開——把「測試命名作為可執行 spec」這個職責拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點被-ci-強制同步的-doc">起點：被 CI 強制同步的 doc</h2>
<p>source code 的 doc comment 有個結構性缺陷：<strong>寫得再好，code 改了 doc 沒改，doc 就在說謊</strong>。沒有任何工具強制 doc 跟 code 同步。</p>
<p>測試是少數例外。一個命名為 <code>removes_item_when_quantity_reaches_zero</code> 的測試，如果實際上 quantity 到 0 時沒移除，<strong>測試會失敗、CI 會擋下 commit</strong>。測試名稱跟實際行為的一致性是被 CI 強制的——這讓測試成為<strong>會自我驗證的文件</strong>。</p>
<p>當你把這個性質有意識地利用起來，測試就不只是 regression 工具，而是<strong>可執行的 API 規格</strong>。</p>
<hr>
<h2 id="測試命名的三種主要模式">測試命名的三種主要模式</h2>
<p>被測單元的契約大致分三類：「<strong>在某狀態下回傳什麼</strong>」「<strong>某操作會做什麼</strong>」「<strong>何時 throw / 失敗</strong>」——對應到測試命名也分三類 pattern。每類 pattern 的命名格式不同、負責驗證契約的不同切面。</p>
<h3 id="模式-1state-based狀態描述">模式 1：state-based（狀態描述）</h3>
<p>「在某個狀態下，呼叫 X 會回傳 / 變成什麼」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_null_when_user_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_empty_list_when_no_items_match&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_cached_value_on_second_call&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：query / read-only 操作。</p>
<h3 id="模式-2scenario-based情境描述">模式 2：scenario-based（情境描述）</h3>
<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="n">test</span><span class="p">(</span><span class="s1">&#39;removes_item_when_quantity_reaches_zero&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases_quantity_when_item_exists_with_quantity_above_one&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;updates_lastChangedItem_on_addItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_not_update_lastChangedItem_on_removeItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：command / mutation 操作。注意 <code>does_not_X</code> 形式——<strong>negative assertion 也該寫進名字</strong>，這正是契約的一部分。</p>
<h3 id="模式-3failure-mode失敗模式描述">模式 3：failure-mode（失敗模式描述）</h3>
<p>「在某輸入 / 狀態下，會 throw / error / 失敗」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;throws_NotFoundException_when_id_does_not_exist&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;throws_StateError_when_called_after_dispose&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_error_when_network_unavailable&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：error path、edge case。<strong>失敗模式是 doc 最容易漏寫的部分</strong>，但對 caller 最關鍵。</p>
<hr>
<h2 id="group-結構作為命名空間">Group 結構作為命名空間</h2>
<p>巢狀 group 提供了「主題 → 操作 → 情境」的階層命名空間，比扁平命名更易讀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">group</span><span class="p">(</span><span class="s1">&#39;CartService&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;addItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;appends_when_item_not_in_cart&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;increments_quantity_when_same_item_exists&#39;</span><span class="p">,</span> <span class="p">()</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 class="n">test</span><span class="p">(</span><span class="s1">&#39;updates_lastChangedItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <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="n">group</span><span class="p">(</span><span class="s1">&#39;removeItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes_when_item_exists&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_nothing_when_item_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_not_update_lastChangedItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</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">group</span><span class="p">(</span><span class="s1">&#39;decreaseQuantity&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases_when_quantity_above_one&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes_item_when_quantity_reaches_zero&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>讀者掃過 group 結構，立刻知道 <code>CartService</code> 對外提供哪些操作、每個操作有哪些行為承諾——<strong>這是這個 service 的 readable spec</strong>。</p>
<p>工具支援：好的 IDE / test runner 會把 group 結構顯示為樹狀，跑測試時的輸出也帶階層。把這個視覺結構利用好，測試 console 本身就是 doc 瀏覽器。</p>
<hr>
<h2 id="把-tests-當-readable-spec-的閱讀流程">把 tests 當 readable spec 的閱讀流程</h2>
<p>當你不確定一個 function 的行為時，閱讀順序通常是：</p>
<ol>
<li><strong>看簽章</strong> → 知道 what / takes / returns</li>
<li><strong>讀 doc</strong> → 知道契約、edge case</li>
<li><strong>看實作</strong> → 知道 how</li>
<li><strong>找測試</strong> → 看具體 case</li>
</ol>
<p>但如果測試命名做得好，<strong>順序可以對調</strong>：</p>
<ol>
<li>看簽章</li>
<li><strong>跳到對應 test file，掃 group + test names</strong> → 看 API 支援哪些 case、各 case 的承諾</li>
<li>不夠才回去讀 doc / 實作</li>
</ol>
<p>這個順序的優勢：</p>
<ul>
<li><strong>測試名是被驗證過的事實</strong>，doc 是聲明（可能 outdated）</li>
<li><strong>測試名涵蓋 edge case</strong>，比 doc 完整</li>
<li><strong>跳到測試只要一個快捷鍵</strong>（多數 IDE 有 &ldquo;Go to Test&rdquo; 命令）</li>
</ul>
<p>當團隊習慣這個閱讀順序，<strong>doc 寫多寫少的壓力就會減輕</strong>——很多 edge case 直接讓測試說明，doc 留給「測試也表達不了」的部分（業務動機、隱性需求）。</p>
<hr>
<h2 id="反模式">反模式</h2>
<h3 id="反模式-1test_-前綴--模糊主題">反模式 1：<code>test_</code> 前綴 + 模糊主題</h3>
<p><strong>正向概念</strong>：測試名字的每個 token 都該承載資訊——前綴或主題詞如果讀者一眼推不出「在驗什麼」、就是浪費 token budget。</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="c1">// 反：純 noise
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user_2&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user_creation&#39;</span><span class="p">,</span> <span class="p">()</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="n">test</span><span class="p">(</span><span class="s1">&#39;creates_user_with_default_role_when_role_omitted&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p><code>test_</code> 前綴是工具年代留下的習慣（早期某些 framework 靠它識別測試 method）；現代 framework 用 annotation / 函式簽章識別、前綴變成純 noise。模糊的主題（<code>test_user</code>、<code>test_creation</code>）等於沒命名——讀者必須跳進 body 才能分辨兩個 test 在驗什麼、命名的 doc 價值消失。</p>
<h3 id="反模式-2實作洩漏的命名">反模式 2：實作洩漏的命名</h3>
<p><strong>正向概念</strong>：測試驗的是<strong>對外可觀察的契約</strong>——換實作而契約沒變、測試應該繼續通過、命名也不該需要改。</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="c1">// 反：洩漏實作（用 hashmap、用 cache）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;uses_hashmap_for_lookup&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;caches_result_after_first_call&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="n">test</span><span class="p">(</span><span class="s1">&#39;returns_value_in_O_1_for_existing_key&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;subsequent_calls_return_same_instance&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>命名洩漏實作後、重構（換 hashmap 為 trie、移除 cache 改用 lazy init）會逼迫測試一起改名——但對外行為其實沒變。一個良好的契約測試、應該在 codebase 大改造後仍能驗證「行為是否還是當初承諾的樣子」、命名洩漏實作會破壞這個性質。</p>
<h3 id="反模式-3描述怎麼做而非做什麼">反模式 3：描述「怎麼做」而非「做什麼」</h3>
<p><strong>正向概念</strong>：測試名描述「被測單元的契約」、test body 描述「測試怎麼寫」——分配給對應的位置、讀者跳到名字看契約、跳到 body 看細節。</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="c1">// 反：描述測試怎麼跑（過程）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;mocks_db_and_calls_findUser_then_asserts_result&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 正：描述被測 function 的行為
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_null_when_user_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>把「mocks_db_and_calls_X」寫進名字、讀者拿到的是「測試怎麼寫的過程」、不是「被測單元承諾什麼」——但讀 spec 想知道的是後者。「怎麼寫」放 test body、「驗證什麼契約」放名字、兩種讀者都得益。</p>
<h3 id="反模式-4assertion-style-命名">反模式 4：assertion-style 命名</h3>
<p><strong>正向概念</strong>：測試名是業務語義的入口、不是 assertion 框架的字面映射——讀者讀名字想推「業務上發生什麼」、不是「assert 用了哪個動詞」。</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="c1">// 反：assertion 寫在名字
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;isFalse_when_disabled&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;equal_when_same_input&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</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="n">test</span><span class="p">(</span><span class="s1">&#39;returns_false_when_feature_disabled&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_same_result_for_equivalent_inputs&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p><code>isTrue</code>、<code>equal</code>、<code>isNotEmpty</code> 是 assertion 動詞、不是行為描述。讀者讀 <code>isFalse_when_disabled</code> 不知道「false」對應什麼業務語義（feature 關掉？user 不存在？status 失效？）——把業務語義寫進名字、讀者一眼就能 map 到實際情境。</p>
<h3 id="反模式-5用-numbering-取代命名">反模式 5：用 numbering 取代命名</h3>
<p><strong>正向概念</strong>：每個 test case 都有獨特的「驗什麼情境」、命名就是把那個情境寫出來。編號只負責「不重複」、不負責「能識別」——失去命名最關鍵的功能。</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="c1">// 反：靠編號區分
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_1&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_2&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_3&#39;</span><span class="p">,</span> <span class="p">()</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="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_appends_when_cart_empty&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_increments_when_same_item_exists&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_handles_null_customization&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>編號是「我懶得想名字」的訊號。讀者要跳進 test body 才能區分 case 1 跟 case 2 是什麼差別——失去測試命名的全部 doc 價值；CI 報告看到「<code>addItem_case_2</code> 失敗」也無從直接判斷哪個情境壞了。</p>
<hr>
<h2 id="邊界什麼時候測試名不適合當-spec">邊界：什麼時候測試名不適合當 spec</h2>
<p>「測試名是 spec 條目」是預設、<strong>但有些情境測試命名無法獨自承擔 doc 責任</strong>：</p>
<ul>
<li><strong>大量參數化 / property-based test</strong>：「對任意輸入 N、結果都 ≥ N」這類 invariant、命名只能寫概念名（<code>preserves_minimum</code>）、具體 input 範圍要靠 doc 或 generator 描述</li>
<li><strong>整合 / e2e test</strong>：跨多個系統的行為、命名常壓不下完整流程（「user_can_complete_checkout_with_loyalty_points_and_split_payment」）、要靠 setup / scenario doc 補上下文</li>
<li><strong>測試本身是業務動機的二次表達</strong>：例如 GDPR 合規規則、業務動機的詳細條款仍要寫在介面 doc / spec 文件、命名只負責「驗證點」</li>
<li><strong>內部行為對齊 vs 對外契約</strong>：私有 helper / internal worker 的測試命名不必當公開 spec、可以直接用實作詞彙（這時候命名價值是「regression 防護」而非「對外文件」）</li>
</ul>
<p>判斷標準：「讀者只看名字、能不能拿到他要的資訊？」答「能」就讓命名當 spec 用、答「不能」就把詳細上下文寫進 doc / scenario file、命名只當「定位錨點」。</p>
<hr>
<h2 id="給測試寫作的-checklist">給測試寫作的 checklist</h2>
<p>寫一個 test 之前，跑這個 checklist：</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>名字能不能讓讀者不看 body 就知道驗證什麼？</strong> 不能 → 重命名</li>
<li><input disabled="" type="checkbox"> <strong>名字描述的是被測 function 的契約嗎？</strong> 不是（描述測試過程）→ 重寫</li>
<li><input disabled="" type="checkbox"> <strong>名字有沒有業務面詞彙？</strong> 沒有（只有 assertion 動詞）→ 加業務詞彙</li>
<li><input disabled="" type="checkbox"> <strong>同 group 下這個名字跟其他 test 有區辨度嗎？</strong> 沒有（靠編號）→ 加情境描述</li>
<li><input disabled="" type="checkbox"> <strong>這個行為契約是 doc 沒寫但這個 test 在驗的嗎？</strong> 是 → 太好了，這個 test 補了 doc 漏洞</li>
<li><input disabled="" type="checkbox"> <strong>這個 test 在驗實作細節嗎？</strong> 是 → 改成驗對外可觀察行為，否則重構必折斷</li>
</ul>
<hr>
<h2 id="trade-off測試名變長的代價">Trade-off：測試名變長的代價</h2>
<p>把測試當 doc 寫，名字會變長——<code>addItem_increments_quantity_when_same_item_exists_with_identical_customizations</code> 比 <code>test_add</code> 長 5 倍。</p>
<p>值得嗎？看你怎麼讀測試：</p>
<ul>
<li><strong>只看綠紅燈、不讀名字</strong> → 短名字便利</li>
<li><strong>把測試當 spec 讀</strong> → 長名字回收成本</li>
</ul>
<p>多數團隊低估「把測試當 spec 讀」的價值，因為這個習慣需要團隊一致才有效——一個人寫好命名，其他人不讀，回收不到。<strong>這是團隊習慣問題，不是個人偏好問題</strong>。要建立這個習慣，最好的切入點是：</p>
<ol>
<li><strong>新功能 PR 直接讀新 test 的名字判斷契約是否合理</strong>——把命名變成 review 的一環</li>
<li><strong>修 bug 時要求新增的 regression test 名字描述 bug 行為</strong>（例如 <code>does_not_double_charge_on_retry</code>）——這些名字本身是 incident 紀錄</li>
<li><strong>重構 PR 不允許改 test 名</strong>（除非是改名抓 bug 暴露的契約變動）——避免重構順手「整理」掉重要命名</li>
</ol>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>測試名是「<strong>讀者跳到測試檔、不看 body 就能讀懂的 spec 條目</strong>」。</p></blockquote>
<p>寫測試名時想像一個讀者只會看到名字，他要能從名字推得：</p>
<ul>
<li>在驗哪個操作？</li>
<li>在哪個情境下？</li>
<li>期待什麼結果？</li>
</ul>
<p>三件事缺一不可。寫到名字過長覺得難寫——通常是被測 function 同時在做多件事，<strong>測試名長是設計訊號</strong>，先別急著縮名字，先想能不能拆 function。</p>
<hr>
<h2 id="收束測試命名是文件設計的一環">收束：測試命名是文件設計的一環</h2>
<p>回到開頭——測試是少數會自我驗證的文件。但這個性質<strong>只在你有意識利用時才有價值</strong>。把測試名寫成 <code>test_1</code>、<code>test_2</code>，你寫的是 regression 網，不是 doc。</p>
<p>把測試名寫成可讀 spec 條目，你寫的是同時包辦兩件事的東西：<strong>驗證 + 文件</strong>。這兩件事用同一份成本同時做完，是測試這個工具的最高槓桿用法。</p>
<p>把「<strong>這份 test file 是這個模組唯一的 doc、讀者夠不夠用？</strong>」當成命名的品質門檻——通過這個門檻的命名、自然就具備可讀 spec 的特性。</p>
]]></content:encoded></item></channel></rss>