<?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>重構 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E9%87%8D%E6%A7%8B/</link><description>Recent content in 重構 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E9%87%8D%E6%A7%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>設計驅動重構方法論 - Domain Root 檢查與技術債務清理實戰</title><link>https://tarrragon.github.io/blog/record/%E8%A8%AD%E8%A8%88%E9%A9%85%E5%8B%95%E9%87%8D%E6%A7%8B%E6%96%B9%E6%B3%95%E8%AB%96-domain-root-%E6%AA%A2%E6%9F%A5%E8%88%87%E6%8A%80%E8%A1%93%E5%82%B5%E5%8B%99%E6%B8%85%E7%90%86%E5%AF%A6%E6%88%B0/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E8%A8%AD%E8%A8%88%E9%A9%85%E5%8B%95%E9%87%8D%E6%A7%8B%E6%96%B9%E6%B3%95%E8%AB%96-domain-root-%E6%AA%A2%E6%9F%A5%E8%88%87%E6%8A%80%E8%A1%93%E5%82%B5%E5%8B%99%E6%B8%85%E7%90%86%E5%AF%A6%E6%88%B0/</guid><description>&lt;p>在一次常規的 Phase 4 重構評估中，我們打開了 &lt;code>library_domain.dart&lt;/code> 這個檔案，發現它足足有 600 多行，裡面混雜著聚合根、Value Object 和服務類別，彼此糾纏在一起。更糟的是，統計書籍狀態的函式正在使用根本不存在的枚舉值——程式編譯不過，但那些錯誤已經在那裡不知多久了。&lt;/p>
&lt;p>如果在重構之前沒有系統性地理解設計意圖，我們很可能會把問題修錯方向，或者在「修好」一處的同時破壞了其他地方。&lt;/p>
&lt;p>於是我們建立了一套方法論，把設計文件的閱讀和驗證，放在所有重構動作的最前面。&lt;/p></description><content:encoded><![CDATA[<p>在一次常規的 Phase 4 重構評估中，我們打開了 <code>library_domain.dart</code> 這個檔案，發現它足足有 600 多行，裡面混雜著聚合根、Value Object 和服務類別，彼此糾纏在一起。更糟的是，統計書籍狀態的函式正在使用根本不存在的枚舉值——程式編譯不過，但那些錯誤已經在那裡不知多久了。</p>
<p>如果在重構之前沒有系統性地理解設計意圖，我們很可能會把問題修錯方向，或者在「修好」一處的同時破壞了其他地方。</p>
<p>於是我們建立了一套方法論，把設計文件的閱讀和驗證，放在所有重構動作的最前面。</p>
<h2 id="為什麼設計文件要在程式碼之前讀">為什麼設計文件要在程式碼之前讀</h2>
<p>看到壞味道就處理、看到重複就提取——傳統重構路線沒有問題，但對 Domain 層的核心實體來說，這樣做有個盲點：我們可能根本不清楚這段程式碼的業務意圖。</p>
<p>不清楚「Library 為什麼要追蹤 SourceType 而不是 ReadingStatus」，技術上漂亮的重構可能在業務上悄悄引入錯誤。設計驅動重構的前提是：所有重構決策必須基於現有的設計文件，而不是單純從程式碼的技術形狀來判斷。</p>
<h2 id="驗證流程">驗證流程</h2>
<h3 id="第一階段設計文件檢查">第一階段：設計文件檢查</h3>
<p>在動任何一行程式碼之前，我們先回頭讀文件。閱讀順序很重要：從需求規格出發，再到用例說明，再到 UI 設計規格，最後到錯誤處理設計。</p>
<p>這個閱讀順序的邏輯是：先確立業務目標（需求規格），再理解使用場景（用例），再看 UI 層如何呈現（UI 規格），最後檢查系統如何應對異常（錯誤處理）。這樣走一遍，Domain Root 在整個系統中扮演的角色就會變得清晰。</p>
<p>以 Library 實體為例，文件告訴我們它需要支援不同來源類型的書籍統計——實體書、電子書、借閱書各有不同的計算邏輯。但程式碼裡用的是 <code>ReadingStatus</code> 枚舉，而且還在使用 <code>'digital'</code>、<code>'borrowed'</code> 這些根本不在枚舉定義裡的值。這不是設計問題，這是實作根本跑錯了方向。</p>
<h3 id="第二階段測試覆蓋率分析">第二階段：測試覆蓋率分析</h3>
<p>讀完文件之後，我們看測試。測試是另一種形式的設計文件，它告訴我們這段程式碼「被期待如何運作」。</p>
<p>這個階段最常發現的問題是測試的方向跑偏了。我們在 <code>book_test.dart</code> 裡發現，它引用了 <code>flutter_test</code> 套件，但 Domain 層的單元測試根本不應該依賴 Flutter 框架——這意味著這些測試在純 Dart 環境中根本無法執行，CI 流程也因此產生了盲點。</p>
<p>此外還有大量被 skip 的測試。每個 skip 都是一個未被驗證的業務假設，累積起來就是風險。</p>
<h3 id="第三階段程式實作驗證">第三階段：程式實作驗證</h3>
<p>前兩個階段讓我們建立了對「設計意圖」的理解。第三階段才是真正對照程式碼，逐一確認哪裡偏離了設計。</p>
<p>這個階段的關鍵產出是一份問題清單，而且每個問題都有分類：是類型系統錯誤、是架構違規、是技術債務，還是可維護性問題。分類決定了後續的修復優先級。</p>
<h2 id="問題分類與修復優先級">問題分類與修復優先級</h2>
<p>我們在實際的 Book 和 Library 重構中，識別出了四類問題，按照影響範圍決定修復順序。</p>
<p><strong>類型系統錯誤優先級最高。</strong> 使用不存在的枚舉值、把 <code>String</code> 傳給期望強類型的參數，這類問題一旦進入生產環境就是執行時崩潰。我們把所有這類問題排在第一批處理。</p>
<p><strong>異常處理不統一緊隨其後。</strong> Library 實體裡有自定義的 <code>DuplicateBookException</code>，但整個專案其他地方都統一使用 <code>AppError</code> 體系。這種不一致性不只是風格問題，它讓錯誤處理的呼叫端無法用一致的方式應對。</p>
<p><strong>技術債務屬於第二優先級。</strong> 最典型的例子是 <code>author</code> 欄位使用 <code>String</code> 類型。這在功能上可以運作，但它限制了後續的多作者支援、譯者解析和富文字搜尋能力。這類問題有明確的演進路徑，但不是緊急的。</p>
<p><strong>可維護性問題排在最後。</strong> 包括缺乏需求追蹤編號的註解、業務規則說明不足等。這些問題不影響功能，但它們是技術債務的溫床——當下一個人來維護這段程式碼時，他沒有足夠的上下文。</p>
<h2 id="幾個具體的重構模式">幾個具體的重構模式</h2>
<h3 id="類型系統修正">類型系統修正</h3>
<p>問題很常見：用字串或錯誤的枚舉值來做判斷，而不是使用正確的類型。</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="n">books</span><span class="p">.</span><span class="n">where</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">book</span><span class="p">.</span><span class="n">readingStatus</span> <span class="o">==</span> <span class="s1">&#39;digital&#39;</span><span class="p">)</span></span></span></code></pre></div><p>修正後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">books</span><span class="p">.</span><span class="n">where</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">book</span><span class="p">.</span><span class="k">source</span><span class="p">.</span><span class="n">type</span><span class="p">.</span><span class="n">isDigital</span><span class="p">)</span></span></span></code></pre></div><p>表面上看這只是換了一個屬性，但背後的意義完全不同：前者依賴字串的巧合匹配，後者依賴類型系統的保證。</p>
<h3 id="異常處理統一">異常處理統一</h3>
<p>自定義異常類別散落在程式碼各處，是一種常見的技術債務模式。</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="k">throw</span> <span class="n">DuplicateBookException</span><span class="p">(</span><span class="s1">&#39;message&#39;</span><span class="p">,</span> <span class="nl">bookId:</span> <span class="n">id</span><span class="p">,</span> <span class="nl">title:</span> <span class="n">title</span><span class="p">);</span></span></span></code></pre></div><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="k">return</span> <span class="n">OperationResult</span><span class="p">.</span><span class="n">failure</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">BusinessLogicError</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nl">message:</span> <span class="s1">&#39;message&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nl">businessRule:</span> <span class="s1">&#39;BOOK_DUPLICATE_PREVENTION&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nl">code:</span> <span class="n">ErrorCodes</span><span class="p">.</span><span class="n">duplicateBook</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nl">context:</span> <span class="p">{</span><span class="s1">&#39;bookId&#39;</span><span class="o">:</span> <span class="n">id</span><span class="p">,</span> <span class="s1">&#39;title&#39;</span><span class="o">:</span> <span class="n">title</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="s1">&#39;userMessage&#39;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">);</span></span></span></code></pre></div><p>這個轉換不只是換個類別，它同時把拋出異常改成了回傳結果——這是更適合 Domain 層的錯誤傳遞方式，讓呼叫端可以明確地決定如何處理每種失敗情況。</p>
<h3 id="value-object-重構">Value Object 重構</h3>
<p>把 <code>String</code> 提升為 Value Object 是一個需要仔細計畫的重構，因為它涉及所有使用處的同步更新。</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="kd">final</span> <span class="kt">String</span> <span class="n">author</span><span class="p">;</span></span></span></code></pre></div><p>修正後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">final</span> <span class="n">BookAuthor</span> <span class="n">author</span><span class="p">;</span></span></span></code></pre></div><p>關鍵在於轉換策略要設計好：<code>BookAuthor.fromString(stringValue)</code> 負責從舊有字串資料遷移，<code>bookAuthor.displayValue</code> 負責在需要字串輸出的場合提供相容性。有了這兩個轉換點，才能在保持向後相容性的前提下完成遷移。</p>
<h2 id="分階段執行每段必驗">分階段執行，每段必驗</h2>
<p>每個修復階段結束後都要驗證：<code>dart analyze</code> 無錯誤、核心功能正常運作、API 介面保持不變。我們曾經跳過這個步驟，結果後續階段的問題疊加在一起，讓除錯變得極其困難。</p>
<h2 id="從這次重構中得到的">從這次重構中得到的</h2>
<p>這次對 Book 和 Library 兩個 Domain Root 的重構，最終涉及了 35 個以上的檔案，把 24 個編譯錯誤降到了零，並且完整建立了需求追蹤的 <code>REQ-LIB-XXX</code> 編號體系。</p>
<p>收穫更大的是設計文件優先讓我們避開了幾個危險方向。如果直接看到 <code>author</code> 是 <code>String</code> 就去重構，可能會建出不符合業務需求的 Value Object。如果直接看到自定義異常就去統一，可能會在類型系統問題還沒修好的基礎上做錯誤的抽象。</p>
<hr>
<p>下次面對需要重構的模組時，先花 15 分鐘讀相關設計文件，再決定從哪裡下手。那 15 分鐘很可能讓你避開一個需要幾個小時才能發現的方向錯誤。</p>]]></content:encoded></item><item><title>程式碼自然語言化撰寫方法論</title><link>https://tarrragon.github.io/blog/record/%E7%A8%8B%E5%BC%8F%E7%A2%BC%E8%87%AA%E7%84%B6%E8%AA%9E%E8%A8%80%E5%8C%96%E6%92%B0%E5%AF%AB%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E7%A8%8B%E5%BC%8F%E7%A2%BC%E8%87%AA%E7%84%B6%E8%AA%9E%E8%A8%80%E5%8C%96%E6%92%B0%E5%AF%AB%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;p>程式碼不是寫給電腦看的，是寫給人類讀的。電腦只管執行，人類才要維護。&lt;/p></description><content:encoded><![CDATA[<p>程式碼不是寫給電腦看的，是寫給人類讀的。電腦只管執行，人類才要維護。</p>
<h2 id="認知負擔一切的出發點">認知負擔：一切的出發點</h2>
<p>人類工作記憶有限，大約一次只能處理七個項目（Miller&rsquo;s Law）。看到縮寫要在腦中展開、看到模糊詞要猜測含義、看到長函式要分段記憶——這些都是認知負擔。</p>
<p>自然語言化的目標很簡單：讓程式碼像讀文章一樣自然，把讀者的認知資源留給理解業務邏輯，而不是解碼程式碼本身。</p>
<hr>
<h2 id="第一原則命名要能直接讀懂">第一原則：命名要能直接讀懂</h2>
<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="kt">void</span> <span class="n">process</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kt">void</span> <span class="n">handle</span><span class="p">(</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></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">calculateBookReadingProgress</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</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">validateUserRegistrationData</span><span class="p">(</span><span class="n">User</span> <span class="n">user</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kt">void</span> <span class="n">enrichBookMetadataFromExternalSource</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</span> <span class="p">{}</span></span></span></code></pre></div><h3 id="變數命名">變數命名</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 錯誤：縮寫和多用途
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="n">usr</span> <span class="o">=</span> <span class="n">getCurrentUser</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">var</span> <span class="n">data</span> <span class="o">=</span> <span class="n">loadUserData</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">data</span> <span class="o">=</span> <span class="n">processBookData</span><span class="p">();</span> <span class="c1">// 100行後同一變數換了意思
</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"></span><span class="kd">final</span> <span class="n">authenticatedUser</span> <span class="o">=</span> <span class="n">getCurrentUser</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kd">final</span> <span class="n">userProfileData</span> <span class="o">=</span> <span class="n">loadUserData</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="n">enrichedBookMetadata</span> <span class="o">=</span> <span class="n">processBookData</span><span class="p">();</span></span></span></code></pre></div><h3 id="類別命名">類別命名</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 錯誤：說不清在做什麼
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">Manager</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kd">class</span> <span class="nc">Handler</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">class</span> <span class="nc">BookDAO</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// 正確：業務職責一目了然
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">BookMetadataEnrichmentService</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">UserRegistrationValidator</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">LibraryBookSearchEngine</span> <span class="p">{}</span></span></span></code></pre></div><h3 id="布林命名">布林命名</h3>
<p>布林變數應該能讀成問句，在 if 裡就能自然被理解：</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">bool</span> <span class="n">isValid</span><span class="p">;</span>      <span class="c1">// &#34;Is valid?&#34;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">bool</span> <span class="n">hasPermission</span><span class="p">;</span> <span class="c1">// &#34;Has permission?&#34;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">bool</span> <span class="n">canEdit</span><span class="p">;</span>       <span class="c1">// &#34;Can edit?&#34;
</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></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">bool</span> <span class="n">permission</span><span class="p">;</span></span></span></code></pre></div><h3 id="常見命名反模式">常見命名反模式</h3>
<ul>
<li><strong>匈牙利命名法</strong>：<code>strName</code>, <code>intCount</code> — 型別系統自己會提供型別資訊，名稱不用重複</li>
<li><strong>無意義前綴</strong>：<code>theUser</code>, <code>aBook</code> — 沒帶來任何資訊，直接刪掉</li>
<li><strong>過度縮寫</strong>：<code>usrMgr</code> — 迫使讀者展開，<code>userManager</code> 更自然</li>
<li><strong>數字後綴</strong>：<code>user1</code>, <code>user2</code> — 改成 <code>primaryUser</code>, <code>secondaryUser</code> 才說明關係</li>
</ul>
<hr>
<h2 id="第二原則函式控制在五到十行">第二原則：函式控制在五到十行</h2>
<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">// 錯誤：15行，三種職責混在一起
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">Book</span> <span class="n">processBook</span><span class="p">(</span><span class="kt">String</span> <span class="n">isbn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">isbn</span><span class="p">.</span><span class="n">length</span> <span class="o">!=</span> <span class="m">13</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(</span><span class="s1">&#39;Invalid ISBN&#39;</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="o">!</span><span class="n">isValidISBNChecksum</span><span class="p">(</span><span class="n">isbn</span><span class="p">))</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(</span><span class="s1">&#39;ISBN checksum failed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">final</span> <span class="n">apiResponse</span> <span class="o">=</span> <span class="n">httpClient</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">&#39;/books/</span><span class="si">$</span><span class="n">isbn</span><span class="s1">&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">apiResponse</span><span class="p">.</span><span class="n">statusCode</span> <span class="o">!=</span> <span class="m">200</span><span class="p">)</span> <span class="k">throw</span> <span class="n">Exception</span><span class="p">(</span><span class="s1">&#39;API failed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="kd">final</span> <span class="n">bookData</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">apiResponse</span><span class="p">.</span><span class="n">body</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">return</span> <span class="n">Book</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nl">id:</span> <span class="n">BookId</span><span class="p">(</span><span class="n">generateUniqueId</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nl">title:</span> <span class="n">BookTitle</span><span class="p">(</span><span class="n">bookData</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">source</span><span class="o">:</span> <span class="n">BookSource</span><span class="p">.</span><span class="n">external</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></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1">// 正確：拆成三個單一職責函式
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="n">Book</span> <span class="n">createBookFromISBN</span><span class="p">(</span><span class="kt">String</span> <span class="n">isbn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="n">validateISBNFormat</span><span class="p">(</span><span class="n">isbn</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="kd">final</span> <span class="n">bookData</span> <span class="o">=</span> <span class="n">fetchBookDataFromExternalAPI</span><span class="p">(</span><span class="n">isbn</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="k">return</span> <span class="n">buildBookFromExternalData</span><span class="p">(</span><span class="n">bookData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="kt">void</span> <span class="n">validateISBNFormat</span><span class="p">(</span><span class="kt">String</span> <span class="n">isbn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">isbn</span><span class="p">.</span><span class="n">length</span> <span class="o">!=</span> <span class="m">13</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(</span><span class="s1">&#39;ISBN must be 13 digits&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isValidISBNChecksum</span><span class="p">(</span><span class="n">isbn</span><span class="p">))</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(</span><span class="s1">&#39;ISBN checksum validation failed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">fetchBookDataFromExternalAPI</span><span class="p">(</span><span class="kt">String</span> <span class="n">isbn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="kd">final</span> <span class="n">apiResponse</span> <span class="o">=</span> <span class="n">httpClient</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">&#39;/books/</span><span class="si">$</span><span class="n">isbn</span><span class="s1">&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">apiResponse</span><span class="p">.</span><span class="n">statusCode</span> <span class="o">!=</span> <span class="m">200</span><span class="p">)</span> <span class="k">throw</span> <span class="n">Exception</span><span class="p">(</span><span class="s1">&#39;Failed to fetch book data&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="k">return</span> <span class="n">json</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">apiResponse</span><span class="p">.</span><span class="n">body</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="n">Book</span> <span class="n">buildBookFromExternalData</span><span class="p">(</span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">dynamic</span><span class="o">&gt;</span> <span class="n">bookData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">  <span class="k">return</span> <span class="n">Book</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="nl">id:</span> <span class="n">BookId</span><span class="p">(</span><span class="n">generateUniqueId</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nl">title:</span> <span class="n">BookTitle</span><span class="p">(</span><span class="n">bookData</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="k">source</span><span class="o">:</span> <span class="n">BookSource</span><span class="p">.</span><span class="n">external</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="第三原則一個變數只做一件事">第三原則：一個變數只做一件事</h2>
<p>同一個變數在不同地方承載不同意義，是我見過最難追蹤的 bug 來源之一。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 錯誤：同一變數三種身分
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="n">result</span> <span class="o">=</span> <span class="n">validateUser</span><span class="p">(</span><span class="n">userData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isValid</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">result</span> <span class="o">=</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">paymentData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">success</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">updateDatabase</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <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">// 正確：每個變數都有自己的名字
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">userValidationResult</span> <span class="o">=</span> <span class="n">validateUser</span><span class="p">(</span><span class="n">userData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">userValidationResult</span><span class="p">.</span><span class="n">isValid</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kd">final</span> <span class="n">paymentProcessingResult</span> <span class="o">=</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">paymentData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">paymentProcessingResult</span><span class="p">.</span><span class="n">success</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="kd">final</span> <span class="n">databaseUpdateResult</span> <span class="o">=</span> <span class="n">updateDatabase</span><span class="p">(</span><span class="n">paymentProcessingResult</span><span class="p">.</span><span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><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">// 錯誤：books 在100行間一直換狀態
</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">processLibraryBooks</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="n">books</span> <span class="o">=</span> <span class="n">getAllBooks</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">books</span> <span class="o">=</span> <span class="n">filterAvailableBooks</span><span class="p">(</span><span class="n">books</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">books</span> <span class="o">=</span> <span class="n">sortBooksByTitle</span><span class="p">(</span><span class="n">books</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 正確：每個階段的狀態都有名字
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">processLibraryBooks</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">final</span> <span class="n">allLibraryBooks</span> <span class="o">=</span> <span class="n">getAllBooks</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kd">final</span> <span class="n">availableBooks</span> <span class="o">=</span> <span class="n">filterAvailableBooks</span><span class="p">(</span><span class="n">allLibraryBooks</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="n">sortedAvailableBooks</span> <span class="o">=</span> <span class="n">sortBooksByTitle</span><span class="p">(</span><span class="n">availableBooks</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><hr>
<h2 id="第四原則用事件驅動表達業務流程">第四原則：用事件驅動表達業務流程</h2>
<p>複雜的業務流程往往會寫成一個大函式，裡面塞滿 if/else。問題不在於 if/else 本身，而是把不同職責的邏輯混在一起。</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">// 錯誤：驗證、API呼叫、結果處理全混在一起
</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">submitForm</span><span class="p">(</span><span class="n">FormData</span> <span class="n">formData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">formData</span><span class="p">.</span><span class="n">name</span><span class="p">.</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span> <span class="n">showErrorMessage</span><span class="p">(</span><span class="s1">&#39;姓名不能為空&#39;</span><span class="p">);</span> <span class="k">return</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">formData</span><span class="p">.</span><span class="n">email</span><span class="p">.</span><span class="n">isEmpty</span><span class="p">)</span> <span class="p">{</span> <span class="n">showErrorMessage</span><span class="p">(</span><span class="s1">&#39;Email不能為空&#39;</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isValidEmail</span><span class="p">(</span><span class="n">formData</span><span class="p">.</span><span class="n">email</span><span class="p">))</span> <span class="p">{</span> <span class="n">showErrorMessage</span><span class="p">(</span><span class="s1">&#39;Email格式不正確&#39;</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">final</span> <span class="n">apiResult</span> <span class="o">=</span> <span class="n">submitToAPI</span><span class="p">(</span><span class="n">formData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">apiResult</span><span class="p">.</span><span class="n">success</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">showSuccessMessage</span><span class="p">(</span><span class="s1">&#39;提交成功&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">navigateToSuccessPage</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">clearForm</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">showErrorMessage</span><span class="p">(</span><span class="s1">&#39;提交失敗：&#39;</span> <span class="o">+</span> <span class="n">apiResult</span><span class="p">.</span><span class="n">error</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">highlightErrorFields</span><span class="p">(</span><span class="n">apiResult</span><span class="p">.</span><span class="n">errorFields</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 正確：每個事件有自己的函式
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">submitUserRegistrationForm</span><span class="p">(</span><span class="n">UserRegistrationFormData</span> <span class="n">formData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="kd">final</span> <span class="n">validationResult</span> <span class="o">=</span> <span class="n">validateUserRegistrationData</span><span class="p">(</span><span class="n">formData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">validationResult</span><span class="p">.</span><span class="n">isValid</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">handleSuccessfulValidation</span><span class="p">(</span><span class="n">formData</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">handleValidationFailure</span><span class="p">(</span><span class="n">validationResult</span><span class="p">.</span><span class="n">errors</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="n">ValidationResult</span> <span class="n">validateUserRegistrationData</span><span class="p">(</span><span class="n">UserRegistrationFormData</span> <span class="n">formData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="kd">final</span> <span class="n">errors</span> <span class="o">=</span> <span class="o">&lt;</span><span class="n">ValidationError</span><span class="o">&gt;</span><span class="p">[];</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isValidUserName</span><span class="p">(</span><span class="n">formData</span><span class="p">.</span><span class="n">name</span><span class="p">))</span> <span class="n">errors</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">ValidationError</span><span class="p">.</span><span class="n">invalidUserName</span><span class="p">());</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isValidUserEmail</span><span class="p">(</span><span class="n">formData</span><span class="p">.</span><span class="n">email</span><span class="p">))</span> <span class="n">errors</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">ValidationError</span><span class="p">.</span><span class="n">invalidEmail</span><span class="p">());</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="k">return</span> <span class="n">ValidationResult</span><span class="p">.</span><span class="n">fromErrors</span><span class="p">(</span><span class="n">errors</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="kt">void</span> <span class="n">handleSuccessfulValidation</span><span class="p">(</span><span class="n">UserRegistrationFormData</span> <span class="n">formData</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">  <span class="n">submitUserRegistrationToAPI</span><span class="p">(</span><span class="n">formData</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="p">.</span><span class="n">then</span><span class="p">(</span><span class="n">handleSuccessfulAPIResponse</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="p">.</span><span class="n">catchError</span><span class="p">(</span><span class="n">handleAPIFailure</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="kt">void</span> <span class="n">handleValidationFailure</span><span class="p">(</span><span class="n">List</span><span class="o">&lt;</span><span class="n">ValidationError</span><span class="o">&gt;</span> <span class="n">errors</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">  <span class="n">displayValidationErrors</span><span class="p">(</span><span class="n">errors</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">  <span class="n">highlightInvalidFormFields</span><span class="p">(</span><span class="n">errors</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><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">// 錯誤：一個函式根據狀態做完全不同的事
</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">updateBookStatus</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">,</span> <span class="kt">String</span> <span class="n">newStatus</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">newStatus</span> <span class="o">==</span> <span class="s1">&#39;available&#39;</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">book</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">BookStatus</span><span class="p">.</span><span class="n">available</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">book</span><span class="p">.</span><span class="n">borrower</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">updateSearchIndex</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">notifyWaitingUsers</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">newStatus</span> <span class="o">==</span> <span class="s1">&#39;borrowed&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">book</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">BookStatus</span><span class="p">.</span><span class="n">borrowed</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">book</span><span class="p">.</span><span class="n">borrowDate</span> <span class="o">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">now</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">sendBorrowConfirmation</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">newStatus</span> <span class="o">==</span> <span class="s1">&#39;maintenance&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">book</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">BookStatus</span><span class="p">.</span><span class="n">maintenance</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">removeFromSearchIndex</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">notifyMaintenanceTeam</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1">// 正確：每個事件獨立
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handleBookReturnEvent</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="n">executeBookReturn</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="n">notifyBookBecameAvailable</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="kt">void</span> <span class="n">handleBookBorrowEvent</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">,</span> <span class="n">User</span> <span class="n">borrower</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="n">executeBookBorrow</span><span class="p">(</span><span class="n">book</span><span class="p">,</span> <span class="n">borrower</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="n">confirmBorrowingToUser</span><span class="p">(</span><span class="n">book</span><span class="p">,</span> <span class="n">borrower</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="kt">void</span> <span class="n">handleBookMaintenanceEvent</span><span class="p">(</span><span class="n">Book</span> <span class="n">book</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="n">markBookForMaintenance</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="n">notifyMaintenanceRequired</span><span class="p">(</span><span class="n">book</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="第五原則可讀性優於簡潔性">第五原則：可讀性優於簡潔性</h2>
<p>程式碼的價值排序：正確性 &gt; 可讀性 &gt; 可維護性 &gt; 簡潔性。</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">// 錯誤：為了少寫幾行犧牲可讀性
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">books</span><span class="p">.</span><span class="n">where</span><span class="p">((</span><span class="n">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">b</span><span class="p">.</span><span class="n">s</span> <span class="o">==</span> <span class="s1">&#39;a&#39;</span> <span class="o">&amp;&amp;</span> <span class="n">b</span><span class="p">.</span><span class="n">p</span> <span class="o">&gt;</span> <span class="m">100</span><span class="p">).</span><span class="n">map</span><span class="p">((</span><span class="n">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">b</span><span class="p">.</span><span class="n">t</span><span class="p">).</span><span class="n">toList</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="kd">final</span> <span class="n">availableBooksWithMoreThan100Pages</span> <span class="o">=</span> <span class="n">allBooks</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">.</span><span class="n">where</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">book</span><span class="p">.</span><span class="n">status</span> <span class="o">==</span> <span class="n">BookStatus</span><span class="p">.</span><span class="n">available</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">where</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">book</span><span class="p">.</span><span class="n">pageCount</span> <span class="o">&gt;</span> <span class="m">100</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">toList</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">final</span> <span class="n">bookTitlesForDisplay</span> <span class="o">=</span> <span class="n">availableBooksWithMoreThan100Pages</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="p">.</span><span class="n">map</span><span class="p">((</span><span class="n">book</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">book</span><span class="p">.</span><span class="n">title</span><span class="p">.</span><span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">.</span><span class="n">toList</span><span class="p">();</span></span></span></code></pre></div><hr>
<h2 id="如何驗證程式碼品質">如何驗證程式碼品質</h2>
<p><strong>陌生人測試</strong>：讓不熟悉這段程式碼的工程師讀。5分鐘內能理解主要邏輯算合格，需要解釋才能理解就要重寫。</p>
<p><strong>自然語言測試</strong>：把程式碼翻譯成中文說出來。翻譯流暢自然算合格，說不清楚就改命名。</p>
<p><strong>六個月後測試</strong>：假設半年後的自己要修改這段程式碼，能快速找到位置算合格，不敢動怕壞掉就要重新設計。</p>
<hr>
<p>每一行程式碼都是一句話，每個函式都是一個段落。好的程式碼是對未來維護者的體貼——不只是風格偏好，而是降低維護成本的工程決策。</p>]]></content:encoded></item><item><title>錯誤修復和重構方法論</title><link>https://tarrragon.github.io/blog/record/%E9%8C%AF%E8%AA%A4%E4%BF%AE%E5%BE%A9%E5%92%8C%E9%87%8D%E6%A7%8B%E6%96%B9%E6%B3%95%E8%AB%96/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E9%8C%AF%E8%AA%A4%E4%BF%AE%E5%BE%A9%E5%92%8C%E9%87%8D%E6%A7%8B%E6%96%B9%E6%B3%95%E8%AB%96/</guid><description>&lt;p>測試失敗了，應該修改程式，還是修改測試？&lt;/p>
&lt;p>這個判斷決定了整個修復方向。判斷錯，我們會花時間做出一個「讓測試通過」但實際上破壞需求的修改。&lt;/p></description><content:encoded><![CDATA[<p>測試失敗了，應該修改程式，還是修改測試？</p>
<p>這個判斷決定了整個修復方向。判斷錯，我們會花時間做出一個「讓測試通過」但實際上破壞需求的修改。</p>
<h2 id="核心原則程式服務測試測試服務需求">核心原則：程式服務測試，測試服務需求</h2>
<p>測試代表需求的具體描述。為了讓測試通過而修改測試本身，等於在需求上妥協。正確的做法是保持測試不變，調整程式實作直到符合期望。</p>
<p>唯一的例外：需求本身發生了變化——架構調整、業務流程重設計。這兩種情況的判斷，就是整個方法論的核心。</p>
<h2 id="第一步分類">第一步：分類</h2>
<p>面對測試失敗，先問「為什麼失敗」，不是「怎麼修」。</p>
<p><strong>程式實作錯誤</strong>：需求沒變，但程式行為不符預期——錯誤輸出、邏輯判斷有誤、型別處理不當。處理方式直接：保持測試不變，修正程式。</p>
<p>最容易犯的錯誤，是在這種情況下改了測試的期望值，讓測試配合錯誤的程式。這等於讓錯誤行為成為「正確需求」。</p>
<p><strong>架構變更需求</strong>：需求文件已更新，業務流程本質性改變，影響多個模組。這類情況確實需要調整測試，但前提是需求文件已反映這個變更。步驟：先確認文件、評估變更範圍、列出需修改的測試，最後才執行。跳過前置確認直接改測試，和無章法地亂改沒有區別。</p>
<h2 id="觀測公開行為不觀測內部實作">觀測公開行為，不觀測內部實作</h2>
<p>我們只驗證透過公開介面的輸入輸出、公開屬性的狀態變化。不該碰私有方法的調用順序、私有屬性的中間值。</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">// 錯誤：觀測內部私有屬性
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;書籍驗證應該檢查所有欄位&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">final</span> <span class="n">validator</span> <span class="o">=</span> <span class="n">BookValidator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">validator</span><span class="p">.</span><span class="n">validate</span><span class="p">(</span><span class="n">invalidBook</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="n">expect</span><span class="p">(</span><span class="n">validator</span><span class="p">.</span><span class="n">_titleValidated</span><span class="p">,</span> <span class="n">isTrue</span><span class="p">);</span>   <span class="c1">// 內部狀態
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">validator</span><span class="p">.</span><span class="n">_isbnValidated</span><span class="p">,</span> <span class="n">isTrue</span><span class="p">);</span>    <span class="c1">// 私有屬性
</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></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 正確：觀測公開行為結果
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;書籍驗證應該檢查所有欄位&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">final</span> <span class="n">validator</span> <span class="o">=</span> <span class="n">BookValidator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="n">validator</span><span class="p">.</span><span class="n">validate</span><span class="p">(</span><span class="n">invalidBook</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isValid</span><span class="p">,</span> <span class="n">isFalse</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">errors</span><span class="p">,</span> <span class="n">contains</span><span class="p">(</span><span class="s1">&#39;標題不可為空&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">errors</span><span class="p">,</span> <span class="n">contains</span><span class="p">(</span><span class="s1">&#39;ISBN 格式錯誤&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h2 id="三個常見的反模式">三個常見的反模式</h2>
<p><strong>測試遷就程式</strong>：程式返回錯誤值，開發者把測試期望值改成那個錯誤值。測試通過了，bug 也永遠存在了。正確做法：保持期望值不動，修正程式邏輯。</p>
<p><strong>內部狀態依賴</strong>：測試直接存取 <code>book._internalState</code> 來驗證操作。一旦重構就失敗。改用公開方法 <code>book.isAvailable()</code> 驗證行為結果。</p>
<p><strong>跳過文件檢查</strong>：覺得需求「應該」要改，就直接改測試，沒確認需求文件是否更新。正確做法：先查規格書，確認文件已反映變更，再評估影響範圍，最後才動手。</p>
<h2 id="驗收標準">驗收標準</h2>
<p>修復前：錯誤類型已判斷、需求文件狀態已確認、受影響測試範圍已識別。</p>
<p>修復後：所有測試 100% 通過、無內部狀態曝露、無行為旁路、每個修改都有測試保護。</p>
<p>100% 通過率是底線。某個測試通過不了，不是跳過它或修改它，是找出真正的問題。</p>]]></content:encoded></item></channel></rss>