<?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>Api-Design on Tarragon</title><link>https://tarrragon.github.io/blog/tags/api-design/</link><description>Recent content in Api-Design on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/api-design/index.xml" rel="self" type="application/rss+xml"/><item><title>SDK 公開 API 設計</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/</guid><description>&lt;p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。&lt;/p>
&lt;h2 id="六個方法">六個方法&lt;/h2>
&lt;h3 id="init">init&lt;/h3>
&lt;p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。&lt;/p>





&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">Monitor.init({
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> endpoint: &amp;#39;https://collector.example.com/v1/events&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> app: &amp;#39;my_app&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> version: &amp;#39;1.2.0&amp;#39;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> flushInterval: 30000, // 毫秒
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> bufferSize: 100,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">})&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。&lt;/p>
&lt;p>&lt;strong>連線驗證策略：lazy&lt;/strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。&lt;/p>
&lt;p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（&lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本&lt;/a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。&lt;/p>
&lt;h3 id="event">event&lt;/h3>
&lt;p>記錄使用者操作事件（&lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類&lt;/a>）。接受事件名稱和可選的 data 物件。&lt;/p>





&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">Monitor.event(&amp;#39;terminal.connect.start&amp;#39;, { url: &amp;#39;wss://...&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.event(&amp;#39;enrollment.qr.scan&amp;#39;)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。&lt;/p>
&lt;h3 id="error">error&lt;/h3>
&lt;p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。&lt;/p>





