<?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/%E6%8A%80%E8%A1%93%E5%82%B5%E5%8B%99/</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/%E6%8A%80%E8%A1%93%E5%82%B5%E5%8B%99/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></channel></rss>