&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">Monitor.error(exception, { step: &amp;#39;ws_connect&amp;#39; })
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.error(&amp;#39;Auth token missing&amp;#39;, { context: &amp;#39;handshake&amp;#39; })&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>error 方法和自動攔截機制（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截&lt;/a>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。&lt;/p>
&lt;h3 id="metric">metric&lt;/h3>
&lt;p>記錄數值指標。接受指標名稱和數值。&lt;/p>





&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">Monitor.metric(&amp;#39;connect.duration_ms&amp;#39;, 320)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Monitor.metric(&amp;#39;terminal.fps&amp;#39;, 58.5)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。&lt;/p>
&lt;h3 id="flush">flush&lt;/h3>
&lt;p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 flush（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出&lt;/a>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。&lt;/p>





&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">await Monitor.flush()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。&lt;/p></description><content:encoded><![CDATA[<p>SDK 的公開 API 是應用程式和監控系統之間的契約。六個方法涵蓋 SDK 的完整生命週期：初始化、四類事件上報、資料送出控制和資源釋放。跨平台的 SDK（JS / Flutter / Python）共用相同的方法簽名，讓開發者在不同平台上使用一致的 API。</p>
<h2 id="六個方法">六個方法</h2>
<h3 id="init">init</h3>
<p>SDK 初始化。設定 collector endpoint、app 識別資訊、flush 間隔、buffer 大小。在 app 啟動時呼叫一次。</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">Monitor.init({
</span></span><span class="line"><span class="ln">2</span><span class="cl">  endpoint: &#39;https://collector.example.com/v1/events&#39;,
</span></span><span class="line"><span class="ln">3</span><span class="cl">  app: &#39;my_app&#39;,
</span></span><span class="line"><span class="ln">4</span><span class="cl">  version: &#39;1.2.0&#39;,
</span></span><span class="line"><span class="ln">5</span><span class="cl">  flushInterval: 30000,   // 毫秒
</span></span><span class="line"><span class="ln">6</span><span class="cl">  bufferSize: 100,
</span></span><span class="line"><span class="ln">7</span><span class="cl">})</span></span></code></pre></div><p>init 負責建立 session、記錄 lifecycle.session.start 事件、啟動 flush 計時器。init 之前呼叫其他方法應該拋出明確錯誤（SDK 未初始化），而非靜默忽略。</p>
<p><strong>連線驗證策略：lazy</strong>。init 不驗證 collector 是否可達 — 不發 HTTP 請求、不 ping endpoint。init 的失敗只代表配置錯誤（缺少 endpoint 參數），不代表網路問題。網路問題在第一次 flush 時才浮現，flush 失敗時事件保留在 buffer 等待重試。</p>
<p>Lazy 策略的理由：SDK 不應阻塞主程式的啟動流程。如果 init 驗證連線，collector 暫時不可用時 app 會啟動失敗 — 監控工具反而變成可用性的瓶頸。短生命週期腳本（<a href="/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配：短生命週期腳本</a>）對這一點更敏感 — hook 腳本不能因為 collector 沒啟動就拒絕執行。</p>
<h3 id="event">event</h3>
<p>記錄使用者操作事件（<a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">四類事件中的 Event 類</a>）。接受事件名稱和可選的 data 物件。</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">Monitor.event(&#39;terminal.connect.start&#39;, { url: &#39;wss://...&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.event(&#39;enrollment.qr.scan&#39;)</span></span></code></pre></div><p>event 方法是非阻塞的 — 事件進入內部 buffer 立即返回，不等待網路送出。應用程式的操作流程不應該被監控 SDK 的網路延遲阻塞。</p>
<h3 id="error">error</h3>
<p>記錄錯誤事件。接受 Error/Exception 物件或自訂的錯誤描述。自動附加 stack trace、錯誤類型、觸發位置。</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">Monitor.error(exception, { step: &#39;ws_connect&#39; })
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.error(&#39;Auth token missing&#39;, { context: &#39;handshake&#39; })</span></span></code></pre></div><p>error 方法和自動攔截機制（<a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截</a>）互補 — 自動攔截處理未捕獲的例外，error 方法處理開發者主動上報的已知錯誤。</p>
<h3 id="metric">metric</h3>
<p>記錄數值指標。接受指標名稱和數值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Monitor.metric(&#39;connect.duration_ms&#39;, 320)
</span></span><span class="line"><span class="ln">2</span><span class="cl">Monitor.metric(&#39;terminal.fps&#39;, 58.5)</span></span></code></pre></div><p>metric 方法記錄的是離散的數值快照。聚合計算（平均、百分位、趨勢）在 collector 端完成，SDK 端只負責記錄原始值。</p>
<h3 id="flush">flush</h3>
<p>強制送出 buffer 中所有待發事件。正常情況下 SDK 按 flushInterval 定期自動 flush（<a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出</a>）。flush 方法用於需要確保事件已送出的場景 — 例如 app 即將進入背景或使用者手動觸發 log 上傳。</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">await Monitor.flush()</span></span></code></pre></div><p>flush 是非同步方法 — 需要等待網路請求完成。呼叫端可以 await 確認送出成功，也可以 fire-and-forget。</p>
<h3 id="close">close</h3>
<p>SDK 資源釋放。停止 flush 計時器、送出 buffer 中剩餘事件、關閉網路連線、記錄 lifecycle.session.end 事件。</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">await Monitor.close()</span></span></code></pre></div><p>close 在 app 關閉時呼叫。呼叫後 SDK 進入已關閉狀態，後續的 event/error/metric 呼叫應該被靜默忽略（不拋錯，因為 app 正在關閉）。</p>
<h2 id="api-設計原則">API 設計原則</h2>
<p><strong>方法名稱和四類事件對齊</strong>。event / error / metric 三個方法直接對應三類事件，lifecycle 事件由 init 和 close 自動產生。開發者看到方法名稱就知道對應哪類事件。</p>
<p><strong>所有上報方法非阻塞</strong>。event、error、metric 進 buffer 立即返回。監控 SDK 阻塞應用程式的操作流程是反模式。</p>
<p><strong>init 和 close 成對出現</strong>。init 開始 session，close 結束 session。兩者界定 SDK 的活躍期間。</p>
<p>各平台的 SDK 整合範例（Flutter 的 pubspec.yaml + main.dart init、Python 的 pip install + init code、JS 的 script tag + init）見 monitor repo 各 SDK 的 README。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>自動攔截未捕獲的錯誤 → <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">自動攔截機制</a></li>
<li>Buffer 和 flush 的策略 → <a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a></li>
<li>SDK 端的資料脫敏 → <a href="/blog/monitoring/03-sdk-design/redaction-helper/" data-link-title="SDK redaction helper" data-link-desc="在事件離開 SDK 前移除敏感資訊 — 預設 redaction rule 處理常見 pattern，自訂 rule 處理業務特定的 secret">SDK redaction helper</a></li>
<li>SDK 的 HTTP POST 行為需要 protocol test → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a></li>
</ul>
]]></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></channel></rss>