<?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/python/07-refactoring/</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>Tue, 20 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/python/07-refactoring/index.xml" rel="self" type="application/rss+xml"/><item><title>重構的動機與策略</title><link>https://tarrragon.github.io/blog/python/07-refactoring/refactoring-strategy/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/refactoring-strategy/</guid><description>&lt;p>你有沒有修完一個 Bug，一週後發現另一個地方有完全一樣的問題？或者改了一個函式的行為，卻不確定還有哪些地方依賴它？這些情境的根源往往是程式碼結構讓問題難以被看見。&lt;/p>
&lt;p>重構是改變程式碼的內部結構，而不改變其外部行為。聽起來簡單，但實務上最困難的問題不是「怎麼改」，而是「為什麼改」和「什麼順序改」。&lt;/p>
&lt;p>本章從 Hook 系統兩次大規模重構（v0.28.0 和 v0.31.0）的經驗出發，討論重構的動機判斷和階段分解策略。Hook 系統是一個由數十個 Python 腳本組成的自動化系統，負責程式碼品質檢查、流程驗證和開發規範執行。&lt;/p>
&lt;h2 id="為什麼要重構">為什麼要重構&lt;/h2>
&lt;h3 id="認知負擔超載">認知負擔超載&lt;/h3>
&lt;p>重構的第一個訊號是：&lt;strong>讀程式碼時，你需要同時記住太多東西。&lt;/strong>&lt;/p>
&lt;p>v0.28.0 重構前，&lt;code>task-dispatch-readiness-check.py&lt;/code> 有 858 行。閱讀這個檔案時，你需要同時追蹤：&lt;/p>
&lt;ul>
&lt;li>15 個代理人的名稱和觸發條件&lt;/li>
&lt;li>Git 分支操作的 &lt;code>subprocess&lt;/code> 細節&lt;/li>
&lt;li>Worktree 路徑解析邏輯&lt;/li>
&lt;li>重複出現的工具函式（同一個 &lt;code>run_git_command&lt;/code> 在多個檔案中各自定義一次）&lt;/li>
&lt;/ul>
&lt;p>心理學家 George Miller 的研究指出，人類的工作記憶一次只能處理 7 加減 2 個項目。858 行的檔案遠超這個限制。你不可能在腦中同時維護這麼多上下文來理解程式碼的行為。&lt;/p>
&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">認知負擔指數 = 變數數 + 分支數 + 巢狀深度 + 依賴數&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指數&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>行動&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1-5&lt;/td>
 &lt;td>優良&lt;/td>
 &lt;td>維持&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6-10&lt;/td>
 &lt;td>可接受&lt;/td>
 &lt;td>考慮最佳化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>11-15&lt;/td>
 &lt;td>需重構&lt;/td>
 &lt;td>排入計畫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;gt; 15&lt;/td>
 &lt;td>必須重構&lt;/td>
 &lt;td>立即處理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>v0.28.0 重構前的 &lt;code>task-dispatch-readiness-check.py&lt;/code>，光是頂層函式就有 23 個，模組級變數超過 10 個。認知負擔指數遠超 15，屬於「必須重構」等級。&lt;/p>
&lt;h3 id="不重構的代價">不重構的代價&lt;/h3>
&lt;p>「能動就不要碰」是常見的想法，但不重構的代價會隨時間累積。&lt;/p>
&lt;p>v0.31.0 的 W24 開發週期提供了一個具體案例。任務是統一 16 個 Hook 檔案的 logger 初始化風格——看起來是一個簡單的機械性修改。但因為缺乏共用模組和清晰的模組邊界，修改引發了一個作用域問題（將全域變數移入函式後，其他引用該變數的函式失去存取權限），導致 7 個 Hook 靜默失敗，影響 41 個函式。更糟的是，這個問題至少持續了 2 個 session 才被發現。&lt;/p>
&lt;blockquote>
&lt;p>完整的作用域迴歸分析參見&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>靜默失敗比直接報錯更危險。錯誤被頂層的 &lt;code>try/except&lt;/code> 吞掉，只寫入日誌檔案，而沒有人在看日誌。如果重構前就有清晰的模組邊界和完整的測試覆蓋，這個問題可以在修改當下就被偵測到。&lt;/p>
&lt;h3 id="重複程式碼的連鎖效應">重複程式碼的連鎖效應&lt;/h3>
&lt;p>另一個推動重構的因素是重複。我們用一行指令就能量化問題的嚴重程度：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 在專案根目錄執行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">grep -h &lt;span class="s2">&amp;#34;^def &amp;#34;&lt;/span> .claude/hooks/*.py &lt;span class="p">|&lt;/span> sort &lt;span class="p">|&lt;/span> uniq -c &lt;span class="p">|&lt;/span> sort -rn &lt;span class="p">|&lt;/span> head -5&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>v0.28.0 重構前的結果顯示，&lt;code>run_git_command&lt;/code> 在多個檔案中各有一份定義。這意味著：&lt;/p>
&lt;ul>
&lt;li>修復一個 Bug 要改 4 個地方&lt;/li>
&lt;li>漏改任何一個就會產生行為不一致&lt;/li>
&lt;li>新增 Hook 時需要再複製一份&lt;/li>
&lt;/ul>
&lt;p>重複程式碼的總行數約 415 行。這 415 行不只是浪費空間——它們是 415 行的維護風險。&lt;/p>
&lt;h3 id="判斷三問">判斷三問&lt;/h3>
&lt;p>在決定是否重構之前，問自己三個問題：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>讀這段程式碼時，我需要同時記住多少東西？&lt;/strong> 超過 7 個就是警訊。&lt;/li>
&lt;li>&lt;strong>如果要修改一個行為，我需要改幾個地方？&lt;/strong> 超過 1 個就有重複的問題。&lt;/li>
&lt;li>&lt;strong>新人加入團隊後，需要多久才能理解這段程式碼？&lt;/strong> 如果答案是「需要有人口頭解釋」，那就是程式碼本身不夠清楚。&lt;/li>
&lt;/ol>
&lt;p>如果三個問題的答案都指向問題，那就該動手了。&lt;/p>
&lt;h2 id="何時不應該重構">何時不應該重構&lt;/h2>
&lt;p>不是所有情況都適合重構。以下三個時機，重構通常會帶來更多問題。&lt;/p>
&lt;h3 id="沒有測試保護時">沒有測試保護時&lt;/h3>
&lt;p>重構的前提是：你能驗證修改後的行為和修改前一致。沒有測試，你就無法確認這一點。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 假設你想把這段程式碼抽成函式&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">())&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">branches&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">branches&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">b&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">branches&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">protected&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">b&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">branches&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">b&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">]]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段邏輯涉及空字串處理、換行符分割、空白清理。如果沒有測試覆蓋這些邊界情況，抽取函式時很容易改變行為而不自知。&lt;/p>
&lt;p>&lt;strong>原則&lt;/strong>：先寫測試，再重構。如果時間只夠做一件事，選擇寫測試。&lt;/p>
&lt;h3 id="修-bug-時順手重構">修 Bug 時順手重構&lt;/h3>
&lt;p>修 Bug 和重構是兩件不同的事。混在一起做會產生兩個問題：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>難以驗證&lt;/strong>：Bug 修好了嗎？還是被重構掩蓋了？&lt;/li>
&lt;li>&lt;strong>難以回溯&lt;/strong>：如果重構引入了新問題，&lt;code>git bisect&lt;/code> 無法區分哪些變更是修 Bug、哪些是重構&lt;/li>
&lt;/ol>





&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"># 錯誤的工作流程
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">commit: &amp;#34;修復 #42 並重構 git_utils&amp;#34; ← 兩件事混在一個 commit
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">commit: &amp;#34;修復 #42：branch 名稱解析的空字串處理&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">commit: &amp;#34;重構 git_utils：抽取 parse_branch_name 函式&amp;#34;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>原則&lt;/strong>：先修好 Bug 並提交，確認測試通過，然後再開始重構。&lt;/p>
&lt;h3 id="時間壓力下">時間壓力下&lt;/h3>
&lt;p>v0.31.0 W24 的 logger 統一修改就是一個教訓——在時間壓力下跳過了跨函式引用的完整驗證，結果 7 個 Hook 靜默失敗，修復花費的時間遠超原本省下的。&lt;/p></description><content:encoded><![CDATA[<p>你有沒有修完一個 Bug，一週後發現另一個地方有完全一樣的問題？或者改了一個函式的行為，卻不確定還有哪些地方依賴它？這些情境的根源往往是程式碼結構讓問題難以被看見。</p>
<p>重構是改變程式碼的內部結構，而不改變其外部行為。聽起來簡單，但實務上最困難的問題不是「怎麼改」，而是「為什麼改」和「什麼順序改」。</p>
<p>本章從 Hook 系統兩次大規模重構（v0.28.0 和 v0.31.0）的經驗出發，討論重構的動機判斷和階段分解策略。Hook 系統是一個由數十個 Python 腳本組成的自動化系統，負責程式碼品質檢查、流程驗證和開發規範執行。</p>
<h2 id="為什麼要重構">為什麼要重構</h2>
<h3 id="認知負擔超載">認知負擔超載</h3>
<p>重構的第一個訊號是：<strong>讀程式碼時，你需要同時記住太多東西。</strong></p>
<p>v0.28.0 重構前，<code>task-dispatch-readiness-check.py</code> 有 858 行。閱讀這個檔案時，你需要同時追蹤：</p>
<ul>
<li>15 個代理人的名稱和觸發條件</li>
<li>Git 分支操作的 <code>subprocess</code> 細節</li>
<li>Worktree 路徑解析邏輯</li>
<li>重複出現的工具函式（同一個 <code>run_git_command</code> 在多個檔案中各自定義一次）</li>
</ul>
<p>心理學家 George Miller 的研究指出，人類的工作記憶一次只能處理 7 加減 2 個項目。858 行的檔案遠超這個限制。你不可能在腦中同時維護這麼多上下文來理解程式碼的行為。</p>
<p>我們可以用一個簡單的公式量化這個問題：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">認知負擔指數 = 變數數 + 分支數 + 巢狀深度 + 依賴數</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>指數</th>
          <th>評估</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-5</td>
          <td>優良</td>
          <td>維持</td>
      </tr>
      <tr>
          <td>6-10</td>
          <td>可接受</td>
          <td>考慮最佳化</td>
      </tr>
      <tr>
          <td>11-15</td>
          <td>需重構</td>
          <td>排入計畫</td>
      </tr>
      <tr>
          <td>&gt; 15</td>
          <td>必須重構</td>
          <td>立即處理</td>
      </tr>
  </tbody>
</table>
<p>v0.28.0 重構前的 <code>task-dispatch-readiness-check.py</code>，光是頂層函式就有 23 個，模組級變數超過 10 個。認知負擔指數遠超 15，屬於「必須重構」等級。</p>
<h3 id="不重構的代價">不重構的代價</h3>
<p>「能動就不要碰」是常見的想法，但不重構的代價會隨時間累積。</p>
<p>v0.31.0 的 W24 開發週期提供了一個具體案例。任務是統一 16 個 Hook 檔案的 logger 初始化風格——看起來是一個簡單的機械性修改。但因為缺乏共用模組和清晰的模組邊界，修改引發了一個作用域問題（將全域變數移入函式後，其他引用該變數的函式失去存取權限），導致 7 個 Hook 靜默失敗，影響 41 個函式。更糟的是，這個問題至少持續了 2 個 session 才被發現。</p>
<blockquote>
<p>完整的作用域迴歸分析參見<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a>。</p></blockquote>
<p>靜默失敗比直接報錯更危險。錯誤被頂層的 <code>try/except</code> 吞掉，只寫入日誌檔案，而沒有人在看日誌。如果重構前就有清晰的模組邊界和完整的測試覆蓋，這個問題可以在修改當下就被偵測到。</p>
<h3 id="重複程式碼的連鎖效應">重複程式碼的連鎖效應</h3>
<p>另一個推動重構的因素是重複。我們用一行指令就能量化問題的嚴重程度：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在專案根目錄執行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -h <span class="s2">&#34;^def &#34;</span> .claude/hooks/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -5</span></span></code></pre></div><p>v0.28.0 重構前的結果顯示，<code>run_git_command</code> 在多個檔案中各有一份定義。這意味著：</p>
<ul>
<li>修復一個 Bug 要改 4 個地方</li>
<li>漏改任何一個就會產生行為不一致</li>
<li>新增 Hook 時需要再複製一份</li>
</ul>
<p>重複程式碼的總行數約 415 行。這 415 行不只是浪費空間——它們是 415 行的維護風險。</p>
<h3 id="判斷三問">判斷三問</h3>
<p>在決定是否重構之前，問自己三個問題：</p>
<ol>
<li><strong>讀這段程式碼時，我需要同時記住多少東西？</strong> 超過 7 個就是警訊。</li>
<li><strong>如果要修改一個行為，我需要改幾個地方？</strong> 超過 1 個就有重複的問題。</li>
<li><strong>新人加入團隊後，需要多久才能理解這段程式碼？</strong> 如果答案是「需要有人口頭解釋」，那就是程式碼本身不夠清楚。</li>
</ol>
<p>如果三個問題的答案都指向問題，那就該動手了。</p>
<h2 id="何時不應該重構">何時不應該重構</h2>
<p>不是所有情況都適合重構。以下三個時機，重構通常會帶來更多問題。</p>
<h3 id="沒有測試保護時">沒有測試保護時</h3>
<p>重構的前提是：你能驗證修改後的行為和修改前一致。沒有測試，你就無法確認這一點。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">branches</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">branches</span> <span class="o">=</span> <span class="p">[</span><span class="n">b</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">branches</span> <span class="k">if</span> <span class="n">b</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">protected</span> <span class="o">=</span> <span class="p">[</span><span class="n">b</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">branches</span> <span class="k">if</span> <span class="n">b</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">]]</span></span></span></code></pre></div><p>這段邏輯涉及空字串處理、換行符分割、空白清理。如果沒有測試覆蓋這些邊界情況，抽取函式時很容易改變行為而不自知。</p>
<p><strong>原則</strong>：先寫測試，再重構。如果時間只夠做一件事，選擇寫測試。</p>
<h3 id="修-bug-時順手重構">修 Bug 時順手重構</h3>
<p>修 Bug 和重構是兩件不同的事。混在一起做會產生兩個問題：</p>
<ol>
<li><strong>難以驗證</strong>：Bug 修好了嗎？還是被重構掩蓋了？</li>
<li><strong>難以回溯</strong>：如果重構引入了新問題，<code>git bisect</code> 無法區分哪些變更是修 Bug、哪些是重構</li>
</ol>





<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"># 錯誤的工作流程
</span></span><span class="line"><span class="ln">2</span><span class="cl">commit: &#34;修復 #42 並重構 git_utils&#34;  ← 兩件事混在一個 commit
</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></span><span class="line"><span class="ln">5</span><span class="cl">commit: &#34;修復 #42：branch 名稱解析的空字串處理&#34;
</span></span><span class="line"><span class="ln">6</span><span class="cl">commit: &#34;重構 git_utils：抽取 parse_branch_name 函式&#34;</span></span></code></pre></div><p><strong>原則</strong>：先修好 Bug 並提交，確認測試通過，然後再開始重構。</p>
<h3 id="時間壓力下">時間壓力下</h3>
<p>v0.31.0 W24 的 logger 統一修改就是一個教訓——在時間壓力下跳過了跨函式引用的完整驗證，結果 7 個 Hook 靜默失敗，修復花費的時間遠超原本省下的。</p>
<p>重構需要完整的注意力。趕進度時進行重構，容易在壓力下跳過驗證步驟（「測試之後再補」），反而製造更多技術債務。</p>
<p><strong>原則</strong>：記錄下需要重構的地方，排入後續計畫。不要在時間壓力下動手。</p>
<h3 id="判斷清單">判斷清單</h3>
<p>把上述三個情境整理成一個快速檢查清單：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>是</th>
          <th>否</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>有測試覆蓋修改區域？</td>
          <td>可以繼續</td>
          <td>先寫測試</td>
      </tr>
      <tr>
          <td>修改範圍只有重構？</td>
          <td>可以繼續</td>
          <td>先分開 Bug 修復和重構</td>
      </tr>
      <tr>
          <td>有足夠的時間完成驗證？</td>
          <td>可以繼續</td>
          <td>記錄後排入計畫</td>
      </tr>
  </tbody>
</table>
<p>三個問題都回答「是」，才開始動手。</p>
<h2 id="wave-分解策略">Wave 分解策略</h2>
<p>大規模重構最容易失敗的原因是：試圖一次做完所有事情。</p>
<blockquote>
<p><strong>Wave</strong>：一個有明確目標和驗證點的重構階段。每完成一個 Wave，程式碼都必須處於可用狀態。</p></blockquote>
<p>Wave 分解的核心思想是：<strong>把重構拆成多個有序的 Wave，確保每一步都可驗證、可回退。</strong></p>
<h3 id="v0280基礎架構重構">v0.28.0：基礎架構重構</h3>
<p>v0.28.0 將 Hook 系統從「各自為政」重構為「共用模組 + 配置分離」的架構。拆分為 4 個 Wave：</p>
<h4 id="wave-1建立共用程式庫">Wave 1：建立共用程式庫</h4>
<p>先建立共用模組的介面和測試，不改動任何現有 Hook。</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">建立模組    →  寫測試    →  確認測試通過
</span></span><span class="line"><span class="ln">2</span><span class="cl">config_loader    4 個測試
</span></span><span class="line"><span class="ln">3</span><span class="cl">git_utils        6 個測試
</span></span><span class="line"><span class="ln">4</span><span class="cl">hook_io          3 個測試
</span></span><span class="line"><span class="ln">5</span><span class="cl">hook_logging     2 個測試</span></span></code></pre></div><p>為什麼先做這步？因為後續 Wave 需要依賴這些模組。如果跳過這步直接改 Hook，會遇到「要用的函式還不存在」的問題。</p>
<h4 id="wave-2配置分離">Wave 2：配置分離</h4>
<p>把 task-dispatch 中的硬編碼清單（代理人名稱、品質規則、指令對應表）抽到 YAML 配置檔。這是行數最多、修改範圍最大的階段。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 重構前：硬編碼在 Python 中</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">AGENTS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;parsley-flutter-developer&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;implementation&#34;</span><span class="p">,</span> <span class="s2">&#34;lang&#34;</span><span class="p">:</span> <span class="s2">&#34;dart&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;thyme-python-developer&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;implementation&#34;</span><span class="p">,</span> <span class="s2">&#34;lang&#34;</span><span class="p">:</span> <span class="s2">&#34;python&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="c1"># ... 15 個代理人定義散落在程式碼中</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"># 重構後：讀取 YAML 配置</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">agents</span> <span class="o">=</span> <span class="n">config_loader</span><span class="o">.</span><span class="n">load_config</span><span class="p">(</span><span class="s2">&#34;agents.yaml&#34;</span><span class="p">)</span></span></span></code></pre></div><p>分離後，新增代理人只需要編輯 YAML 檔案，不需要動 Python 程式碼。</p>
<h4 id="wave-3逐檔重構">Wave 3：逐檔重構</h4>
<p>有了共用程式庫和配置檔，逐一修改 Hook 檔案。每改完一個檔案就執行測試，確保沒改壞東西。這個階段的關鍵是<strong>紀律</strong>：每次只改一個檔案，改完就跑測試，不要累積多個修改後一起驗證。</p>
<h4 id="wave-4驗證與清理">Wave 4：驗證與清理</h4>
<p>28 個單元測試全部通過。移除不再需要的重複程式碼。檢查是否有遺漏的相依性。</p>
<h3 id="v0310風格統一與防護強化">v0.31.0：風格統一與防護強化</h3>
<p>v0.31.0 的重構是在既有架構上統一風格和強化防護，規模與性質都與 v0.28.0 不同。拆分為 4 個 Wave（W22-W25）：</p>
<table>
  <thead>
      <tr>
          <th>Wave</th>
          <th>目標</th>
          <th>性質</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>W22</td>
          <td>日誌系統統一</td>
          <td>機械性修改</td>
      </tr>
      <tr>
          <td>W23</td>
          <td>訊息常數抽取</td>
          <td>機械性修改</td>
      </tr>
      <tr>
          <td>W24</td>
          <td>程式碼風格統一</td>
          <td>機械性，但觸發了作用域問題</td>
      </tr>
      <tr>
          <td>W25</td>
          <td>修復 W24 問題 + 防護機制</td>
          <td>修復 + 新功能</td>
      </tr>
  </tbody>
</table>
<p>注意 W25 的存在。它不在原始計畫中，而是 W24 出問題後臨時新增的。<strong>好的分解策略要預留處理意外的空間。</strong></p>
<p>兩次重構的對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>v0.28.0</th>
          <th>v0.31.0</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>性質</td>
          <td>建立新架構</td>
          <td>統一既有架構的風格</td>
      </tr>
      <tr>
          <td>風險來源</td>
          <td>介面設計是否正確</td>
          <td>機械性修改是否有隱含的邏輯變更</td>
      </tr>
      <tr>
          <td>Wave 數</td>
          <td>4（全部計畫內）</td>
          <td>4（第 4 個是意外新增）</td>
      </tr>
      <tr>
          <td>教訓</td>
          <td>先建基礎設施再動手</td>
          <td>機械性修改也可能觸發邏輯問題</td>
      </tr>
  </tbody>
</table>
<h3 id="wave-分解的原則">Wave 分解的原則</h3>
<p>從兩次重構經驗中，可以歸納出三個分解原則：</p>
<h4 id="原則-1依賴方向決定順序">原則 1：依賴方向決定順序</h4>
<p>被依賴的模組先做。v0.28.0 先建共用程式庫（Wave 1），因為後續所有 Wave 都會用到它。</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">Wave 1: 共用模組（被所有 Hook 依賴）
</span></span><span class="line"><span class="ln">2</span><span class="cl">    ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">Wave 2: 配置檔（被 task-dispatch 依賴）
</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">Wave 3: Hook 檔案（依賴共用模組和配置檔）
</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">Wave 4: 驗證（依賴所有修改完成）</span></span></code></pre></div><h4 id="原則-2機械性修改和邏輯修改分開">原則 2：機械性修改和邏輯修改分開</h4>
<p>機械性修改（如統一命名風格、統一匯入路徑）和邏輯修改（如改變函式行為、改變變數作用域）不該放在同一個 Wave。</p>
<p>v0.31.0 的 W24 表面上是機械性修改（統一 logger 初始化位置），但實際上涉及了作用域的邏輯變更——把全域變數移入函式，會影響所有引用它的其他函式。如果在規劃時就識別出這一點，應該把「移動 logger 位置」和「修改函式簽名以傳遞 logger 參數」拆成兩個步驟。</p>
<p>怎麼區分？問自己：<strong>這個修改會不會改變任何函式的可見變數？</strong> 如果會，就不是純機械性修改。</p>
<h4 id="原則-3每個-wave-結束時程式碼必須可用">原則 3：每個 Wave 結束時程式碼必須可用</h4>
<p>不能出現「改到一半，程式跑不起來」的狀態。每個 Wave 完成後：</p>
<ul>
<li>所有測試通過</li>
<li>程式碼可正常執行</li>
<li>可以安全地提交</li>
</ul>
<p>這保證了即使中途需要停下來處理其他事情，程式碼也不會處於損壞狀態。</p>
<h3 id="分解流程">分解流程</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">Step 1: 畫出依賴關係圖
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    → 哪些模組被其他模組依賴？（先做）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    → 哪些模組互相獨立？（可以並行）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">Step 2: 分類修改類型
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    → 機械性修改：統一命名、統一格式、統一匯入
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    → 邏輯修改：改變函式行為、改變資料流、改變作用域
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Step 3: 分配到 Wave
</span></span><span class="line"><span class="ln">10</span><span class="cl">    → Wave N: 基礎設施（被依賴的、獨立的）
</span></span><span class="line"><span class="ln">11</span><span class="cl">    → Wave N+1: 機械性修改（依賴基礎設施）
</span></span><span class="line"><span class="ln">12</span><span class="cl">    → Wave N+2: 邏輯修改（依賴前面的修改）
</span></span><span class="line"><span class="ln">13</span><span class="cl">    → Wave N+3: 驗證與清理
</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">Step 4: 每個 Wave 定義驗證標準
</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></span><span class="line"><span class="ln">18</span><span class="cl">    → 可以安全提交嗎？</span></span></code></pre></div><h2 id="度量表">度量表</h2>
<p>用量化指標驗證重構是否達到目標：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>v0.28.0 前</th>
          <th>v0.28.0 後</th>
          <th>v0.31.0 後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>共用模組數</td>
          <td>0</td>
          <td>4</td>
          <td>7+</td>
      </tr>
      <tr>
          <td>重複程式碼行數</td>
          <td>~415 行</td>
          <td>~0</td>
          <td>~0</td>
      </tr>
      <tr>
          <td>新增 Hook 的樣板行數</td>
          <td>~15 行</td>
          <td>~10 行</td>
          <td>~5 行（核心呼叫 1 行）</td>
      </tr>
      <tr>
          <td>Error Patterns 記錄數</td>
          <td>4</td>
          <td>8</td>
          <td>19</td>
      </tr>
      <tr>
          <td>task-dispatch 行數</td>
          <td>858</td>
          <td>296</td>
          <td>267</td>
      </tr>
  </tbody>
</table>
<p>幾個值得注意的趨勢：</p>
<ul>
<li><strong>共用模組數</strong>從 0 成長到 7+。這代表重複程式碼有了歸屬，不再散落各處。</li>
<li><strong>新增 Hook 的樣板行數</strong>從 15 行降到約 5 行（核心只需 1 行匯入呼叫）。新增一個 Hook 從「複製一堆工具函式」變成「匯入共用模組」。</li>
<li><strong>Error Patterns</strong> 從 4 成長到 19。這不是壞事——它代表團隊開始系統性地記錄和傳承經驗，而不是每個人各自出問題。</li>
</ul>
<h2 id="小結">小結</h2>
<p>重構的決策可以歸納為三個問題：</p>
<ol>
<li><strong>該不該做？</strong> 認知負擔超載、重複程式碼累積、維護成本持續上升，就該做。</li>
<li><strong>現在能做嗎？</strong> 有測試保護、不在修 Bug、時間充裕，才能做。</li>
<li><strong>怎麼拆分？</strong> 按依賴順序，機械和邏輯分開，每步結束都可用。</li>
</ol>
<p>後續章節會深入每個具體的重構技巧：如何識別壞味道、如何抽取配置、如何消除重複、如何處理作用域陷阱。</p>
<h2 id="思考題">思考題</h2>
<ol>
<li>
<p>你目前的專案中，有哪些檔案的行數超過 200 行？列出前三名，分析它們為什麼會這麼長——是職責太多、重複程式碼、還是配置和邏輯混在一起？</p>
</li>
<li>
<p>回想一次你在修 Bug 時順手重構的經驗。事後回頭看 <code>git log</code>，能清楚區分哪些變更是修 Bug、哪些是重構嗎？</p>
</li>
<li>
<p>如果你的專案完全沒有測試，但認知負擔已經很高，你會怎麼規劃「補測試」和「重構」的先後順序？</p>
</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>
<p>用以下指令掃描你的專案，找出重複定義的函式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在專案根目錄執行</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -h <span class="s2">&#34;^def &#34;</span> your_project/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -10</span></span></code></pre></div><p>分析結果：哪些函式被重複定義了？它們應該被抽到哪個共用模組？</p>
</li>
<li>
<p>選一個超過 200 行的檔案，嘗試畫出它的 Wave 分解計畫。回答以下問題：</p>
<ul>
<li>哪些部分被其他部分依賴？（先做）</li>
<li>哪些修改是機械性的？（可以批量處理）</li>
<li>每個 Wave 完成後，程式碼能正常執行嗎？</li>
</ul>
</li>
<li>
<p>計算你選定檔案的認知負擔指數（變數數 + 分支數 + 巢狀深度 + 依賴數）。找出指數最高的函式，思考如何將它拆分到指數低於 10。</p>
</li>
</ol>
<hr>
<p>下一章：<a href="/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道偵測</a></p>
<p><em>文件版本：v0.31.0</em>
<em>最後更新：2026-03-04</em></p>
]]></content:encoded></item><item><title>程式碼壞味道偵測</title><link>https://tarrragon.github.io/blog/python/07-refactoring/code-smells/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/code-smells/</guid><description>&lt;p>上一章：&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/refactoring-strategy/" data-link-title="重構的動機與策略" data-link-desc="從 Hook 系統重構經驗出發，學習何時重構、何時不該重構，以及如何將大規模重構拆分成可管理的階段">重構的動機與策略&lt;/a>&lt;/p>
&lt;p>「程式碼壞味道」(Code Smell) 是 Martin Fowler 在《Refactoring》中提出的概念：程式碼中暗示深層問題的表面跡象。壞味道不是 Bug，程式仍然能正常執行，但它們預告了維護成本的攀升。上一章介紹了認知負擔指數——重複程式碼和難以理解的結構是指數升高的主要原因。本章把這些讓認知負擔上升的具體模式系統化，稱為「壞味道」。&lt;/p>
&lt;p>本章建立一套從「識別」到「行動」的完整流程：先以三級分類理解問題的嚴重程度，再以工具鏈偵測，最後透過 5 Why 分析找到根本原因。&lt;/p>
&lt;h2 id="壞味道三級分類">壞味道三級分類&lt;/h2>
&lt;p>不是所有壞味道都一樣嚴重。依照影響範圍和修復成本，分成三個等級：&lt;/p>
&lt;h3 id="第一級實作級--單一檔案內的問題">第一級：實作級 &amp;ndash; 單一檔案內的問題&lt;/h3>
&lt;p>影響範圍最小，通常改一個檔案就能解決。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pattern ID&lt;/th>
 &lt;th>壞味道&lt;/th>
 &lt;th>典型症狀&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>IMP-001&lt;/td>
 &lt;td>重複程式碼散落各處&lt;/td>
 &lt;td>同一個函式在 4 個檔案各寫一次&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IMP-002&lt;/td>
 &lt;td>魔法數字&lt;/td>
 &lt;td>&lt;code>line[9:]&lt;/code> &amp;ndash; 為什麼是 9？&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h4 id="imp-001-範例四份一模一樣的函式">IMP-001 範例：四份一模一樣的函式&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># hooks/pre_commit.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">run_git_command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">capture_output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&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">return&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&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"># hooks/post_merge.py -- 完全相同的程式碼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">run_git_command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">capture_output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&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="k">return&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&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>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># hooks/branch_check.py -- 又是一模一樣&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># hooks/worktree_guardian.py -- 第四份...&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>問題在於當你需要加入錯誤處理時，要改四個地方，漏掉一個就是 Bug。&lt;/p>
&lt;h4 id="imp-002-範例沒人記得的數字">IMP-002 範例：沒人記得的數字&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">parse_worktree_line&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;worktree &amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">9&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="c1"># 三個月後，你還記得 9 是什麼嗎？&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="第二級架構級--跨模組的結構問題">第二級：架構級 &amp;ndash; 跨模組的結構問題&lt;/h3>
&lt;p>影響多個檔案的互動方式，需要架構層面的重新設計。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pattern ID&lt;/th>
 &lt;th>壞味道&lt;/th>
 &lt;th>典型症狀&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>ARCH-001&lt;/td>
 &lt;td>配置與程式碼混合&lt;/td>
 &lt;td>800 行的檔案，一半是配置資料&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h4 id="arch-001-範例被配置淹沒的邏輯">ARCH-001 範例：被配置淹沒的邏輯&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 一個 800+ 行的 Hook 檔案&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">PROTECTED_BRANCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;develop&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">ALLOWED_PATTERNS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;feat/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fix/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;chore/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n">ERROR_MESSAGES&lt;/span> &lt;span class="o">=&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="s2">&amp;#34;branch_not_allowed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;分支名稱不符合規範&amp;#34;&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="s2">&amp;#34;missing_ticket&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;缺少 Ticket 引用&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ... 數十行配置繼續&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&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="k">def&lt;/span> &lt;span class="nf">check_branch&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="c1"># 真正的邏輯只有幾十行，卻埋在幾百行配置之下&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">pass&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>修改一條錯誤訊息就要打開整個程式碼檔案，負責配置的人被迫閱讀程式邏輯，負責邏輯的人被迫捲過數百行配置——兩者都承受了不必要的負擔。&lt;/p>
&lt;h3 id="第三級遷移級--重構過程中引入的問題">第三級：遷移級 &amp;ndash; 重構過程中引入的問題&lt;/h3>
&lt;p>最危險的一類。它們是在修復其他壞味道時「創造」出來的新問題。遷移級問題在 Error Pattern 系統中仍使用 IMP 前綴，因為它們本質上是實作層面的作用域和 Import 問題——只是發生在重構過程中，因此格外危險。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pattern ID&lt;/th>
 &lt;th>壞味道&lt;/th>
 &lt;th>典型症狀&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>IMP-003&lt;/td>
 &lt;td>重構作用域迴歸&lt;/td>
 &lt;td>變數移入函式後，其他函式找不到&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IMP-005&lt;/td>
 &lt;td>模組遷移 Import 斷裂&lt;/td>
 &lt;td>檔案搬家後，Import 路徑沒跟著改&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h4 id="imp-003-範例搬家沒留新地址">IMP-003 範例：搬家沒留新地址&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修正前：logger 是全域變數，所有函式都看得到&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook-name&amp;#34;&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="k">def&lt;/span> &lt;span class="nf">helper_function&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;doing something&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK，全域可見&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="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">helper_function&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"># 修正後：logger 搬進 main()，但 helper 沒收到通知&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">def&lt;/span> &lt;span class="nf">helper_function&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="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;doing something&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># NameError! logger 不見了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&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">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook-name&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 現在是區域變數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">helper_function&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># helper 找不到 logger&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 Bug 在真實專案中影響了 7 個 Hook、41 個函式。更危險的是，例外捕捉機制將錯誤靜默吞掉，直到開發者主動翻查日誌才發現。在事件發生當時，錯誤被靜默吞掉。此問題後來已修復，現在 Hook 失敗會輸出到 stderr 確保開發者可見。&lt;/p></description><content:encoded><![CDATA[<p>上一章：<a href="/blog/python/07-refactoring/refactoring-strategy/" data-link-title="重構的動機與策略" data-link-desc="從 Hook 系統重構經驗出發，學習何時重構、何時不該重構，以及如何將大規模重構拆分成可管理的階段">重構的動機與策略</a></p>
<p>「程式碼壞味道」(Code Smell) 是 Martin Fowler 在《Refactoring》中提出的概念：程式碼中暗示深層問題的表面跡象。壞味道不是 Bug，程式仍然能正常執行，但它們預告了維護成本的攀升。上一章介紹了認知負擔指數——重複程式碼和難以理解的結構是指數升高的主要原因。本章把這些讓認知負擔上升的具體模式系統化，稱為「壞味道」。</p>
<p>本章建立一套從「識別」到「行動」的完整流程：先以三級分類理解問題的嚴重程度，再以工具鏈偵測，最後透過 5 Why 分析找到根本原因。</p>
<h2 id="壞味道三級分類">壞味道三級分類</h2>
<p>不是所有壞味道都一樣嚴重。依照影響範圍和修復成本，分成三個等級：</p>
<h3 id="第一級實作級--單一檔案內的問題">第一級：實作級 &ndash; 單一檔案內的問題</h3>
<p>影響範圍最小，通常改一個檔案就能解決。</p>
<table>
  <thead>
      <tr>
          <th>Pattern ID</th>
          <th>壞味道</th>
          <th>典型症狀</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IMP-001</td>
          <td>重複程式碼散落各處</td>
          <td>同一個函式在 4 個檔案各寫一次</td>
          <td>中</td>
      </tr>
      <tr>
          <td>IMP-002</td>
          <td>魔法數字</td>
          <td><code>line[9:]</code> &ndash; 為什麼是 9？</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<h4 id="imp-001-範例四份一模一樣的函式">IMP-001 範例：四份一模一樣的函式</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># hooks/pre_commit.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">run_git_command</span><span class="p">(</span><span class="n">cmd</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</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"># hooks/post_merge.py -- 完全相同的程式碼</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">run_git_command</span><span class="p">(</span><span class="n">cmd</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</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="c1"># hooks/branch_check.py -- 又是一模一樣</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># hooks/worktree_guardian.py -- 第四份...</span></span></span></code></pre></div><p>問題在於當你需要加入錯誤處理時，要改四個地方，漏掉一個就是 Bug。</p>
<h4 id="imp-002-範例沒人記得的數字">IMP-002 範例：沒人記得的數字</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;worktree &#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">return</span> <span class="n">line</span><span class="p">[</span><span class="mi">9</span><span class="p">:]</span>  <span class="c1"># 三個月後，你還記得 9 是什麼嗎？</span></span></span></code></pre></div><h3 id="第二級架構級--跨模組的結構問題">第二級：架構級 &ndash; 跨模組的結構問題</h3>
<p>影響多個檔案的互動方式，需要架構層面的重新設計。</p>
<table>
  <thead>
      <tr>
          <th>Pattern ID</th>
          <th>壞味道</th>
          <th>典型症狀</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ARCH-001</td>
          <td>配置與程式碼混合</td>
          <td>800 行的檔案，一半是配置資料</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<h4 id="arch-001-範例被配置淹沒的邏輯">ARCH-001 範例：被配置淹沒的邏輯</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 一個 800+ 行的 Hook 檔案</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">PROTECTED_BRANCHES</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">,</span> <span class="s2">&#34;develop&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">ALLOWED_PATTERNS</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;feat/*&#34;</span><span class="p">,</span> <span class="s2">&#34;fix/*&#34;</span><span class="p">,</span> <span class="s2">&#34;chore/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">ERROR_MESSAGES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;branch_not_allowed&#34;</span><span class="p">:</span> <span class="s2">&#34;分支名稱不符合規範&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s2">&#34;missing_ticket&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少 Ticket 引用&#34;</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="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="k">def</span> <span class="nf">check_branch</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># 真正的邏輯只有幾十行，卻埋在幾百行配置之下</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">pass</span></span></span></code></pre></div><p>修改一條錯誤訊息就要打開整個程式碼檔案，負責配置的人被迫閱讀程式邏輯，負責邏輯的人被迫捲過數百行配置——兩者都承受了不必要的負擔。</p>
<h3 id="第三級遷移級--重構過程中引入的問題">第三級：遷移級 &ndash; 重構過程中引入的問題</h3>
<p>最危險的一類。它們是在修復其他壞味道時「創造」出來的新問題。遷移級問題在 Error Pattern 系統中仍使用 IMP 前綴，因為它們本質上是實作層面的作用域和 Import 問題——只是發生在重構過程中，因此格外危險。</p>
<table>
  <thead>
      <tr>
          <th>Pattern ID</th>
          <th>壞味道</th>
          <th>典型症狀</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IMP-003</td>
          <td>重構作用域迴歸</td>
          <td>變數移入函式後，其他函式找不到</td>
          <td>高</td>
      </tr>
      <tr>
          <td>IMP-005</td>
          <td>模組遷移 Import 斷裂</td>
          <td>檔案搬家後，Import 路徑沒跟著改</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<h4 id="imp-003-範例搬家沒留新地址">IMP-003 範例：搬家沒留新地址</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 修正前：logger 是全域變數，所有函式都看得到</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook-name&#34;</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="k">def</span> <span class="nf">helper_function</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;doing something&#34;</span><span class="p">)</span>  <span class="c1"># OK，全域可見</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">helper_function</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"># 修正後：logger 搬進 main()，但 helper 沒收到通知</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">helper_function</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;doing something&#34;</span><span class="p">)</span>  <span class="c1"># NameError! logger 不見了</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook-name&#34;</span><span class="p">)</span>  <span class="c1"># 現在是區域變數</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">helper_function</span><span class="p">()</span>  <span class="c1"># helper 找不到 logger</span></span></span></code></pre></div><p>這個 Bug 在真實專案中影響了 7 個 Hook、41 個函式。更危險的是，例外捕捉機制將錯誤靜默吞掉，直到開發者主動翻查日誌才發現。在事件發生當時，錯誤被靜默吞掉。此問題後來已修復，現在 Hook 失敗會輸出到 stderr 確保開發者可見。</p>
<h3 id="三級分類速查表">三級分類速查表</h3>
<table>
  <thead>
      <tr>
          <th>級別</th>
          <th>影響範圍</th>
          <th>修復成本</th>
          <th>偵測難度</th>
          <th>典型 Pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作級</td>
          <td>單一檔案</td>
          <td>低</td>
          <td>容易</td>
          <td>IMP-001, IMP-002</td>
      </tr>
      <tr>
          <td>架構級</td>
          <td>跨模組</td>
          <td>中-高</td>
          <td>中等</td>
          <td>ARCH-001</td>
      </tr>
      <tr>
          <td>遷移級</td>
          <td>重構過程</td>
          <td>高</td>
          <td>困難（可能靜默）</td>
          <td>IMP-003, IMP-005</td>
      </tr>
  </tbody>
</table>
<h2 id="偵測工具鏈">偵測工具鏈</h2>
<p>識別壞味道不能只靠肉眼。以下工具從簡單到進階，組成完整的偵測鏈。</p>
<h3 id="第一層grep-模式掃描">第一層：grep 模式掃描</h3>
<p>最快的初步篩檢，幾秒鐘就能掃完整個專案。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 偵測 IMP-001：找出重複的函式定義</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">grep -rh <span class="s2">&#34;^def &#34;</span> hooks/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -10
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#  4 def run_git_command(cmd):    &lt;-- 出現 4 次，高度疑似重複</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#  2 def parse_output(line):      &lt;-- 出現 2 次，需要確認</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"># 偵測 IMP-002：找出魔法數字</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">grep -rn -E <span class="s2">&#34;\[[0-9]+:\]&#34;</span> hooks/*.py      <span class="c1"># 數字切片 [9:]</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">grep -rn <span class="s2">&#34;sleep([0-9]&#34;</span> hooks/*.py        <span class="c1"># 硬編碼的等待時間</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">grep -rn <span class="s2">&#34;range([0-9]&#34;</span> hooks/*.py        <span class="c1"># 硬編碼的迴圈次數</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"># 偵測 ARCH-001：找出超長檔案</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">find hooks/ -name <span class="s2">&#34;*.py&#34;</span> -exec wc -l <span class="o">{}</span> <span class="se">\;</span> <span class="p">|</span> awk <span class="s1">&#39;$1 &gt; 500&#39;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 847 hooks/user_prompt_submit.py    &lt;-- 紅色警報</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"># 偵測 IMP-005：模組遷移後殘留的舊 Import</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">grep -rn <span class="s2">&#34;from common_functions import&#34;</span> hooks/*.py
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 如果 common_functions.py 已經搬到 lib/，這些都是未更新的引用</span></span></span></code></pre></div><p><strong>grep 的限制</strong>：只做文字比對，無法理解程式碼結構。<code>line[9:]</code> 會被抓到，但 <code>offset = 9; line[offset:]</code> 就抓不到了。</p>
<h3 id="第二層ast-分析">第二層：AST 分析</h3>
<p>Python 的 <code>ast</code> 模組能解析程式碼結構，做到 grep 做不到的事。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">ast</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</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="k">def</span> <span class="nf">find_scope_references</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">variable_name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;&#34;&#34;找出所有在非 main 函式中引用特定變數的位置。
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">    限制：此函式只做名稱比對，無法追蹤賦值或閉包捕獲。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">tree</span> <span class="o">=</span> <span class="n">ast</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">read</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="n">issues</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">for</span> <span class="n">node</span> <span class="ow">in</span> <span class="n">ast</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">tree</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="nb">isinstance</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">ast</span><span class="o">.</span><span class="n">FunctionDef</span><span class="p">)</span> <span class="ow">and</span> <span class="n">node</span><span class="o">.</span><span class="n">name</span> <span class="o">!=</span> <span class="s2">&#34;main&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="n">param_names</span> <span class="o">=</span> <span class="p">{</span><span class="n">arg</span><span class="o">.</span><span class="n">arg</span> <span class="k">for</span> <span class="n">arg</span> <span class="ow">in</span> <span class="n">node</span><span class="o">.</span><span class="n">args</span><span class="o">.</span><span class="n">args</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="n">variable_name</span> <span class="ow">in</span> <span class="n">param_names</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">                <span class="k">continue</span>  <span class="c1"># 函式已接收此變數為參數，非問題</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">for</span> <span class="n">child</span> <span class="ow">in</span> <span class="n">ast</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">node</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">                <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">child</span><span class="p">,</span> <span class="n">ast</span><span class="o">.</span><span class="n">Name</span><span class="p">)</span> <span class="ow">and</span> <span class="n">child</span><span class="o">.</span><span class="n">id</span> <span class="o">==</span> <span class="n">variable_name</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">                    <span class="n">issues</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;  </span><span class="si">{</span><span class="n">node</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">() 在第 </span><span class="si">{</span><span class="n">child</span><span class="o">.</span><span class="n">lineno</span><span class="si">}</span><span class="s2"> 行引用 </span><span class="si">{</span><span class="n">variable_name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">                    <span class="k">break</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="n">issues</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="c1"># 使用方式</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="n">issues</span> <span class="o">=</span> <span class="n">find_scope_references</span><span class="p">(</span><span class="s2">&#34;hooks/pre_commit.py&#34;</span><span class="p">,</span> <span class="s2">&#34;logger&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">for</span> <span class="n">issue</span> <span class="ow">in</span> <span class="n">issues</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">issue</span><span class="p">)</span></span></span></code></pre></div><p><strong>AST 能做而 grep 做不到的事</strong>：</p>
<table>
  <thead>
      <tr>
          <th>能力</th>
          <th>grep</th>
          <th>AST</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>找出字面上的文字模式</td>
          <td>可以</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>區分變數定義和使用</td>
          <td>不行</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>分析函式的參數列表</td>
          <td>不行</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>偵測作用域問題</td>
          <td>不行</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>計算巢狀深度</td>
          <td>不行</td>
          <td>可以</td>
      </tr>
  </tbody>
</table>
<p>自己撰寫 AST 腳本適合針對特定問題的精確偵測。但對於更廣泛的靜態分析需求，現成工具能用更低的成本涵蓋更多場景。</p>
<h3 id="第三層靜態分析工具比較">第三層：靜態分析工具比較</h3>
<p>不同工具的偵測能力差異很大，選錯工具會漏掉關鍵問題。</p>
<table>
  <thead>
      <tr>
          <th>偵測能力</th>
          <th>py_compile</th>
          <th>pylint</th>
          <th>mypy</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>語法錯誤</td>
          <td>可以</td>
          <td>可以</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>未使用的變數</td>
          <td>不行</td>
          <td>可以</td>
          <td>不行</td>
      </tr>
      <tr>
          <td>作用域問題 (IMP-003)</td>
          <td><strong>不行</strong></td>
          <td>可以</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>Import 路徑錯誤 (IMP-005)</td>
          <td><strong>不行</strong></td>
          <td>可以</td>
          <td>部分*</td>
      </tr>
      <tr>
          <td>型別錯誤</td>
          <td>不行</td>
          <td>部分</td>
          <td>可以</td>
      </tr>
      <tr>
          <td>程式碼風格</td>
          <td>不行</td>
          <td>可以</td>
          <td>不行</td>
      </tr>
      <tr>
          <td>執行速度</td>
          <td>最快</td>
          <td>中等</td>
          <td>較慢</td>
      </tr>
  </tbody>
</table>
<p>*mypy 偵測 Import 路徑錯誤需正確設定 MYPYPATH 或 mypy.ini，對動態 <code>sys.path</code> 無效。</p>
<p><code>py_compile</code> 只檢查語法是否合法。<code>logger</code> 變數不存在是執行期錯誤，不是語法錯誤。這就是為什麼 IMP-003 能通過 <code>py_compile</code> 的檢查，卻在執行時爆炸。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># py_compile：語法 OK 不代表能跑</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">python3 -m py_compile hooks/pre_commit.py  <span class="c1"># 通過！但 logger 根本找不到</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"># pylint：能抓到更多問題</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">pylint hooks/pre_commit.py
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># E0602: Undefined variable &#39;logger&#39; (undefined-variable)</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">python3 hooks/pre_commit.py &lt; /dev/null
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># NameError: name &#39;logger&#39; is not defined</span></span></span></code></pre></div><p><strong>建議的偵測策略</strong>：先用 grep 做快速掃描，對疑似問題用 AST 確認，重構後用 pylint 或實際執行做最終驗證。</p>
<table>
  <thead>
      <tr>
          <th>使用場景</th>
          <th>推薦工具</th>
          <th>適用理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>快速掃描重複模式</td>
          <td>grep</td>
          <td>速度最快，適合初篩</td>
      </tr>
      <tr>
          <td>確認特定函式結構問題</td>
          <td>AST 分析</td>
          <td>精確到語法層級，無正規表達式偽陽性</td>
      </tr>
      <tr>
          <td>重構後整體品質驗證</td>
          <td>pylint / mypy</td>
          <td>涵蓋面廣，可持續整合</td>
      </tr>
      <tr>
          <td>作用域和型別問題</td>
          <td>實際執行</td>
          <td>py_compile 不夠，需 pytest 或直接執行</td>
      </tr>
  </tbody>
</table>
<h2 id="5-why-根因分析">5 Why 根因分析</h2>
<p>找到壞味道只是起點；若要防止問題再次出現，必須找到根本原因。</p>
<h3 id="完整範例arch-001-配置與程式碼混合">完整範例：ARCH-001 配置與程式碼混合</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">問題：單一 Hook 檔案超過 800 行，其中約一半是硬編碼的配置資料
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Why 1: 為什麼檔案會有 800 行？
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">--&gt; 因為配置資料（分支規則、錯誤訊息、檔案模式）和程式邏輯
</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></span><span class="line"><span class="ln"> 7</span><span class="cl">Why 2: 為什麼配置和邏輯放在一起？
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">--&gt; 因為開發時為求快速，直接在程式碼中定義配置常數
</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">Why 3: 為什麼選擇快速做法而非分離？
</span></span><span class="line"><span class="ln">11</span><span class="cl">--&gt; 因為缺乏配置管理策略，沒有標準化的做法可以遵循
</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">Why 4: 為什麼沒有配置管理策略？
</span></span><span class="line"><span class="ln">14</span><span class="cl">--&gt; 因為 Hook 系統初期設計時，只考慮了功能實現，
</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></span><span class="line"><span class="ln">17</span><span class="cl">Why 5: 為什麼初期設計沒考慮配置增長？
</span></span><span class="line"><span class="ln">18</span><span class="cl">--&gt; 【根本原因】缺乏明確的架構原則指導配置與程式碼分離</span></span></code></pre></div><p><strong>根因指向的行動</strong>：制定架構原則，明確規定什麼放在 YAML、什麼留在程式碼中。</p>
<table>
  <thead>
      <tr>
          <th>資料類型</th>
          <th>正確位置</th>
          <th>判斷依據</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>業務規則配置</td>
          <td>YAML 檔案</td>
          <td>會隨環境改變嗎？非工程師可能修改嗎？</td>
      </tr>
      <tr>
          <td>錯誤訊息</td>
          <td>YAML 或 i18n</td>
          <td>需要多語言嗎？</td>
      </tr>
      <tr>
          <td>常數定義</td>
          <td>Python 常數檔</td>
          <td>與程式邏輯緊密耦合嗎？</td>
      </tr>
      <tr>
          <td>程式邏輯</td>
          <td>Python 檔案</td>
          <td>是演算法或流程控制嗎？</td>
      </tr>
  </tbody>
</table>
<h3 id="5-why-的技巧">5 Why 的技巧</h3>
<ol>
<li><strong>持續追問</strong>：第一個「為什麼」幾乎永遠不是根本原因</li>
<li><strong>客觀描述</strong>：寫「缺乏審查機制」而不是「某人偷懶」</li>
<li><strong>可驗證</strong>：每一層的回答都應該可以被事實確認</li>
<li><strong>可行動</strong>：最終原因必須能轉化成具體的改善措施</li>
<li><strong>停止條件</strong>：當答案指向「流程或規範的缺失」時，通常就是根因</li>
</ol>
<h2 id="error-patterns-經驗傳承系統">Error Patterns 經驗傳承系統</h2>
<p>個人發現壞味道是一次性的收穫；將其記錄為 Error Pattern，才能讓整個團隊持續受益。</p>
<h3 id="目錄結構">目錄結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">.claude/error-patterns/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── README.md              # 系統說明與索引
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── test/                  # 測試相關：TEST-001, TEST-002, ...
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── documentation/         # 文件相關：DOC-001, DOC-002, ...
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── architecture/          # 架構相關：ARCH-001, ARCH-002, ...
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── implementation/        # 實作相關：IMP-001, IMP-002, ...</span></span></code></pre></div><h3 id="pattern-文件模板">Pattern 文件模板</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gh"># [Pattern ID]: [簡短標題]
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="gu">## 基本資訊
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">-</span> **Pattern ID**: {CATEGORY}-{NNN}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">-</span> **風險等級**: 高/中/低
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">-</span> **發現日期**: YYYY-MM-DD
</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="gu">## 問題描述
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="gu">### 症狀
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">[用程式碼範例展示問題的外在表現]
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="gu">### 根本原因 (5 Why 分析)
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">1.</span> Why 1: ...
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="k">2.</span> Why 2: ...
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="k">3.</span> Why 3: ...
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="k">4.</span> Why 4: ...
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">5.</span> Why 5: (根本原因)
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="gu">## 解決方案
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="gu">### 正確做法
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">[程式碼範例]
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="gu">### 錯誤做法 (避免)
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">[程式碼範例]
</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="gu">## 檢測方法
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">[grep 指令、AST 腳本或工具配置]</span></span></code></pre></div><h3 id="建立流程">建立流程</h3>
<ol>
<li><strong>識別模式</strong>：確認問題確實重複出現（至少 2 次）</li>
<li><strong>分類歸檔</strong>：選擇 TEST / DOC / ARCH / IMP</li>
<li><strong>5 Why 分析</strong>：找出根本原因</li>
<li><strong>記錄方案</strong>：寫下正確和錯誤做法的對比</li>
<li><strong>加入偵測</strong>：提供 grep 或 AST 的偵測指令</li>
</ol>
<h2 id="從識別到行動的決策流程">從識別到行動的決策流程</h2>
<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">發現壞味道
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">影響正確性嗎？（會導致 Bug）
</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">    +-- 是 --&gt; 立即修復，建立 Ticket
</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">    +-- 否 --&gt; 影響多個檔案嗎？
</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">                +-- 是 --&gt; 記錄 Error Pattern + 建立 Ticket
</span></span><span class="line"><span class="ln">11</span><span class="cl">                |
</span></span><span class="line"><span class="ln">12</span><span class="cl">                +-- 否 --&gt; 認知負擔高嗎？（函式超長、巢狀太深）
</span></span><span class="line"><span class="ln">13</span><span class="cl">                            |
</span></span><span class="line"><span class="ln">14</span><span class="cl">                            +-- 是 --&gt; 排入下次重構
</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">                            +-- 否 --&gt; 記錄，暫不處理</span></span></code></pre></div><p><strong>關鍵原則</strong>：遷移級壞味道（IMP-003、IMP-005）幾乎都會影響正確性，必須立即處理。實作級壞味道（IMP-001、IMP-002）通常不影響正確性，可以排入重構計畫。</p>
<h2 id="實作練習">實作練習</h2>
<h3 id="練習-1分類壞味道">練習 1：分類壞味道</h3>
<p>以下程式碼有哪些壞味道？各屬於哪一級？</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">BRANCH_RULES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;protected&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s2">&#34;max_length&#34;</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;patterns&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;feat/*&#34;</span><span class="p">,</span> <span class="s2">&#34;fix/*&#34;</span><span class="p">,</span> <span class="s2">&#34;chore/*&#34;</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="k">def</span> <span class="nf">check</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">res</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s2">&#34;type&#34;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&#34;A&#34;</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="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s2">&#34;status&#34;</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</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="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s2">&#34;value&#34;</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">                    <span class="n">res</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="s2">&#34;name&#34;</span><span class="p">][</span><span class="mi">5</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="n">res</span></span></span></code></pre></div><details>
<summary>參考答案</summary>
<p><strong>實作級壞味道</strong>：</p>
<ol>
<li><strong>重複程式碼散落各處</strong> (IMP-001) &ndash; <code>data[i]</code> 在迴圈中重複出現 5 次，應提取為區域變數</li>
<li><strong>魔法數字</strong> (IMP-002) &ndash; <code>1</code>、<code>100</code>、<code>[5:]</code> 含義不明</li>
<li><strong>巢狀過深</strong> &ndash; 三層 if 應該用 Guard Clause 攤平</li>
<li><strong>使用 range(len())</strong> &ndash; 應該直接迭代集合</li>
</ol>
<p><strong>架構級壞味道</strong>：5. <strong>配置與程式碼混合</strong> (ARCH-001) &ndash; <code>BRANCH_RULES</code> 字典直接寫在程式碼中</p>
</details>
<h3 id="練習-2設計偵測指令">練習 2：設計偵測指令</h3>
<p>針對以下壞味道，各寫一條 grep 指令來偵測：</p>
<ol>
<li>在 <code>src/</code> 目錄下找出所有超過 3 層巢狀的 if 語句</li>
<li>找出可能的重複函式定義</li>
<li>找出所有引用已遷移模組 <code>old_utils</code> 的檔案</li>
</ol>
<details>
<summary>參考答案</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 找出深層巢狀（透過縮排層級近似偵測，偵測第 4 層起始）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">grep -rn <span class="s2">&#34;^                if &#34;</span> src/*.py  <span class="c1"># 16 個空格 = 第四層（超過 3 層）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 這個方法假設每層縮排使用 4 個空格。如果專案使用 2 格縮排，對應數字應改為 8。</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 更可靠的做法是使用 AST 分析計算實際巢狀深度。</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"># 2. 找出重複的函式定義</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">grep -rh <span class="s2">&#34;^def &#34;</span> src/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -10
</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="c1"># 3. 找出未更新的舊 import</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">grep -rn <span class="s2">&#34;from old_utils import&#34;</span> src/*.py
</span></span><span class="line"><span class="ln">11</span><span class="cl">grep -rn <span class="s2">&#34;import old_utils&#34;</span> src/*.py</span></span></code></pre></div></details>
<h3 id="練習-35-why-分析">練習 3：5 Why 分析</h3>
<p>對以下問題進行 5 Why 分析：「重構時把 <code>logger</code> 從全域移到 <code>main()</code> 內部，導致 7 個 Hook 靜默失敗」。</p>
<details>
<summary>參考答案</summary>





<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">Why 1: 為什麼 7 個 Hook 會失敗？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">--&gt; 因為 helper 函式引用了 logger，但 logger 已不在全域作用域
</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">Why 2: 為什麼 logger 不在全域了？
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">--&gt; 因為重構要求統一 logger 初始化風格為「main() 內部」
</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">Why 3: 為什麼只移動了 logger，沒有更新引用？
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">--&gt; 因為執行重構時沒有先列出所有引用 logger 的函式
</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">Why 4: 為什麼沒有做引用分析？
</span></span><span class="line"><span class="ln">11</span><span class="cl">--&gt; 因為缺乏「作用域變更檢查清單」的標準步驟
</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">Why 5: 為什麼沒有這個檢查清單？
</span></span><span class="line"><span class="ln">14</span><span class="cl">--&gt; 【根本原因】重構流程缺乏「影響範圍分析」的強制步驟</span></span></code></pre></div><p><strong>行動</strong>：在每次變更變數作用域前，強制執行 grep 或 AST 分析列出所有引用。</p>
</details>
<h2 id="小結">小結</h2>
<ul>
<li>壞味道分三級：實作級影響單一檔案，架構級需跨模組重新設計，遷移級最危險——它是重構過程中創造出來的新問題</li>
<li>偵測工具鏈由淺入深：grep 快速掃描、AST 結構分析、pylint/mypy 靜態檢查</li>
<li><code>py_compile</code> 只檢查語法，無法偵測作用域問題和 Import 錯誤</li>
<li>5 Why 分析追問到「流程或規範的缺失」才是根因</li>
<li>Error Patterns 把個人經驗變成團隊資產</li>
</ul>
<p>下一章：<a href="/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a></p>
]]></content:encoded></item><item><title>DRY 原則與共用程式庫</title><link>https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/</guid><description>&lt;p>上一章：&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道偵測&lt;/a>&lt;/p>
&lt;p>DRY (Don&amp;rsquo;t Repeat Yourself) 是軟體開發的核心原則之一。本章基於 Error Pattern IMP-001，學習如何識別重複程式碼並建立共用模組。後半部分以 v0.31.0 的模組演進和遷移實戰為例，示範共用庫如何隨系統成長持續演進。&lt;/p>
&lt;h2 id="問題背景">問題背景&lt;/h2>
&lt;h3 id="症狀">症狀&lt;/h3>
&lt;p>相同功能在多個檔案中重複實作：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># hooks/pre_commit.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">run_git_command&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cmd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">capture_output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&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">return&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&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"># hooks/post_merge.py -- 完全相同&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"># hooks/branch_check.py -- 完全相同&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"># hooks/worktree_guardian.py -- 完全相同&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>四個檔案中存在完全相同的函式定義。&lt;/p>
&lt;h3 id="5-why-分析">5 Why 分析&lt;/h3>
&lt;ol>
&lt;li>Why 1: 相同的 run_git_command 函式在 4 個檔案中重複&lt;/li>
&lt;li>Why 2: 每個 Hook 獨立開發，沒有共用模組&lt;/li>
&lt;li>Why 3: 缺乏 Hook 系統的架構設計和共用程式庫規劃&lt;/li>
&lt;li>Why 4: 快速開發時複製貼上最快&lt;/li>
&lt;li>Why 5: &lt;strong>缺乏 DRY 原則的強制檢查機制&lt;/strong>&lt;/li>
&lt;/ol>
&lt;h2 id="dry-原則核心">DRY 原則核心&lt;/h2>
&lt;p>重複程式碼的四大壞處：&lt;strong>修改需改多處&lt;/strong>、&lt;strong>容易不一致&lt;/strong>、&lt;strong>增加維護成本&lt;/strong>、&lt;strong>測試困難&lt;/strong>。&lt;/p>
&lt;p>DRY 的完整含義不只是「不要複製貼上」：&lt;/p>
&lt;blockquote>
&lt;p>Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.&lt;/p>
&lt;p>&amp;ndash; Andy Hunt &amp;amp; Dave Thomas, &lt;em>The Pragmatic Programmer&lt;/em>&lt;/p>&lt;/blockquote>
&lt;p>這意味著不只是程式碼，還包括業務邏輯、資料定義、設定內容。&lt;/p>
&lt;h2 id="識別重複程式碼">識別重複程式碼&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 找出重複的函式定義&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">grep -rh &lt;span class="s2">&amp;#34;^def &amp;#34;&lt;/span> .claude/hooks/*.py &lt;span class="p">|&lt;/span> sort &lt;span class="p">|&lt;/span> uniq -c &lt;span class="p">|&lt;/span> sort -rn &lt;span class="p">|&lt;/span> head -20
&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"># 4 def run_git_command(cmd):&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"># 3 def get_current_branch():&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"># 2 def parse_worktree_line(line):&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>重複類型&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;th>處理方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>完全相同&lt;/td>
 &lt;td>複製貼上的程式碼&lt;/td>
 &lt;td>抽取到共用模組&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構相同&lt;/td>
 &lt;td>相似但參數不同&lt;/td>
 &lt;td>抽取並參數化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>概念相同&lt;/td>
 &lt;td>做同樣的事但實作不同&lt;/td>
 &lt;td>統一介面&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="建立共用程式庫">建立共用程式庫&lt;/h2>
&lt;h3 id="模組結構">模組結構&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">.claude/lib/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── __init__.py # 公開介面
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">├── git_utils.py # Git 操作
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">├── config_loader.py # 配置載入
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">├── hook_io.py # 輸入輸出
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">└── hook_logging.py # 日誌系統&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="抽取共用函式">抽取共用函式&lt;/h3>
&lt;p>從重複程式碼中抽取，加上完整的型別標註和 docstring：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># lib/git_utils.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Git 操作工具模組。&amp;#34;&amp;#34;&amp;#34;&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="kn">import&lt;/span> &lt;span class="nn">subprocess&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">typing&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Optional&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="k">def&lt;/span> &lt;span class="nf">run_git_command&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">cmd&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">List&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&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">cwd&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Optional&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">check&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">bool&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&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 class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&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="s2">&amp;#34;&amp;#34;&amp;#34;執行 Git 命令並回傳輸出。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s2"> Args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s2"> cmd: Git 命令列表，例如 [&amp;#34;git&amp;#34;, &amp;#34;status&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s2"> cwd: 工作目錄，預設為當前目錄
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="s2"> check: 是否在命令失敗時拋出異常
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">subprocess&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">run&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="n">cmd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">capture_output&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">cwd&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cwd&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">check&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">check&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stdout&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">get_current_branch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cwd&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Optional&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;取得當前分支名稱。&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">run_git_command&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="s2">&amp;#34;git&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;branch&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--show-current&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">cwd&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cwd&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="更新使用處">更新使用處&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># hooks/pre_commit.py（重構後）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">lib.git_utils&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">run_git_command&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">get_current_branch&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="k">def&lt;/span> &lt;span class="nf">check_branch&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">current_branch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_current_branch&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="c1"># 使用共用函式，不再重複定義&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="抽取技巧">抽取技巧&lt;/h2>
&lt;h3 id="處理微小差異">處理微小差異&lt;/h3>
&lt;p>當重複程式碼有微小差異時，使用參數化：&lt;/p></description><content:encoded><![CDATA[<p>上一章：<a href="/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道偵測</a></p>
<p>DRY (Don&rsquo;t Repeat Yourself) 是軟體開發的核心原則之一。本章基於 Error Pattern IMP-001，學習如何識別重複程式碼並建立共用模組。後半部分以 v0.31.0 的模組演進和遷移實戰為例，示範共用庫如何隨系統成長持續演進。</p>
<h2 id="問題背景">問題背景</h2>
<h3 id="症狀">症狀</h3>
<p>相同功能在多個檔案中重複實作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># hooks/pre_commit.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">run_git_command</span><span class="p">(</span><span class="n">cmd</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</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"># hooks/post_merge.py  -- 完全相同</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># hooks/branch_check.py  -- 完全相同</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># hooks/worktree_guardian.py  -- 完全相同</span></span></span></code></pre></div><p>四個檔案中存在完全相同的函式定義。</p>
<h3 id="5-why-分析">5 Why 分析</h3>
<ol>
<li>Why 1: 相同的 run_git_command 函式在 4 個檔案中重複</li>
<li>Why 2: 每個 Hook 獨立開發，沒有共用模組</li>
<li>Why 3: 缺乏 Hook 系統的架構設計和共用程式庫規劃</li>
<li>Why 4: 快速開發時複製貼上最快</li>
<li>Why 5: <strong>缺乏 DRY 原則的強制檢查機制</strong></li>
</ol>
<h2 id="dry-原則核心">DRY 原則核心</h2>
<p>重複程式碼的四大壞處：<strong>修改需改多處</strong>、<strong>容易不一致</strong>、<strong>增加維護成本</strong>、<strong>測試困難</strong>。</p>
<p>DRY 的完整含義不只是「不要複製貼上」：</p>
<blockquote>
<p>Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.</p>
<p>&ndash; Andy Hunt &amp; Dave Thomas, <em>The Pragmatic Programmer</em></p></blockquote>
<p>這意味著不只是程式碼，還包括業務邏輯、資料定義、設定內容。</p>
<h2 id="識別重複程式碼">識別重複程式碼</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 找出重複的函式定義</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rh <span class="s2">&#34;^def &#34;</span> .claude/hooks/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -20
</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">#    4 def run_git_command(cmd):</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#    3 def get_current_branch():</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">#    2 def parse_worktree_line(line):</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>重複類型</th>
          <th>範例</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完全相同</td>
          <td>複製貼上的程式碼</td>
          <td>抽取到共用模組</td>
      </tr>
      <tr>
          <td>結構相同</td>
          <td>相似但參數不同</td>
          <td>抽取並參數化</td>
      </tr>
      <tr>
          <td>概念相同</td>
          <td>做同樣的事但實作不同</td>
          <td>統一介面</td>
      </tr>
  </tbody>
</table>
<h2 id="建立共用程式庫">建立共用程式庫</h2>
<h3 id="模組結構">模組結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">.claude/lib/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── __init__.py           # 公開介面
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── git_utils.py          # Git 操作
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── config_loader.py      # 配置載入
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── hook_io.py            # 輸入輸出
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── hook_logging.py       # 日誌系統</span></span></code></pre></div><h3 id="抽取共用函式">抽取共用函式</h3>
<p>從重複程式碼中抽取，加上完整的型別標註和 docstring：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># lib/git_utils.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s2">&#34;&#34;&#34;Git 操作工具模組。&#34;&#34;&#34;</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="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</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="k">def</span> <span class="nf">run_git_command</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">cmd</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">cwd</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Path</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">check</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="s2">&#34;&#34;&#34;執行 Git 命令並回傳輸出。
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s2">    Args:
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">        cmd: Git 命令列表，例如 [&#34;git&#34;, &#34;status&#34;]
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s2">        cwd: 工作目錄，預設為當前目錄
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s2">        check: 是否在命令失敗時拋出異常
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">cmd</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">cwd</span><span class="o">=</span><span class="n">cwd</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="n">check</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><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="k">def</span> <span class="nf">get_current_branch</span><span class="p">(</span><span class="n">cwd</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Path</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="s2">&#34;&#34;&#34;取得當前分支名稱。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="k">return</span> <span class="n">run_git_command</span><span class="p">([</span><span class="s2">&#34;git&#34;</span><span class="p">,</span> <span class="s2">&#34;branch&#34;</span><span class="p">,</span> <span class="s2">&#34;--show-current&#34;</span><span class="p">],</span> <span class="n">cwd</span><span class="o">=</span><span class="n">cwd</span><span class="p">)</span></span></span></code></pre></div><h3 id="更新使用處">更新使用處</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># hooks/pre_commit.py（重構後）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.git_utils</span> <span class="kn">import</span> <span class="n">run_git_command</span><span class="p">,</span> <span class="n">get_current_branch</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="k">def</span> <span class="nf">check_branch</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">current_branch</span> <span class="o">=</span> <span class="n">get_current_branch</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="c1"># 使用共用函式，不再重複定義</span></span></span></code></pre></div><h2 id="抽取技巧">抽取技巧</h2>
<h3 id="處理微小差異">處理微小差異</h3>
<p>當重複程式碼有微小差異時，使用參數化：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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"># hooks/file_a.py</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">return</span> <span class="n">line</span><span class="p">[</span><span class="mi">9</span><span class="p">:]</span>                        <span class="c1"># 不 strip</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"># hooks/file_b.py</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</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">line</span><span class="p">[</span><span class="mi">9</span><span class="p">:]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>                <span class="c1"># 有 strip</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"># hooks/file_c.py</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="n">line</span><span class="o">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="s2">&#34;worktree &#34;</span><span class="p">)</span>  <span class="c1"># 用 Python 3.9+ API</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 重構後：統一實作，支援選項</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">WORKTREE_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;worktree &#34;</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="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">strip</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="s2">&#34;&#34;&#34;解析 worktree 輸出行。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="n">WORKTREE_PREFIX</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">if</span> <span class="n">strip</span> <span class="k">else</span> <span class="n">result</span></span></span></code></pre></div><h3 id="使用高階函式">使用高階函式</h3>
<p>當邏輯結構相同但操作不同時：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Callable</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="k">def</span> <span class="nf">check_all_python_files</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;**/*.py&#34;</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="n">validate_python</span><span class="p">(</span><span class="n">file</span><span class="p">):</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;OK: </span><span class="si">{</span><span class="n">file</span><span class="si">}</span><span class="s2">&#34;</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="k">def</span> <span class="nf">check_all_yaml_files</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">for</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;**/*.yaml&#34;</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="n">validate_yaml</span><span class="p">(</span><span class="n">file</span><span class="p">):</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;OK: </span><span class="si">{</span><span class="n">file</span><span class="si">}</span><span class="s2">&#34;</span><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"># 重構後</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">check_files</span><span class="p">(</span><span class="n">pattern</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">validator</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="n">Path</span><span class="p">],</span> <span class="nb">bool</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">for</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">pattern</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">if</span> <span class="n">validator</span><span class="p">(</span><span class="n">file</span><span class="p">):</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;OK: </span><span class="si">{</span><span class="n">file</span><span class="si">}</span><span class="s2">&#34;</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="n">check_files</span><span class="p">(</span><span class="s2">&#34;**/*.py&#34;</span><span class="p">,</span> <span class="n">validate_python</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">check_files</span><span class="p">(</span><span class="s2">&#34;**/*.yaml&#34;</span><span class="p">,</span> <span class="n">validate_yaml</span><span class="p">)</span></span></span></code></pre></div><h2 id="共用模組設計原則">共用模組設計原則</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>做法</th>
          <th>反面教材</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>單一職責</strong></td>
          <td><code>git_utils.py</code>（Git 操作）、<code>config_loader.py</code>（配置載入）。模組名稱即可看出職責</td>
          <td><code>utils.py</code>（什麼都放，職責不明確）</td>
      </tr>
      <tr>
          <td><strong>穩定的介面</strong></td>
          <td>透過 <code>__init__.py</code> 定義公開 API，內部可自由重構</td>
          <td>讓使用者直接 import 內部實作細節</td>
      </tr>
      <tr>
          <td><strong>完整的 docstring</strong></td>
          <td>每個公開函式都要有 docstring（Args/Returns/Raises）</td>
          <td>只有程式碼，沒有使用說明</td>
      </tr>
      <tr>
          <td><strong>充分的測試</strong></td>
          <td>每個共用函式都要有對應的單元測試</td>
          <td>重構後不跑測試就上線</td>
      </tr>
  </tbody>
</table>
<h2 id="模組演進從-4-個到-7-個">模組演進：從 4 個到 7+ 個</h2>
<p>共用程式庫隨著系統成長持續演進。</p>
<h3 id="模組演進表">模組演進表</h3>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>模組</th>
          <th>職責</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>v0.28.0</td>
          <td><code>git_utils.py</code></td>
          <td>Git 命令執行、分支管理</td>
          <td>消除 4 處 run_git_command 重複</td>
      </tr>
      <tr>
          <td>v0.28.0</td>
          <td><code>hook_io.py</code></td>
          <td>Hook JSON 輸入讀取、輸出生成</td>
          <td>統一 stdin/stdout 處理</td>
      </tr>
      <tr>
          <td>v0.28.0</td>
          <td><code>config_loader.py</code></td>
          <td>YAML 配置檔案載入</td>
          <td>支援 PyYAML fallback JSON</td>
      </tr>
      <tr>
          <td>v0.28.0</td>
          <td><code>hook_logging.py</code></td>
          <td>日誌設定</td>
          <td>統一日誌格式</td>
      </tr>
      <tr>
          <td>v0.31.0</td>
          <td><code>hook_utils.py</code></td>
          <td>統一日誌 + 頂層例外處理</td>
          <td>取代分散的兩套日誌系統</td>
      </tr>
      <tr>
          <td>v0.31.0</td>
          <td><code>hook_messages.py</code></td>
          <td>訊息常數集中管理</td>
          <td>消除 19 個 Hook 的硬編碼訊息</td>
      </tr>
      <tr>
          <td>v0.31.0</td>
          <td><code>hook_validator.py</code></td>
          <td>Hook 健康檢查</td>
          <td>驗證 import 和執行狀態</td>
      </tr>
  </tbody>
</table>
<h3 id="演進的驅動力">演進的驅動力</h3>
<p>每次新增模組都有明確的驅動力，而非預先設計：</p>
<p><strong>v0.28.0（初建期）</strong>：四個函式重複 → 建立四個共用模組。</p>
<p><strong>v0.31.0（成熟期）</strong>：Hook 數量從 7 個成長到 40+ 個，新的重複模式浮現：</p>
<ol>
<li><strong>日誌系統分裂</strong>：<code>hook_logging.py</code> 和 <code>common_functions.setup_hook_logging</code> 兩套實作並存，40+ 個 Hook 各自選用。最終建立 <code>hook_utils.py</code> 統一取代</li>
<li><strong>訊息散落各處</strong>：19 個 Hook 各自硬編碼使用者訊息 → 建立 <code>hook_messages.py</code> 集中管理</li>
</ol>
<p>這驗證了「至少重複兩次再抽取」的 Rule of Three 原則：模組是在真實需求驅動下自然長出來的。</p>
<h2 id="漸進遷移策略">漸進遷移策略</h2>
<p>共用庫建立後，需要將現有使用者逐步遷移。「一次全改」風險太高，以下是 W22 遷移 40+ 個 Hook 到新日誌系統的實戰策略。</p>
<h3 id="分批遷移計畫">分批遷移計畫</h3>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>範圍</th>
          <th>檔案數</th>
          <th>策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>W22-001.2</td>
          <td>主力遷移</td>
          <td>14 個</td>
          <td>按 Hook 事件類型分組遷移</td>
      </tr>
      <tr>
          <td>W22-001.3</td>
          <td>補漏</td>
          <td>3 個</td>
          <td>掃描殘留的舊 import</td>
      </tr>
  </tbody>
</table>
<h3 id="每個-hook-的遷移步驟">每個 Hook 的遷移步驟</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># === 步驟 1：替換 import ===</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="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">setup_hook_logging</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="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">setup_hook_logging</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"># === 步驟 2：包裹主函式 ===</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="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">main</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;執行失敗: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</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="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">run_hook_safely</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;my-hook&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># === 步驟 3：驗證 ===</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">uv</span> <span class="n">run</span> <span class="n">python</span> <span class="n">hook</span><span class="o">-</span><span class="n">name</span><span class="o">.</span><span class="n">py</span> <span class="o">&lt;</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">null</span></span></span></code></pre></div><h3 id="為什麼分批而非一次全改">為什麼分批而非一次全改</h3>
<table>
  <thead>
      <tr>
          <th>一次全改</th>
          <th>分批遷移</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改動 40+ 個檔案，review 困難</td>
          <td>每批 14-3 個，可仔細確認</td>
      </tr>
      <tr>
          <td>一個錯誤影響所有 Hook</td>
          <td>錯誤影響範圍有限</td>
      </tr>
      <tr>
          <td>無法中途暫停</td>
          <td>每批獨立可交付</td>
      </tr>
      <tr>
          <td>回滾等於全部回滾</td>
          <td>只回滾出問題的批次</td>
      </tr>
  </tbody>
</table>
<h2 id="遷移陷阱imp-005">遷移陷阱：IMP-005</h2>
<p>模組遷移最常見的陷阱是 <strong>import 路徑未同步更新</strong>。這個問題在系統中發生過兩次，我們將其記錄為 Error Pattern IMP-005。</p>
<h3 id="症狀-1">症狀</h3>
<p>模組從目錄 A 移到目錄 B 後，部分使用者的 import 忘記更新：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="kn">from</span> <span class="nn">common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># OK</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"># 遷移後（模組移到 lib/，但 import 未更新）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kn">from</span> <span class="nn">common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># ModuleNotFoundError!</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"># 正確的遷移後 import</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># OK</span></span></span></code></pre></div><h3 id="為什麼容易遺漏">為什麼容易遺漏</h3>
<ol>
<li><strong>py_compile 不偵測 import 問題</strong>：只檢查語法，不解析模組路徑</li>
<li><strong>部分 Hook 不常觸發</strong>：SessionStart Hook 只在啟動時執行，測試不容易覆蓋</li>
<li><strong>多源錯誤疊加</strong>：多個 Hook 同時報錯，修完幾個就以為全部修好</li>
</ol>
<h3 id="遷移前強制檢查清單">遷移前強制檢查清單</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 列出所有引用舊路徑的檔案</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -r <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py
</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"># 2. 逐一更新每個引用者的 import 路徑</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"># 3. 逐一驗證（不能只跑其中幾個！）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">for</span> f in .claude/hooks/*.py<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    uv run python <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> &lt; /dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> grep -q <span class="s2">&#34;Error&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;FAIL: </span><span class="nv">$f</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><h3 id="import-防護機制">Import 防護機制</h3>
<p>在 Hook 入口加 try-except，讓 import 失敗時顯示具體原因：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">setup_hook_logging</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">except</span> <span class="ne">ImportError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Import Error] </span><span class="si">{</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span></span></span></code></pre></div><h2 id="實際案例統計">實際案例統計</h2>
<p>v0.28.0 初建共用庫：</p>
<table>
  <thead>
      <tr>
          <th>函式</th>
          <th>重複次數</th>
          <th>重構後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>run_git_command</td>
          <td>4</td>
          <td>1 (git_utils.py)</td>
      </tr>
      <tr>
          <td>get_current_branch</td>
          <td>3</td>
          <td>1 (git_utils.py)</td>
      </tr>
      <tr>
          <td>parse_worktree_line</td>
          <td>2</td>
          <td>1 (git_utils.py)</td>
      </tr>
      <tr>
          <td>load_json</td>
          <td>2</td>
          <td>1 (hook_io.py)</td>
      </tr>
  </tbody>
</table>
<p>總計消除數百行重複程式碼。</p>
<p>v0.31.0 持續演進：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>重複次數</th>
          <th>重構後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>setup_hook_logging</td>
          <td>2 套系統</td>
          <td>1 (hook_utils.py)</td>
      </tr>
      <tr>
          <td>run_hook_safely</td>
          <td>40+ 處 try-except</td>
          <td>1 (hook_utils.py)</td>
      </tr>
      <tr>
          <td>使用者訊息字串</td>
          <td>19 個 Hook 散落</td>
          <td>1 (hook_messages.py)</td>
      </tr>
  </tbody>
</table>
<h2 id="常見錯誤">常見錯誤</h2>
<h3 id="錯誤-1過早抽象">錯誤 1：過早抽象</h3>
<p>只用一次就抽出去是過度抽象。<strong>原則</strong>：至少重複兩次再抽取（Rule of Three）。</p>
<h3 id="錯誤-2強行統一">錯誤 2：強行統一</h3>
<p>不同概念硬塞進同一個函式（靠 mode 參數切換）。<strong>解決</strong>：不同概念應該是不同的函式。</p>
<h3 id="錯誤-3忽略測試">錯誤 3：忽略測試</h3>
<p>重構時沒有先寫測試，導致引入新 bug。<strong>原則</strong>：先寫測試，確保重構不改變行為。</p>
<h3 id="錯誤-4遷移不徹底">錯誤 4：遷移不徹底</h3>
<p>模組搬家後只更新「自己知道的」使用處。<strong>原則</strong>：用 grep 列出所有引用，逐一更新並驗證（詳見 IMP-005）。</p>
<h2 id="實作練習">實作練習</h2>
<h3 id="練習-1識別重複">練習 1：識別重複</h3>
<p>找出以下程式碼的可抽取重複：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># file1.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">process_user_data</span><span class="p">(</span><span class="n">user</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="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;name&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少姓名&#34;</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="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;email&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少信箱&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;success&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&#34;data&#34;</span><span class="p">:</span> <span class="n">user</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="c1"># file2.py</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">def</span> <span class="nf">process_order_data</span><span class="p">(</span><span class="n">order</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="ow">not</span> <span class="n">order</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;product&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少商品&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">order</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;quantity&#34;</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="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少數量&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;success&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&#34;data&#34;</span><span class="p">:</span> <span class="n">order</span><span class="p">}</span></span></span></code></pre></div><details>
<summary>參考答案</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">validate_required_fields</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">required_fields</span><span class="p">:</span> <span class="nb">list</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;驗證必填欄位。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">required_fields</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="ow">not</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">field</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;缺少</span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;success&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&#34;data&#34;</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></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">process_user_data</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="n">validate_required_fields</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;name&#34;</span><span class="p">,</span> <span class="s2">&#34;email&#34;</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="k">def</span> <span class="nf">process_order_data</span><span class="p">(</span><span class="n">order</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="n">validate_required_fields</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;product&#34;</span><span class="p">,</span> <span class="s2">&#34;quantity&#34;</span><span class="p">])</span></span></span></code></pre></div></details>
<h3 id="練習-2規劃遷移策略">練習 2：規劃遷移策略</h3>
<p>20 個 Hook 要從 <code>from common_functions import setup_logging</code> 遷移到 <code>from hook_utils import setup_hook_logging</code>，請規劃遷移策略。</p>
<details>
<summary>參考答案</summary>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 盤點</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">grep -rl <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py <span class="p">|</span> wc -l
</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"># 2. 分批（按事件類型）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 第一批：SessionStart hooks（啟動就能看到）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 第二批：UserPromptSubmit hooks</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 第三批：PreToolUse / PostToolUse hooks</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="c1"># 3. 逐批執行，每批完成後 commit</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"># 4. 全量掃描（不可省略！防止 IMP-005）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">grep -r <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 預期輸出：空</span></span></span></code></pre></div></details>
<h2 id="小結">小結</h2>
<ul>
<li>DRY 原則要求每個知識只有單一權威來源，用 <code>grep</code> 識別重複的函式定義</li>
<li>不要過早抽象，至少重複兩次再抽取（Rule of Three）</li>
<li>建立結構清晰的共用程式庫，重構前先寫測試確保行為不變</li>
<li>共用庫隨系統成長持續演進，大規模遷移採用分批策略</li>
<li>模組搬家後必須全量 <code>grep</code> 引用並逐一驗證，防止 IMP-005 陷阱</li>
</ul>
<p>下一章：<a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a></p>
<hr>
<p><em>文件版本：v0.31.1</em>
<em>建立日期：2026-03-04</em></p>
]]></content:encoded></item><item><title>配置分離與常數管理</title><link>https://tarrragon.github.io/blog/python/07-refactoring/constants-management/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/constants-management/</guid><description>&lt;p>上一章：&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫&lt;/a>&lt;/p>
&lt;p>硬編碼問題不只是魔法數字。當專案成長到數十個模組時，三種不同形態的硬編碼會同時出現：看不懂的數字、混在邏輯裡的配置資料、散落各處的使用者訊息。本章整合 Error Pattern IMP-002（魔法數字）和 ARCH-001（配置與邏輯混合）的實戰經驗，並加入 W23 訊息集中化的完整案例。&lt;/p>
&lt;hr>
&lt;h2 id="三種硬編碼問題">三種硬編碼問題&lt;/h2>
&lt;p>在維護 19 個 Hook 模組的過程中，我們遇到了三種不同但相關的硬編碼問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>Error Pattern&lt;/th>
 &lt;th>典型症狀&lt;/th>
 &lt;th>危害&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>魔法數字&lt;/td>
 &lt;td>IMP-002&lt;/td>
 &lt;td>&lt;code>line[9:]&lt;/code>、&lt;code>sleep(3)&lt;/code>、&lt;code>range(5)&lt;/code>&lt;/td>
 &lt;td>無法理解數字含義，修改時容易遺漏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配置混合&lt;/td>
 &lt;td>ARCH-001&lt;/td>
 &lt;td>800 行檔案中 400 行是配置資料&lt;/td>
 &lt;td>配置散落各處，同一資料有多個版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>散落訊息&lt;/td>
 &lt;td>W23 發現&lt;/td>
 &lt;td>57+ 個硬編碼中文字串散落在 19 個檔案中&lt;/td>
 &lt;td>訊息不一致，無法統一維護&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三種問題的共同根因：&lt;strong>開發時為求快速，把應該集中管理的資料直接寫在邏輯程式碼裡。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="一消除魔法數字-imp-002">一、消除魔法數字 (IMP-002)&lt;/h2>
&lt;p>魔法數字是程式碼中無法理解含義的字面值：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">parse_worktree_line&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;worktree &amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">9&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="c1"># 為什麼是 9？&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">return&lt;/span> &lt;span class="n">line&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="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">branch&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># 為什麼是 50？&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;分支名稱過長&amp;#34;&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">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 為什麼等 3 秒？&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>問題不只是可讀性。當前綴改成 &lt;code>&amp;quot;work tree &amp;quot;&lt;/code> 時，&lt;code>line[9:]&lt;/code> 不會自動更新，產生隱蔽的 bug。&lt;/p>
&lt;h3 id="三種消除方法">三種消除方法&lt;/h3>
&lt;h4 id="方法-1len-動態計算最安全">方法 1：len() 動態計算（最安全）&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">WORKTREE_PREFIX&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;worktree &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">parse_worktree_line&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&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="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">WORKTREE_PREFIX&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="k">return&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">WORKTREE_PREFIX&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="k">return&lt;/span> &lt;span class="n">line&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>前綴改變時切片自動正確，不需要同步更新數字。&lt;/p>
&lt;h4 id="方法-2removeprefix最簡潔python-39">方法 2：removeprefix（最簡潔，Python 3.9+）&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">WORKTREE_PREFIX&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;worktree &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">parse_worktree_line&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">line&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&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">return&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">removeprefix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">WORKTREE_PREFIX&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不需要先檢查 &lt;code>startswith&lt;/code>，沒有前綴時安全返回原字串。&lt;/p>
&lt;h4 id="方法-3intenum-管理相關常數群組">方法 3：IntEnum 管理相關常數群組&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">enum&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">IntEnum&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">Limits&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IntEnum&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">MAX_BRANCH_LENGTH&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">MAX_COMMIT_MSG_LENGTH&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">72&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">MAX_RETRIES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="n">TIMEOUT_SECONDS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">30&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="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">branch&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">Limits&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">MAX_BRANCH_LENGTH&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="k">raise&lt;/span> &lt;span class="ne">ValueError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;分支名稱過長&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="常見處理對照">常見處理對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>壞&lt;/th>
 &lt;th>好&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>字串切片&lt;/td>
 &lt;td>&lt;code>line[7:]&lt;/code>&lt;/td>
 &lt;td>&lt;code>line.removeprefix(PREFIX)&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間限制&lt;/td>
 &lt;td>&lt;code>sleep(3)&lt;/code>&lt;/td>
 &lt;td>&lt;code>sleep(RETRY_DELAY_SECONDS)&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大小限制&lt;/td>
 &lt;td>&lt;code>len(x) &amp;gt; 50&lt;/code>&lt;/td>
 &lt;td>&lt;code>len(x) &amp;gt; MAX_BRANCH_LENGTH&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重試次數&lt;/td>
 &lt;td>&lt;code>range(5)&lt;/code>&lt;/td>
 &lt;td>&lt;code>range(MAX_RETRIES)&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="可接受的例外">可接受的例外&lt;/h3>
&lt;p>不是所有數字都需要命名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">count&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># 可接受：0 在布林邏輯中&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">text&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;key&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># 可接受：-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="n">half&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">total&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="c1"># 可接受：明顯的數學常數&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判斷標準：&lt;strong>如果閱讀者需要思考「這個數字為什麼是這個值」，就應該命名。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="二yaml-配置分離-arch-001">二、YAML 配置分離 (ARCH-001)&lt;/h2>
&lt;h3 id="問題識別">問題識別&lt;/h3>
&lt;p>單一 Hook 檔案超過 800 行，其中約一半是硬編碼的配置資料：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># user_prompt_submit.py (847 行，配置佔 400+)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">PROTECTED_BRANCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;develop&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">ALLOWED_PATTERNS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;feat/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fix/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;chore/*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n">ERROR_MESSAGES&lt;/span> &lt;span class="o">=&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="s2">&amp;#34;branch_not_allowed&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;分支名稱不符合規範&amp;#34;&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="s2">&amp;#34;missing_ticket&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;缺少 Ticket 引用&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ... 數百行配置&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&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="k">def&lt;/span> &lt;span class="nf">main&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="c1"># 實際邏輯只有 200 行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">pass&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>更嚴重的是，同一份配置在多個檔案中各自定義，彼此不一致：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># file1.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">PROTECTED_BRANCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># file2.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n">PROTECTED_BRANCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;master&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;develop&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1"># 多了 develop！&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="判斷標準">判斷標準&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題&lt;/th>
 &lt;th>若答「是」&lt;/th>
 &lt;th>放置位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>會隨環境改變？&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>YAML 配置檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>非工程師可能修改？&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>YAML 配置檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>是業務規則？&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>程式碼常數檔（附註解）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>與程式邏輯緊密耦合？&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>程式碼內常數&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>簡單記憶：&lt;strong>資料放配置，邏輯留程式碼。&lt;/strong>&lt;/p></description><content:encoded><![CDATA[<p>上一章：<a href="/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a></p>
<p>硬編碼問題不只是魔法數字。當專案成長到數十個模組時，三種不同形態的硬編碼會同時出現：看不懂的數字、混在邏輯裡的配置資料、散落各處的使用者訊息。本章整合 Error Pattern IMP-002（魔法數字）和 ARCH-001（配置與邏輯混合）的實戰經驗，並加入 W23 訊息集中化的完整案例。</p>
<hr>
<h2 id="三種硬編碼問題">三種硬編碼問題</h2>
<p>在維護 19 個 Hook 模組的過程中，我們遇到了三種不同但相關的硬編碼問題：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>Error Pattern</th>
          <th>典型症狀</th>
          <th>危害</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>魔法數字</td>
          <td>IMP-002</td>
          <td><code>line[9:]</code>、<code>sleep(3)</code>、<code>range(5)</code></td>
          <td>無法理解數字含義，修改時容易遺漏</td>
      </tr>
      <tr>
          <td>配置混合</td>
          <td>ARCH-001</td>
          <td>800 行檔案中 400 行是配置資料</td>
          <td>配置散落各處，同一資料有多個版本</td>
      </tr>
      <tr>
          <td>散落訊息</td>
          <td>W23 發現</td>
          <td>57+ 個硬編碼中文字串散落在 19 個檔案中</td>
          <td>訊息不一致，無法統一維護</td>
      </tr>
  </tbody>
</table>
<p>三種問題的共同根因：<strong>開發時為求快速，把應該集中管理的資料直接寫在邏輯程式碼裡。</strong></p>
<hr>
<h2 id="一消除魔法數字-imp-002">一、消除魔法數字 (IMP-002)</h2>
<p>魔法數字是程式碼中無法理解含義的字面值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;worktree &#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">return</span> <span class="n">line</span><span class="p">[</span><span class="mi">9</span><span class="p">:]</span>  <span class="c1"># 為什麼是 9？</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">line</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="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">branch</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">50</span><span class="p">:</span>    <span class="c1"># 為什麼是 50？</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">raise</span> <span class="n">Error</span><span class="p">(</span><span class="s2">&#34;分支名稱過長&#34;</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">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>           <span class="c1"># 為什麼等 3 秒？</span></span></span></code></pre></div><p>問題不只是可讀性。當前綴改成 <code>&quot;work tree &quot;</code> 時，<code>line[9:]</code> 不會自動更新，產生隱蔽的 bug。</p>
<h3 id="三種消除方法">三種消除方法</h3>
<h4 id="方法-1len-動態計算最安全">方法 1：len() 動態計算（最安全）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">WORKTREE_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;worktree &#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</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="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">WORKTREE_PREFIX</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="n">line</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">WORKTREE_PREFIX</span><span class="p">):]</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="n">line</span></span></span></code></pre></div><p>前綴改變時切片自動正確，不需要同步更新數字。</p>
<h4 id="方法-2removeprefix最簡潔python-39">方法 2：removeprefix（最簡潔，Python 3.9+）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">WORKTREE_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;worktree &#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">parse_worktree_line</span><span class="p">(</span><span class="n">line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">line</span><span class="o">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="n">WORKTREE_PREFIX</span><span class="p">)</span></span></span></code></pre></div><p>不需要先檢查 <code>startswith</code>，沒有前綴時安全返回原字串。</p>
<h4 id="方法-3intenum-管理相關常數群組">方法 3：IntEnum 管理相關常數群組</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">IntEnum</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">Limits</span><span class="p">(</span><span class="n">IntEnum</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">MAX_BRANCH_LENGTH</span> <span class="o">=</span> <span class="mi">50</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">MAX_COMMIT_MSG_LENGTH</span> <span class="o">=</span> <span class="mi">72</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">MAX_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">TIMEOUT_SECONDS</span> <span class="o">=</span> <span class="mi">30</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="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">branch</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">Limits</span><span class="o">.</span><span class="n">MAX_BRANCH_LENGTH</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;分支名稱過長&#34;</span><span class="p">)</span></span></span></code></pre></div><h3 id="常見處理對照">常見處理對照</h3>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>壞</th>
          <th>好</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字串切片</td>
          <td><code>line[7:]</code></td>
          <td><code>line.removeprefix(PREFIX)</code></td>
      </tr>
      <tr>
          <td>時間限制</td>
          <td><code>sleep(3)</code></td>
          <td><code>sleep(RETRY_DELAY_SECONDS)</code></td>
      </tr>
      <tr>
          <td>大小限制</td>
          <td><code>len(x) &gt; 50</code></td>
          <td><code>len(x) &gt; MAX_BRANCH_LENGTH</code></td>
      </tr>
      <tr>
          <td>重試次數</td>
          <td><code>range(5)</code></td>
          <td><code>range(MAX_RETRIES)</code></td>
      </tr>
  </tbody>
</table>
<h3 id="可接受的例外">可接受的例外</h3>
<p>不是所有數字都需要命名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">if</span> <span class="n">count</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>               <span class="c1"># 可接受：0 在布林邏輯中</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="n">text</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>   <span class="c1"># 可接受：-1 作為找不到的標記</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">half</span> <span class="o">=</span> <span class="n">total</span> <span class="o">/</span> <span class="mi">2</span>              <span class="c1"># 可接受：明顯的數學常數</span></span></span></code></pre></div><p>判斷標準：<strong>如果閱讀者需要思考「這個數字為什麼是這個值」，就應該命名。</strong></p>
<hr>
<h2 id="二yaml-配置分離-arch-001">二、YAML 配置分離 (ARCH-001)</h2>
<h3 id="問題識別">問題識別</h3>
<p>單一 Hook 檔案超過 800 行，其中約一半是硬編碼的配置資料：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># user_prompt_submit.py (847 行，配置佔 400+)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">PROTECTED_BRANCHES</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">,</span> <span class="s2">&#34;develop&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">ALLOWED_PATTERNS</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;feat/*&#34;</span><span class="p">,</span> <span class="s2">&#34;fix/*&#34;</span><span class="p">,</span> <span class="s2">&#34;chore/*&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">ERROR_MESSAGES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;branch_not_allowed&#34;</span><span class="p">:</span> <span class="s2">&#34;分支名稱不符合規範&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="s2">&#34;missing_ticket&#34;</span><span class="p">:</span> <span class="s2">&#34;缺少 Ticket 引用&#34;</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="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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1"># 實際邏輯只有 200 行</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">pass</span></span></span></code></pre></div><p>更嚴重的是，同一份配置在多個檔案中各自定義，彼此不一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># file1.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">PROTECTED_BRANCHES</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># file2.py</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">PROTECTED_BRANCHES</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">,</span> <span class="s2">&#34;develop&#34;</span><span class="p">]</span>  <span class="c1"># 多了 develop！</span></span></span></code></pre></div><h3 id="判斷標準">判斷標準</h3>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>若答「是」</th>
          <th>放置位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>會隨環境改變？</td>
          <td>是</td>
          <td>YAML 配置檔</td>
      </tr>
      <tr>
          <td>非工程師可能修改？</td>
          <td>是</td>
          <td>YAML 配置檔</td>
      </tr>
      <tr>
          <td>是業務規則？</td>
          <td>是</td>
          <td>程式碼常數檔（附註解）</td>
      </tr>
      <tr>
          <td>與程式邏輯緊密耦合？</td>
          <td>是</td>
          <td>程式碼內常數</td>
      </tr>
  </tbody>
</table>
<p>簡單記憶：<strong>資料放配置，邏輯留程式碼。</strong></p>
<h3 id="實作config_loader-模式">實作：config_loader 模式</h3>
<h4 id="步驟-1抽離配置到-yaml">步驟 1：抽離配置到 YAML</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># config/branch_rules.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">protected_branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span>- <span class="l">master</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span>- <span class="l">develop</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">allowed_patterns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span>- <span class="s2">&#34;feat/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="s2">&#34;fix/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>- <span class="s2">&#34;chore/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">error_messages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">branch_not_allowed</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;分支名稱不符合規範&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">missing_ticket</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;缺少 Ticket 引用&#34;</span></span></span></code></pre></div><h4 id="步驟-2建立載入器含快取">步驟 2：建立載入器（含快取）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># lib/config_loader.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Dict</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">import</span> <span class="nn">yaml</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">_config_cache</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span> <span class="o">=</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="k">def</span> <span class="nf">load_config</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="s2">&#34;&#34;&#34;載入 YAML 配置檔案（含快取）。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">_config_cache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="n">_config_cache</span><span class="p">[</span><span class="n">filename</span><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="n">config_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;config&#34;</span> <span class="o">/</span> <span class="n">filename</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">config_path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;配置檔案不存在: </span><span class="si">{</span><span class="n">config_path</span><span class="si">}</span><span class="s2">&#34;</span><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="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">config_path</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">config</span> <span class="o">=</span> <span class="n">yaml</span><span class="o">.</span><span class="n">safe_load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">_config_cache</span><span class="p">[</span><span class="n">filename</span><span class="p">]</span> <span class="o">=</span> <span class="n">config</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">return</span> <span class="n">config</span></span></span></code></pre></div><h4 id="步驟-3在-hook-中使用">步驟 3：在 Hook 中使用</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.config_loader</span> <span class="kn">import</span> <span class="n">load_config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">check_branch</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">config</span> <span class="o">=</span> <span class="n">load_config</span><span class="p">(</span><span class="s2">&#34;branch_rules.yaml&#34;</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="n">current_branch</span> <span class="ow">in</span> <span class="n">config</span><span class="p">[</span><span class="s2">&#34;protected_branches&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;錯誤: </span><span class="si">{</span><span class="n">config</span><span class="p">[</span><span class="s1">&#39;error_messages&#39;</span><span class="p">][</span><span class="s1">&#39;branch_not_allowed&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="kc">True</span></span></span></code></pre></div><p>重構後結構：847 行的單一檔案拆成約 200 行純邏輯 + <code>config/</code> 目錄的 YAML 檔 + 共用的 <code>config_loader.py</code>。</p>
<h3 id="常見錯誤">常見錯誤</h3>
<p><strong>過度配置化</strong> &ndash; 把程式邏輯也放進配置檔：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># 錯誤：這是邏輯，不是資料</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">process_steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;validate&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">function</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;validate_input&#34;</span></span></span></code></pre></div><p><strong>缺乏預設值</strong> &ndash; 沒有處理配置缺失：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">timeout</span> <span class="o">=</span> <span class="n">config</span><span class="p">[</span><span class="s2">&#34;timeout&#34;</span><span class="p">]</span>        <span class="c1"># KeyError!</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">timeout</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;timeout&#34;</span><span class="p">,</span> <span class="mi">30</span><span class="p">)</span>  <span class="c1"># 正確</span></span></span></code></pre></div><hr>
<h2 id="三訊息集中化-w23">三、訊息集中化 (W23)</h2>
<p>消除魔法數字和分離配置後，還有一種硬編碼藏在邏輯裡：使用者訊息字串。</p>
<p>W23 審計發現 19 個 Hook 中散落了 57+ 個硬編碼中文字串：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># hook_a.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;錯誤：未找到待處理的 Ticket&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;建議執行 /ticket create 建立新 Ticket&#34;</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"># hook_b.py</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;錯誤：未找到待處理的 Ticket&#34;</span><span class="p">)</span>  <span class="c1"># 同一訊息，略有不同</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;請先建立 Ticket 再執行&#34;</span><span class="p">)</span></span></span></code></pre></div><p>同一個錯誤概念有 2-3 種不同措辭，修改一則訊息需要搜尋所有檔案。</p>
<h3 id="messages-類別模式">Messages 類別模式</h3>
<p>解決方案：建立 <code>hook_messages.py</code>，用類別分組管理所有訊息常數。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># lib/hook_messages.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">CoreMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Hook 執行通用訊息 - 所有 Hook 共用&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">HOOK_START</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">{hook_name}</span><span class="s2"> 啟動&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">INPUT_EMPTY</span> <span class="o">=</span> <span class="s2">&#34;輸入為空，預設允許&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">JSON_PARSE_ERROR</span> <span class="o">=</span> <span class="s2">&#34;JSON 解析錯誤，預設允許: </span><span class="si">{error}</span><span class="s2">&#34;</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="k">class</span> <span class="nc">GateMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="s2">&#34;&#34;&#34;Gate Hook 阻擋訊息 - 5 個 gate hooks 使用&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">TICKET_NOT_FOUND_ERROR</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;錯誤：未找到待處理的 Ticket
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">建議: 執行 /ticket create 建立新 Ticket&#34;&#34;&#34;</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="n">TICKET_NOT_CLAIMED_ERROR</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;錯誤：Ticket </span><span class="si">{ticket_id}</span><span class="s2"> 尚未認領
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">建議: 執行 /ticket track claim </span><span class="si">{ticket_id}</span><span class="s2"> 認領&#34;&#34;&#34;</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="k">class</span> <span class="nc">WorkflowMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="s2">&#34;&#34;&#34;工作流指導訊息 - 5 個工作流 hooks 使用&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">PRE_FIX_EVAL_REQUIRED</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;[強制] 修復前評估
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s2">  1. 執行 /pre-fix-eval
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="s2">  2. 派發 incident-responder 分析&#34;&#34;&#34;</span></span></span></code></pre></div><p>最終產出 7 個 Messages 類別，管理約 45 個訊息常數。</p>
<h3 id="使用方式">使用方式</h3>
<p>Hook 中引用常數，使用 <code>.format()</code> 填入動態值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.hook_messages</span> <span class="kn">import</span> <span class="n">GateMessages</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">validate_ticket</span><span class="p">(</span><span class="n">ticket_id</span><span class="p">:</span> <span class="nb">str</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="ow">not</span> <span class="n">is_claimed</span><span class="p">(</span><span class="n">ticket_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">GateMessages</span><span class="o">.</span><span class="n">TICKET_NOT_CLAIMED_ERROR</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">            <span class="n">ticket_id</span><span class="o">=</span><span class="n">ticket_id</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="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">    <span class="k">return</span> <span class="kc">True</span></span></span></code></pre></div><h3 id="組織原則">組織原則</h3>
<table>
  <thead>
      <tr>
          <th>分類依據</th>
          <th>類別名稱</th>
          <th>涵蓋範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心通用</td>
          <td><code>CoreMessages</code></td>
          <td>所有 Hook 共用的啟動、錯誤訊息</td>
      </tr>
      <tr>
          <td>阻擋訊息</td>
          <td><code>GateMessages</code></td>
          <td>5 個 Gate Hook 的阻止原因和建議</td>
      </tr>
      <tr>
          <td>工作流指導</td>
          <td><code>WorkflowMessages</code></td>
          <td>5 個工作流 Hook 的流程提示</td>
      </tr>
      <tr>
          <td>品質檢查</td>
          <td><code>QualityMessages</code></td>
          <td>5 個品質 Hook 的檢查結果</td>
      </tr>
      <tr>
          <td>驗證相關</td>
          <td><code>ValidationMessages</code></td>
          <td>驗證 Hook 的成功/失敗訊息</td>
      </tr>
  </tbody>
</table>
<p>分類原則：<strong>按使用者角色和觸發情境分組，而不是按技術功能。</strong></p>
<h3 id="命名規範">命名規範</h3>
<table>
  <thead>
      <tr>
          <th>常數類型</th>
          <th>命名規則</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊息常數</td>
          <td>大寫蛇形</td>
          <td><code>TICKET_NOT_FOUND_ERROR</code></td>
      </tr>
      <tr>
          <td>Messages 類別</td>
          <td>PascalCase + Messages</td>
          <td><code>GateMessages</code></td>
      </tr>
      <tr>
          <td>格式化佔位符</td>
          <td><code>{variable_name}</code></td>
          <td><code>&quot;Ticket {ticket_id} 尚未認領&quot;</code></td>
      </tr>
  </tbody>
</table>
<h3 id="w23-實際數據">W23 實際數據</h3>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>重構前</th>
          <th>重構後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬編碼訊息位置</td>
          <td>散落 19 個檔案</td>
          <td>集中 1 個檔案</td>
      </tr>
      <tr>
          <td>訊息總數</td>
          <td>57+ 個（含重複）</td>
          <td>45 個（去重後）</td>
      </tr>
      <tr>
          <td>修改訊息需搜尋</td>
          <td>所有 Hook 檔案</td>
          <td>只需 hook_messages.py</td>
      </tr>
      <tr>
          <td>訊息一致性</td>
          <td>同概念 2-3 種措辭</td>
          <td>每個概念一個定義</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="決策框架">決策框架</h2>
<p>遇到硬編碼時，用這張表判斷該怎麼處理：</p>
<table>
  <thead>
      <tr>
          <th>硬編碼類型</th>
          <th>識別特徵</th>
          <th>處理方式</th>
          <th>存放位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>魔法數字</td>
          <td>裸露的數字或字串切片</td>
          <td>具名常數、<code>len()</code>、<code>removeprefix()</code></td>
          <td>同檔案頂部或常數模組</td>
      </tr>
      <tr>
          <td>配置資料</td>
          <td>清單、規則表、業務參數</td>
          <td>抽離到 YAML 配置檔</td>
          <td><code>config/</code> 目錄</td>
      </tr>
      <tr>
          <td>使用者訊息</td>
          <td>字串直接嵌入邏輯</td>
          <td>提取到 Messages 類別</td>
          <td><code>lib/*_messages.py</code></td>
      </tr>
      <tr>
          <td>程式邏輯常數</td>
          <td>與邏輯緊密耦合的值</td>
          <td>具名常數，保留在程式碼</td>
          <td>檔案頂部</td>
      </tr>
  </tbody>
</table>
<h3 id="決策流程">決策流程</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">發現硬編碼
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    |
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">會隨環境改變？ ─是→ YAML 配置檔
</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></span><span class="line"><span class="ln"> 7</span><span class="cl">    v
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">是使用者看到的文字？ ─是→ Messages 類別
</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></span><span class="line"><span class="ln">11</span><span class="cl">    v
</span></span><span class="line"><span class="ln">12</span><span class="cl">是無法理解的數字？ ─是→ 具名常數 / len() / removeprefix()
</span></span><span class="line"><span class="ln">13</span><span class="cl">    |
</span></span><span class="line"><span class="ln">14</span><span class="cl">    否
</span></span><span class="line"><span class="ln">15</span><span class="cl">    v
</span></span><span class="line"><span class="ln">16</span><span class="cl">保留原樣（程式邏輯的一部分）</span></span></code></pre></div><hr>
<h2 id="完整重構範例">完整重構範例</h2>
<h3 id="重構前">重構前</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">validate_branch</span><span class="p">(</span><span class="n">branch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">branch</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">50</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="n">branch</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;refs/heads/&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">branch</span> <span class="o">=</span> <span class="n">branch</span><span class="p">[</span><span class="mi">11</span><span class="p">:]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</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="n">check_remote</span><span class="p">(</span><span class="n">branch</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="kc">True</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="kc">False</span></span></span></code></pre></div><h3 id="重構後">重構後</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">MAX_BRANCH_LENGTH</span> <span class="o">=</span> <span class="mi">50</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">REFS_HEADS_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;refs/heads/&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">MAX_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">RETRY_DELAY_SECONDS</span> <span class="o">=</span> <span class="mi">2</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="k">def</span> <span class="nf">validate_branch</span><span class="p">(</span><span class="n">branch</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="s2">&#34;&#34;&#34;驗證分支名稱。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">branch</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">MAX_BRANCH_LENGTH</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">branch</span> <span class="o">=</span> <span class="n">branch</span><span class="o">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="n">REFS_HEADS_PREFIX</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">for</span> <span class="n">attempt</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">MAX_RETRIES</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="n">check_remote</span><span class="p">(</span><span class="n">branch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">RETRY_DELAY_SECONDS</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="kc">False</span></span></span></code></pre></div><p>四個魔法數字全部消除，每個值的含義一目了然。</p>
<hr>
<h2 id="檢測方法">檢測方法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 找出數字切片（潛在魔法數字）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rn <span class="s2">&#34;\[[0-9]*:\]&#34;</span> hooks/*.py
</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"># 找出 sleep 和 range 中的硬編碼</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">grep -rn <span class="s2">&#34;sleep([0-9]&#34;</span> hooks/*.py
</span></span><span class="line"><span class="ln">6</span><span class="cl">grep -rn <span class="s2">&#34;range([0-9]&#34;</span> hooks/*.py
</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">grep -rn <span class="s1">&#39;[一-龥]&#39;</span> hooks/*.py</span></span></code></pre></div><hr>
<h2 id="實作練習">實作練習</h2>
<p>找出以下程式碼中的三種硬編碼問題，並提出修正方案：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">process_hook_result</span><span class="p">(</span><span class="n">result_line</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">if</span> <span class="n">result_line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;status: &#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="n">result_line</span><span class="p">[</span><span class="mi">8</span><span class="p">:]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="s2">&#34;unknown&#34;</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="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;狀態文字過長，已截斷&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="n">status</span><span class="p">[:</span><span class="mi">97</span><span class="p">]</span> <span class="o">+</span> <span class="s2">&#34;...&#34;</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">VALID_STATUSES</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;pass&#34;</span><span class="p">,</span> <span class="s2">&#34;fail&#34;</span><span class="p">,</span> <span class="s2">&#34;skip&#34;</span><span class="p">,</span> <span class="s2">&#34;error&#34;</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="n">status</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">VALID_STATUSES</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;無效的狀態值: &#34;</span> <span class="o">+</span> <span class="n">status</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="kc">None</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">status</span></span></span></code></pre></div><details>
<summary>參考答案</summary>
<p>三種硬編碼問題：</p>
<ol>
<li><strong>魔法數字</strong>：<code>result_line[8:]</code>、<code>100</code>、<code>97</code></li>
<li><strong>配置資料</strong>：<code>VALID_STATUSES</code> 清單應該可配置</li>
<li><strong>散落訊息</strong>：<code>&quot;狀態文字過長，已截斷&quot;</code>、<code>&quot;無效的狀態值: &quot;</code></li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.config_loader</span> <span class="kn">import</span> <span class="n">load_config</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">STATUS_PREFIX</span> <span class="o">=</span> <span class="s2">&#34;status: &#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">MAX_STATUS_LENGTH</span> <span class="o">=</span> <span class="mi">100</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">ELLIPSIS</span> <span class="o">=</span> <span class="s2">&#34;...&#34;</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="k">class</span> <span class="nc">HookResultMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">STATUS_TRUNCATED</span> <span class="o">=</span> <span class="s2">&#34;狀態文字過長，已截斷&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">INVALID_STATUS</span> <span class="o">=</span> <span class="s2">&#34;無效的狀態值: </span><span class="si">{status}</span><span class="s2">&#34;</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="k">def</span> <span class="nf">process_hook_result</span><span class="p">(</span><span class="n">result_line</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">status</span> <span class="o">=</span> <span class="n">result_line</span><span class="o">.</span><span class="n">removeprefix</span><span class="p">(</span><span class="n">STATUS_PREFIX</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="n">result_line</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="s2">&#34;unknown&#34;</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="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">status</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">MAX_STATUS_LENGTH</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">HookResultMessages</span><span class="o">.</span><span class="n">STATUS_TRUNCATED</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">truncate_at</span> <span class="o">=</span> <span class="n">MAX_STATUS_LENGTH</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">ELLIPSIS</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="n">status</span><span class="p">[:</span><span class="n">truncate_at</span><span class="p">]</span> <span class="o">+</span> <span class="n">ELLIPSIS</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">config</span> <span class="o">=</span> <span class="n">load_config</span><span class="p">(</span><span class="s2">&#34;hook_rules.yaml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">if</span> <span class="n">status</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">config</span><span class="p">[</span><span class="s2">&#34;valid_statuses&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">HookResultMessages</span><span class="o">.</span><span class="n">INVALID_STATUS</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">status</span><span class="o">=</span><span class="n">status</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">return</span> <span class="n">status</span></span></span></code></pre></div></details>
<hr>
<h2 id="小結">小結</h2>
<ul>
<li>硬編碼問題有三種形態：魔法數字、配置混合、散落訊息</li>
<li>魔法數字用 <code>len()</code>、<code>removeprefix()</code>、<code>IntEnum</code> 消除</li>
<li>配置資料用 YAML 檔案集中管理，透過 <code>config_loader</code> 載入</li>
<li>使用者訊息用 Messages 類別集中化，按角色和情境分組</li>
<li>決策關鍵：<strong>會隨環境改變 → 配置檔；是使用者文字 → Messages；是裸露數字 → 常數</strong></li>
</ul>
<p>下一章：<a href="/blog/python/07-refactoring/unified-infrastructure/" data-link-title="大規模統一化重構" data-link-desc="從 44 種不同實作到統一基礎設施：日誌、訊息、風格的三階段漸進式重構">大規模統一化重構</a></p>
<hr>
<p><em>文件版本：v0.31.1</em>
<em>建立日期：2026-03-04</em></p>
]]></content:encoded></item><item><title>大規模統一化重構</title><link>https://tarrragon.github.io/blog/python/07-refactoring/unified-infrastructure/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/unified-infrastructure/</guid><description>&lt;p>前面幾章的重構案例都在解決局部問題：提取常數、分離配置、消除重複。本章探討一個更大的挑戰：當系統中有 &lt;strong>44 個獨立腳本&lt;/strong>，各自發展出不同的基礎設施實作時，如何系統性地統一它們？&lt;/p>
&lt;p>這是 W22-W24 開發週期中實際執行的三階段統一化重構。每個階段解決一個維度的分歧，最終讓所有 Hook 共享同一套基礎設施。&lt;/p>
&lt;h2 id="問題全貌">問題全貌&lt;/h2>
&lt;h3 id="44-個-hookn-種實作">44 個 Hook，N 種實作&lt;/h3>
&lt;p>Hook 系統經過數個版本的有機成長，累積了大量不一致：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Hook A：用 common_functions 的 setup_hook_logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">lib.common_functions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook-a&amp;#34;&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"># Hook B：用 hook_logging 的 setup_hook_logging（不同模組，同名函式）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">lib.hook_logging&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook-b&amp;#34;&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="c1"># Hook C：直接用 logging 模組&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">basicConfig&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">level&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">INFO&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="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&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>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># Hook D：print 大法&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;[hook-d] &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">msg&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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>3 種&lt;/td>
 &lt;td>common_functions / hook_logging / 直接 logging&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>錯誤處理&lt;/td>
 &lt;td>3 種&lt;/td>
 &lt;td>try-except 包 main / 不處理 / 自訂裝飾器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者訊息&lt;/td>
 &lt;td>19 個檔案各自定義&lt;/td>
 &lt;td>每個 Hook 硬編碼自己的字串（共 57+ 個）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>logger 作用域&lt;/td>
 &lt;td>2 種&lt;/td>
 &lt;td>模組級全域 / main() 內區域&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="為什麼要統一">為什麼要統一&lt;/h3>
&lt;p>分歧帶來的實際問題：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>修改成本倍增&lt;/strong>：改一個日誌格式，要改 44 個檔案&lt;/li>
&lt;li>&lt;strong>行為不一致&lt;/strong>：有的 Hook 失敗時靜默，有的會 crash&lt;/li>
&lt;li>&lt;strong>難以排查問題&lt;/strong>：每個 Hook 的日誌格式不同，無法統一搜尋&lt;/li>
&lt;li>&lt;strong>新 Hook 沒有範本&lt;/strong>：寫新 Hook 時不知道該參考哪個&lt;/li>
&lt;/ol>
&lt;h2 id="統一化模式">統一化模式&lt;/h2>
&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">1. 建立統一介面 → 寫一個所有人都要用的模組
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2. 漸進式遷移 → 逐批將現有 Hook 切換到新介面
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">3. 驗證 → 確認行為一致
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">4. 處理例外 → 處理少數無法直接遷移的情況&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個模式的關鍵在於&lt;strong>不一次改完&lt;/strong>。每個階段只統一一個維度，確認穩定後再進入下一個。&lt;/p>
&lt;h2 id="第一階段統一日誌w22">第一階段：統一日誌（W22）&lt;/h2>
&lt;h3 id="設計統一介面">設計統一介面&lt;/h3>
&lt;p>目標是用一個模組取代三套日誌實作。核心 API 只有兩個函式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># hook_utils.py — 統一日誌模組&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Logger&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="s2">&amp;#34;&amp;#34;&amp;#34;建立並設定 Hook 日誌系統
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 建立日誌目錄 .claude/hook-logs/&lt;/span>&lt;span class="si">{hook_name}&lt;/span>&lt;span class="s2">/
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 建立帶時間戳的日誌檔案
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 配置 FileHandler + StreamHandler
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">run_hook_safely&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">main_func&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Callable&lt;/span>&lt;span class="p">[[],&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">hook_name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">int&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="s2">&amp;#34;&amp;#34;&amp;#34;安全執行 Hook 函式，頂層例外處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="s2">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 呼叫 setup_hook_logging 取得 logger
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 執行 main_func，捕獲所有 Exception
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="s2"> - 異常時記錄完整 traceback，返回 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>setup_hook_logging&lt;/code> 封裝了所有日誌配置細節：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="n">sanitized_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_sanitize_hook_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">root_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_find_project_root&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">log_base_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">root_dir&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;.claude&amp;#34;&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;hook-logs&amp;#34;&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">sanitized_name&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="k">try&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="n">log_base_dir&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkdir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parents&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exist_ok&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">OSError&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="k">return&lt;/span> &lt;span class="n">_create_fallback_logger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&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>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&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="n">_clear_logger_handlers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&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="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setLevel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DEBUG&lt;/span>&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>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="n">is_debug&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;HOOK_DEBUG&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lower&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">_setup_logger_handlers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">log_base_dir&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sanitized_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">is_debug&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">logger&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>幾個設計決策值得注意：&lt;/p></description><content:encoded><![CDATA[<p>前面幾章的重構案例都在解決局部問題：提取常數、分離配置、消除重複。本章探討一個更大的挑戰：當系統中有 <strong>44 個獨立腳本</strong>，各自發展出不同的基礎設施實作時，如何系統性地統一它們？</p>
<p>這是 W22-W24 開發週期中實際執行的三階段統一化重構。每個階段解決一個維度的分歧，最終讓所有 Hook 共享同一套基礎設施。</p>
<h2 id="問題全貌">問題全貌</h2>
<h3 id="44-個-hookn-種實作">44 個 Hook，N 種實作</h3>
<p>Hook 系統經過數個版本的有機成長，累積了大量不一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Hook A：用 common_functions 的 setup_hook_logging</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">setup_hook_logging</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook-a&#34;</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"># Hook B：用 hook_logging 的 setup_hook_logging（不同模組，同名函式）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.hook_logging</span> <span class="kn">import</span> <span class="n">setup_hook_logging</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook-b&#34;</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="c1"># Hook C：直接用 logging 模組</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">logging</span><span class="o">.</span><span class="n">basicConfig</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Hook D：print 大法</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">def</span> <span class="nf">log</span><span class="p">(</span><span class="n">msg</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[hook-d] </span><span class="si">{</span><span class="n">msg</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div><p>不只日誌，訊息和錯誤處理也是如此：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>分歧數量</th>
          <th>常見變體</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日誌初始化</td>
          <td>3 種</td>
          <td>common_functions / hook_logging / 直接 logging</td>
      </tr>
      <tr>
          <td>錯誤處理</td>
          <td>3 種</td>
          <td>try-except 包 main / 不處理 / 自訂裝飾器</td>
      </tr>
      <tr>
          <td>使用者訊息</td>
          <td>19 個檔案各自定義</td>
          <td>每個 Hook 硬編碼自己的字串（共 57+ 個）</td>
      </tr>
      <tr>
          <td>logger 作用域</td>
          <td>2 種</td>
          <td>模組級全域 / main() 內區域</td>
      </tr>
  </tbody>
</table>
<h3 id="為什麼要統一">為什麼要統一</h3>
<p>分歧帶來的實際問題：</p>
<ol>
<li><strong>修改成本倍增</strong>：改一個日誌格式，要改 44 個檔案</li>
<li><strong>行為不一致</strong>：有的 Hook 失敗時靜默，有的會 crash</li>
<li><strong>難以排查問題</strong>：每個 Hook 的日誌格式不同，無法統一搜尋</li>
<li><strong>新 Hook 沒有範本</strong>：寫新 Hook 時不知道該參考哪個</li>
</ol>
<h2 id="統一化模式">統一化模式</h2>
<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">1. 建立統一介面    → 寫一個所有人都要用的模組
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 漸進式遷移      → 逐批將現有 Hook 切換到新介面
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 驗證           → 確認行為一致
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 處理例外       → 處理少數無法直接遷移的情況</span></span></code></pre></div><p>這個模式的關鍵在於<strong>不一次改完</strong>。每個階段只統一一個維度，確認穩定後再進入下一個。</p>
<h2 id="第一階段統一日誌w22">第一階段：統一日誌（W22）</h2>
<h3 id="設計統一介面">設計統一介面</h3>
<p>目標是用一個模組取代三套日誌實作。核心 API 只有兩個函式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># hook_utils.py — 統一日誌模組</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;建立並設定 Hook 日誌系統
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">    - 建立日誌目錄 .claude/hook-logs/</span><span class="si">{hook_name}</span><span class="s2">/
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    - 建立帶時間戳的日誌檔案
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    - 配置 FileHandler + StreamHandler
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</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="k">def</span> <span class="nf">run_hook_safely</span><span class="p">(</span><span class="n">main_func</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[],</span> <span class="nb">int</span><span class="p">],</span> <span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="s2">&#34;&#34;&#34;安全執行 Hook 函式，頂層例外處理
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">    - 呼叫 setup_hook_logging 取得 logger
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s2">    - 執行 main_func，捕獲所有 Exception
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">    - 異常時記錄完整 traceback，返回 1
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span></span></span></code></pre></div><p><code>setup_hook_logging</code> 封裝了所有日誌配置細節：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">sanitized_name</span> <span class="o">=</span> <span class="n">_sanitize_hook_name</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">root_dir</span> <span class="o">=</span> <span class="n">_find_project_root</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">log_base_dir</span> <span class="o">=</span> <span class="n">root_dir</span> <span class="o">/</span> <span class="s2">&#34;.claude&#34;</span> <span class="o">/</span> <span class="s2">&#34;hook-logs&#34;</span> <span class="o">/</span> <span class="n">sanitized_name</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="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">log_base_dir</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="n">_create_fallback_logger</span><span class="p">(</span><span class="n">hook_name</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">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">_clear_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">DEBUG</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">is_debug</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;HOOK_DEBUG&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">_setup_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">log_base_dir</span><span class="p">,</span> <span class="n">sanitized_name</span><span class="p">,</span> <span class="n">is_debug</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="k">return</span> <span class="n">logger</span></span></span></code></pre></div><p>幾個設計決策值得注意：</p>
<table>
  <thead>
      <tr>
          <th>決策</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_sanitize_hook_name</code></td>
          <td>Hook 名稱可能包含 <code>/</code> 等特殊字元，不能直接用作目錄名</td>
      </tr>
      <tr>
          <td><code>_clear_logger_handlers</code></td>
          <td>避免重複呼叫時 handler 累加</td>
      </tr>
      <tr>
          <td>Fallback logger</td>
          <td>目錄建立失敗時仍可輸出到 stdout，不會 crash</td>
      </tr>
      <tr>
          <td><code>HOOK_DEBUG</code> 環境變數</td>
          <td>開發時可開啟 DEBUG 級別的 stream 輸出</td>
      </tr>
  </tbody>
</table>
<h3 id="run_hook_safely一行搞定錯誤處理"><code>run_hook_safely</code>：一行搞定錯誤處理</h3>
<p>這是統一化的核心武器。原本每個 Hook 自己寫 try-except：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 重構前：每個 Hook 自己處理</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">try</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">main</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="c1"># 有的寫日誌，有的 print，有的什麼都不做</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">        <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span></span></span></code></pre></div><p>統一後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;acceptance-gate&#34;</span><span class="p">))</span></span></span></code></pre></div><p><code>run_hook_safely</code> 內部處理三個邊界：</p>
<ul>
<li><strong>返回值驗證</strong>：<code>main()</code> 可能回傳 <code>None</code> 或布林值，<code>run_hook_safely</code> 會將非整數返回值轉換為 <code>0</code>（成功）或 <code>1</code>（失敗），確保 <code>sys.exit</code> 收到合法的退出碼</li>
<li><strong>不攔截 <code>SystemExit</code></strong>：刻意的 <code>sys.exit()</code> 呼叫不該被吃掉</li>
<li><strong>不攔截 <code>KeyboardInterrupt</code></strong>：Ctrl+C 中斷不該被捕獲</li>
</ul>
<p>所有其他 <code>Exception</code> 子類別都被捕獲、記錄到日誌、返回錯誤碼 1。</p>
<h3 id="遷移策略">遷移策略</h3>
<p>不可能一次改完 44 個檔案。按風險分批：</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>範圍</th>
          <th>策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 批</td>
          <td>5 個低風險 Hook</td>
          <td>驗證新模組行為正確</td>
      </tr>
      <tr>
          <td>第 2 批</td>
          <td>15 個中等複雜度</td>
          <td>建立遷移信心</td>
      </tr>
      <tr>
          <td>第 3 批</td>
          <td>剩餘所有 Hook</td>
          <td>批量遷移</td>
      </tr>
  </tbody>
</table>
<p>每批遷移後執行全量測試，確認無迴歸。</p>
<h2 id="第二階段統一訊息w23">第二階段：統一訊息（W23）</h2>
<h3 id="問題硬編碼訊息散落各處">問題：硬編碼訊息散落各處</h3>
<p>日誌統一後，下一個問題浮現：每個 Hook 的使用者訊息各自定義。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># command-entrance-gate-hook.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;錯誤：未找到待處理的 Ticket</span><span class="se">\n</span><span class="s2">建議操作: 執行 /ticket create&#34;</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"># acceptance-gate-hook.py</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;[ERROR] 子任務未全部完成</span><span class="se">\n</span><span class="s2">Ticket: </span><span class="si">{}</span><span class="se">\n</span><span class="s2">請先完成所有子任務&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># main-thread-edit-restriction-hook.py</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;編輯操作受限&#34;</span><span class="p">)</span></span></span></code></pre></div><p>同樣的問題：改一個訊息格式要翻遍所有 Hook。訊息重複時會出現不一致。</p>
<h3 id="集中管理hook_messagespy">集中管理：hook_messages.py</h3>
<p>建立一個訊息常數模組，按職責分類：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># lib/hook_messages.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">class</span> <span class="nc">CoreMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="s2">&#34;&#34;&#34;所有 Hook 共用的通用訊息&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">HOOK_START</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">{hook_name}</span><span class="s2"> 啟動&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">INPUT_EMPTY</span> <span class="o">=</span> <span class="s2">&#34;輸入為空，預設允許&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">JSON_PARSE_ERROR</span> <span class="o">=</span> <span class="s2">&#34;JSON 解析錯誤，預設允許: </span><span class="si">{error}</span><span class="s2">&#34;</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="k">class</span> <span class="nc">GateMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="s2">&#34;&#34;&#34;5 個 Gate Hook 的阻擋/警告訊息&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">TICKET_NOT_FOUND_ERROR</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;錯誤：未找到待處理的 Ticket
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s2">    為什麼阻止執行：
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">      開發命令必須有對應的 Ticket，確保工作可追蹤和驗收。
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">    建議操作:
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s2">      1. 執行 /ticket create 建立新 Ticket
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s2">      2. 或執行 /ticket track claim </span><span class="si">{id}</span><span class="s2"> 認領現有 Ticket&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="k">class</span> <span class="nc">WorkflowMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="s2">&#34;&#34;&#34;工作流指導 Hook 的訊息&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">EXTERNAL_QUERY_DETECTED</span> <span class="o">=</span> <span class="s2">&#34;檢測到 </span><span class="si">{tool_name}</span><span class="s2"> 調用&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">class</span> <span class="nc">QualityMessages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="s2">&#34;&#34;&#34;品質檢查 Hook 的訊息&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="c1"># ...</span></span></span></code></pre></div><h3 id="分類原則">分類原則</h3>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>包含的 Hook</th>
          <th>訊息特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CoreMessages</td>
          <td>所有 Hook</td>
          <td>啟動、錯誤、預設行為</td>
      </tr>
      <tr>
          <td>GateMessages</td>
          <td>5 個 Gate Hook</td>
          <td>阻擋、警告、建議操作</td>
      </tr>
      <tr>
          <td>WorkflowMessages</td>
          <td>5 個工作流 Hook</td>
          <td>流程指導、步驟說明</td>
      </tr>
      <tr>
          <td>QualityMessages</td>
          <td>品質檢查 Hook</td>
          <td>掃描結果、改善建議</td>
      </tr>
      <tr>
          <td>ValidationMessages</td>
          <td>驗證 Hook</td>
          <td>格式檢查、合規結果</td>
      </tr>
  </tbody>
</table>
<p>使用方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="nb">print</span><span class="p">(</span><span class="s2">&#34;錯誤：未找到待處理的 Ticket</span><span class="se">\n</span><span class="s2">建議操作: ...&#34;</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="kn">from</span> <span class="nn">lib.hook_messages</span> <span class="kn">import</span> <span class="n">GateMessages</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">GateMessages</span><span class="o">.</span><span class="n">TICKET_NOT_FOUND_ERROR</span><span class="p">)</span></span></span></code></pre></div><p>參數化的訊息用 <code>format()</code> ：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="nb">print</span><span class="p">(</span><span class="n">GateMessages</span><span class="o">.</span><span class="n">TICKET_NOT_CLAIMED_ERROR</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">ticket_id</span><span class="o">=</span><span class="s2">&#34;0.31.0-W2-001&#34;</span><span class="p">))</span></span></span></code></pre></div><h3 id="效果">效果</h3>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>重構前</th>
          <th>重構後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊息定義位置</td>
          <td>散落 19 個檔案（57+ 個硬編碼字串）</td>
          <td>集中 1 個模組（45 個常數）</td>
      </tr>
      <tr>
          <td>修改訊息格式</td>
          <td>逐檔搜尋修改</td>
          <td>改一處生效</td>
      </tr>
      <tr>
          <td>訊息一致性</td>
          <td>同概念 2-3 種措辭</td>
          <td>每個概念一個定義</td>
      </tr>
      <tr>
          <td>新 Hook 訊息</td>
          <td>自行發明</td>
          <td>複用現有類別</td>
      </tr>
  </tbody>
</table>
<h2 id="第三階段統一風格w24">第三階段：統一風格（W24）</h2>
<h3 id="問題logger-初始化位置不一致">問題：logger 初始化位置不一致</h3>
<p>日誌模組和訊息常數統一後，16 個 Hook 的 logger 初始化位置仍然不一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 風格 A：模組級初始化（13 個 Hook）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>  <span class="c1"># 最外層</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="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>           <span class="c1"># 引用全域 logger</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">helper</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="mi">0</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"># 風格 B：main() 內初始化（3 個 Hook）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">def</span> <span class="nf">helper</span><span class="p">(</span><span class="n">logger</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>           <span class="c1"># 接收 logger 參數</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">helper</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span></span></span></code></pre></div><p>目標是統一為風格 B。理由是：模組級初始化的 <code>logger</code> 會在 <code>import</code> 時立即建立日誌目錄和檔案，即使這個模組只是被其他工具引用而不是作為 Hook 執行。將 <code>logger</code> 移入 <code>main()</code> 可以確保只有<strong>真正執行</strong>時才初始化日誌系統。</p>
<h3 id="事故7-個-hook-靜默失敗">事故：7 個 Hook 靜默失敗</h3>
<p>統一風格的過程中發生了一個典型的作用域迴歸 bug。把 <code>logger</code> 從模組級移到 <code>main()</code> 內部後，引用全域 <code>logger</code> 的 helper 函式觸發了 <code>NameError</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 修改後（有 bug）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">check_acceptance_criteria</span><span class="p">(</span><span class="n">ticket_path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Checking </span><span class="si">{</span><span class="n">ticket_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># NameError!</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">check_acceptance_criteria</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span></span></code></pre></div><p>更危險的是，<code>run_hook_safely</code> 的頂層 try-except 捕獲了 <code>NameError</code>（它是 <code>Exception</code> 的子類別），寫入日誌檔案，返回錯誤碼。用戶完全看不到任何異常。<strong>7 個 Hook 在至少 2 個 session 中靜默失敗</strong>。</p>
<blockquote>
<p>這個事故的完整分析見下一章：<a href="/blog/python/07-refactoring/refactoring-pitfalls/" data-link-title="重構陷阱與防護" data-link-desc="三個真實重構事故的共通模式：部分更新問題與系統性防護方法">重構陷阱與防護</a></p></blockquote>
<h3 id="修正逐一分析影響範圍">修正：逐一分析影響範圍</h3>
<p>正確的做法是在修改作用域<strong>之前</strong>，用 AST 分析或 grep 找出所有引用 <code>logger</code> 的非 main 函式，然後為每個函式加入 <code>logger</code> 參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">check_acceptance_criteria</span><span class="p">(</span><span class="n">ticket_path</span><span class="p">,</span> <span class="n">logger</span><span class="p">):</span>  <span class="c1"># 加入參數</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Checking </span><span class="si">{</span><span class="n">ticket_path</span><span class="si">}</span><span class="s2">&#34;</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</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">check_acceptance_criteria</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span>  <span class="c1"># 傳遞 logger</span></span></span></code></pre></div><p>修正規模：7 個 Hook、41 個函式、+143/-81 行。</p>
<h3 id="事故後的改善">事故後的改善</h3>
<p>這次事故直接促成了 <code>_log_exception</code> 的 stderr 輸出改善（W25-005）：在寫入日誌檔案之外，額外輸出一行到 <code>sys.stderr</code>，確保即使 <code>run_hook_safely</code> 捕獲了異常，用戶也能在終端看到 <code>[Hook Error]</code> 提示。</p>
<h2 id="重構後的標準樣板">重構後的標準樣板</h2>
<p>三階段統一完成後，每個 Hook 的結構變得極為一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="s2">&#34;&#34;&#34;Hook 說明文件&#34;&#34;&#34;</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="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</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"># Hook 不是安裝的套件，需要手動把 hooks/ 目錄加入 Python 搜尋路徑</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 這樣才能 import 同目錄下的 hook_utils 和 lib/ 子模組</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">_hooks_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">if</span> <span class="n">_hooks_dir</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">path</span> <span class="k">if</span> <span class="n">Path</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="o">==</span> <span class="n">_hooks_dir</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">_hooks_dir</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="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">run_hook_safely</span><span class="p">,</span> <span class="n">setup_hook_logging</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.hook_messages</span> <span class="kn">import</span> <span class="n">GateMessages</span><span class="p">,</span> <span class="n">CoreMessages</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"># 常數定義</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">EXIT_SUCCESS</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">EXIT_BLOCK</span> <span class="o">=</span> <span class="mi">2</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="c1"># ---- 業務邏輯 ----</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">def</span> <span class="nf">check_something</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">logger</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="s2">&#34;&#34;&#34;每個 helper 都接收 logger 參數&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="n">CoreMessages</span><span class="o">.</span><span class="n">HOOK_START</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">hook_name</span><span class="o">=</span><span class="s2">&#34;my-hook&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="c1"># 讀取輸入、執行檢查、輸出結果</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="k">return</span> <span class="n">EXIT_SUCCESS</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="c1"># ---- 入口 ----</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;my-hook&#34;</span><span class="p">))</span></span></span></code></pre></div><p>對比重構前後：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>重構前</th>
          <th>重構後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日誌初始化</td>
          <td>3 種模組 + 散裝 logging</td>
          <td><code>setup_hook_logging</code> 一行</td>
      </tr>
      <tr>
          <td>錯誤處理</td>
          <td>自寫 try-except 或不處理</td>
          <td><code>run_hook_safely</code> 一行</td>
      </tr>
      <tr>
          <td>使用者訊息</td>
          <td>硬編碼在各檔案</td>
          <td>引用 <code>hook_messages</code> 常數</td>
      </tr>
      <tr>
          <td>logger 傳遞</td>
          <td>全域變數</td>
          <td>參數傳遞</td>
      </tr>
      <tr>
          <td>入口點</td>
          <td>5-15 行樣板</td>
          <td>1 行</td>
      </tr>
      <tr>
          <td>新 Hook 開發</td>
          <td>參考哪個都不確定</td>
          <td>複製標準樣板</td>
      </tr>
  </tbody>
</table>
<h2 id="統一化的通用教訓">統一化的通用教訓</h2>
<h3 id="教訓-1先建介面再遷移">教訓 1：先建介面，再遷移</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">錯誤路徑：邊改邊用 → 半成品狀態 → 新舊混合更亂
</span></span><span class="line"><span class="ln">2</span><span class="cl">正確路徑：新模組獨立完成 → 逐批遷移 → 舊模組標記棄用</span></span></code></pre></div><h3 id="教訓-2分批遷移每批驗證">教訓 2：分批遷移，每批驗證</h3>
<p>44 個 Hook 一次改完的風險太高。分批的目的不只是降低風險，更是建立信心。第一批 5 個成功後，第二批 15 個就能更快。</p>
<h3 id="教訓-3統一風格是最危險的一步">教訓 3：統一風格是最危險的一步</h3>
<p>統一「介面」（W22 日誌、W23 訊息）相對安全，因為是新增模組再切換引用。統一「風格」（W24 作用域）涉及修改現有程式碼的結構，牽一髮動全身。</p>
<table>
  <thead>
      <tr>
          <th>風險等級</th>
          <th>操作類型</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>低</td>
          <td>新增模組 + 替換 import</td>
          <td>W22 新增 hook_utils.py</td>
      </tr>
      <tr>
          <td>中</td>
          <td>替換訊息字串</td>
          <td>W23 硬編碼 → 常數引用</td>
      </tr>
      <tr>
          <td>高</td>
          <td>修改變數作用域</td>
          <td>W24 全域 logger → 參數傳遞</td>
      </tr>
  </tbody>
</table>
<h3 id="教訓-4安全網要先到位">教訓 4：安全網要先到位</h3>
<p>W24 的事故之所以嚴重，是因為安全網（stderr 輸出）在事故<strong>之後</strong>才補上。正確的順序應該是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 先確認安全網（stderr 輸出、測試覆蓋）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 再執行風險操作（作用域修改）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 最後清理（移除棄用程式碼）</span></span></code></pre></div><h2 id="量化成果">量化成果</h2>
<p>三階段統一化的最終成果：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>統一前</th>
          <th>統一後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日誌模組</td>
          <td>3 個</td>
          <td>1 個 (hook_utils.py)</td>
      </tr>
      <tr>
          <td>錯誤處理模式</td>
          <td>3 種</td>
          <td>1 種 (run_hook_safely)</td>
      </tr>
      <tr>
          <td>訊息定義位置</td>
          <td>19 個檔案（57+ 個字串）</td>
          <td>1 個 (hook_messages.py)</td>
      </tr>
      <tr>
          <td>logger 初始化風格</td>
          <td>2 種</td>
          <td>1 種 (main 內 + 參數傳遞)</td>
      </tr>
      <tr>
          <td>新 Hook 開發時間</td>
          <td>~30 分鐘</td>
          <td>~10 分鐘</td>
      </tr>
      <tr>
          <td>Hook 入口樣板</td>
          <td>5-15 行</td>
          <td>1 行</td>
      </tr>
  </tbody>
</table>
<h2 id="思考題">思考題</h2>
<ol>
<li>如果你的系統有 100 個腳本而不是 44 個，統一化策略會有什麼不同？</li>
<li><code>run_hook_safely</code> 選擇返回錯誤碼而不是重新拋出異常，這個設計在什麼情境下會是錯誤的？</li>
<li>訊息常數用 class 分類（<code>GateMessages</code>、<code>WorkflowMessages</code>）而不是單一字典，有什麼優缺點？</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>為一組 3 個以上的腳本設計統一日誌模組，包含 <code>setup_logging</code> 和 <code>run_safely</code> 兩個函式</li>
<li>掃描一個多檔案專案，找出所有硬編碼的使用者訊息字串，規劃集中管理方案</li>
<li>嘗試用本章的分批遷移策略，將練習 2 的訊息逐批遷移到常數模組</li>
</ol>
<h2 id="小結">小結</h2>
<ul>
<li>大規模統一化的核心模式：<strong>建立統一介面 -&gt; 分批遷移 -&gt; 驗證 -&gt; 處理例外</strong></li>
<li>統一「介面」（新增模組 + 替換引用）風險低，統一「風格」（修改現有結構）風險高</li>
<li><code>run_hook_safely</code> 一行取代 44 套自寫的錯誤處理，確保行為一致</li>
<li>訊息集中化用 Messages 類別按使用者角色分組，消除散落的硬編碼字串</li>
<li>分批遷移不只降低風險，更是建立信心的過程</li>
<li>安全網（stderr 輸出、測試覆蓋）必須在風險操作<strong>之前</strong>到位</li>
</ul>
<hr>
<p><em>上一章：<a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a></em>
<em>下一章：<a href="/blog/python/07-refactoring/refactoring-pitfalls/" data-link-title="重構陷阱與防護" data-link-desc="三個真實重構事故的共通模式：部分更新問題與系統性防護方法">重構陷阱與防護</a></em>
<em>相關：<a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a></em></p>
]]></content:encoded></item><item><title>作用域迴歸案例研究</title><link>https://tarrragon.github.io/blog/python/07-refactoring/scope-regression/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/scope-regression/</guid><description>&lt;p>本章記錄 W24 開發週期中發生的一個真實 bug：在統一 16 個 Hook 的 logger 初始化風格時，7 個 Hook 因為&lt;strong>變數作用域變更&lt;/strong>而靜默失敗，影響 41 個函式。&lt;/p>
&lt;p>這個案例的價值在於：bug 本身很簡單（&lt;code>NameError&lt;/code>），但它暴露了重構時一個容易被忽略的系統性風險。&lt;/p>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>W24 的任務是統一所有 Hook 的 logger 初始化風格。原本各 Hook 的 logger 初始化位置不一致：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 風格 A：模組級初始化（13 個 Hook 使用）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;my-hook&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 在最外層&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="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;working...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK：logger 是全域變數&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="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">helper&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;done&amp;#34;&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="k">return&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># 風格 B：main() 內初始化（已有部分 Hook 使用）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&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="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;working...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK：logger 是參數&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="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;my-hook&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 在 main() 內&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="n">helper&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;done&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="mi">0&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>統一目標：全部改為&lt;strong>風格 B&lt;/strong>（&lt;code>main()&lt;/code> 內初始化），理由是：&lt;/p>
&lt;ul>
&lt;li>logger 不該在模組被 import 時就建立&lt;/li>
&lt;li>&lt;code>main()&lt;/code> 內初始化更明確，生命週期更可控&lt;/li>
&lt;/ul>
&lt;h2 id="出了什麼問題">出了什麼問題&lt;/h2>
&lt;p>修改時只做了一件事：把 &lt;code>logger = setup_hook_logging(...)&lt;/code> 從模組級移到 &lt;code>main()&lt;/code> 內部。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改前&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;acceptance-gate-hook&amp;#34;&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="k">def&lt;/span> &lt;span class="nf">check_acceptance_criteria&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ticket_path&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Checking &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ticket_path&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK&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>&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="k">def&lt;/span> &lt;span class="nf">validate_ticket_format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Validating format&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&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="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">check_acceptance_criteria&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">path&lt;/span>&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="c1"># ...&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"># 修改後（有 bug）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">check_acceptance_criteria&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ticket_path&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Checking &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">ticket_path&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># NameError!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">validate_ticket_format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">content&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Validating format&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># NameError!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;acceptance-gate-hook&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># 區域變數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">check_acceptance_criteria&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="c1"># ...&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>logger&lt;/code> 從全域變數變成了 &lt;code>main()&lt;/code> 的區域變數。但 &lt;code>check_acceptance_criteria&lt;/code> 和 &lt;code>validate_ticket_format&lt;/code> 仍然以全域方式引用 &lt;code>logger&lt;/code>——它們不知道 &lt;code>logger&lt;/code> 已經不在全域作用域了。&lt;/p>
&lt;h2 id="python-作用域規則回顧">Python 作用域規則回顧&lt;/h2>
&lt;p>Python 的變數查找遵循 &lt;strong>LEGB 規則&lt;/strong>：&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">L - Local : 函式內部
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">E - Enclosing : 外層函式（閉包）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">G - Global : 模組級
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">B - Built-in : Python 內建&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改前：logger 在 G（Global）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># Global scope&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="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># L 找不到 → E 找不到 → G 找到了&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"># 修改後：logger 在 main 的 L（Local）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># L 找不到 → E 找不到 → G 找不到 → NameError!&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&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="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hook&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># main 的 Local scope&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="n">helper&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># helper 無法存取 main 的 Local&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>main()&lt;/code> 的區域變數對 &lt;code>helper()&lt;/code> 來說是&lt;strong>不可見的&lt;/strong>。&lt;code>helper()&lt;/code> 不是定義在 &lt;code>main()&lt;/code> 內部（不是閉包），所以 Enclosing scope 也找不到。&lt;/p></description><content:encoded><![CDATA[<p>本章記錄 W24 開發週期中發生的一個真實 bug：在統一 16 個 Hook 的 logger 初始化風格時，7 個 Hook 因為<strong>變數作用域變更</strong>而靜默失敗，影響 41 個函式。</p>
<p>這個案例的價值在於：bug 本身很簡單（<code>NameError</code>），但它暴露了重構時一個容易被忽略的系統性風險。</p>
<h2 id="背景">背景</h2>
<p>W24 的任務是統一所有 Hook 的 logger 初始化風格。原本各 Hook 的 logger 初始化位置不一致：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 風格 A：模組級初始化（13 個 Hook 使用）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>  <span class="c1"># 在最外層</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="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>  <span class="c1"># OK：logger 是全域變數</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">helper</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;done&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 風格 B：main() 內初始化（已有部分 Hook 使用）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">def</span> <span class="nf">helper</span><span class="p">(</span><span class="n">logger</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>  <span class="c1"># OK：logger 是參數</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>  <span class="c1"># 在 main() 內</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">helper</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;done&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span></span></span></code></pre></div><p>統一目標：全部改為<strong>風格 B</strong>（<code>main()</code> 內初始化），理由是：</p>
<ul>
<li>logger 不該在模組被 import 時就建立</li>
<li><code>main()</code> 內初始化更明確，生命週期更可控</li>
</ul>
<h2 id="出了什麼問題">出了什麼問題</h2>
<p>修改時只做了一件事：把 <code>logger = setup_hook_logging(...)</code> 從模組級移到 <code>main()</code> 內部。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</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="k">def</span> <span class="nf">check_acceptance_criteria</span><span class="p">(</span><span class="n">ticket_path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Checking </span><span class="si">{</span><span class="n">ticket_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># OK</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></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">validate_ticket_format</span><span class="p">(</span><span class="n">content</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;Validating format&#34;</span><span class="p">)</span>  <span class="c1"># OK</span>
</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></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">check_acceptance_criteria</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="c1"># ...</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"># 修改後（有 bug）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">def</span> <span class="nf">check_acceptance_criteria</span><span class="p">(</span><span class="n">ticket_path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Checking </span><span class="si">{</span><span class="n">ticket_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>  <span class="c1"># NameError!</span>
</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></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">def</span> <span class="nf">validate_ticket_format</span><span class="p">(</span><span class="n">content</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;Validating format&#34;</span><span class="p">)</span>  <span class="c1"># NameError!</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="c1"># ...</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</span><span class="p">)</span>  <span class="c1"># 區域變數</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">check_acceptance_criteria</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="c1"># ...</span></span></span></code></pre></div><p><code>logger</code> 從全域變數變成了 <code>main()</code> 的區域變數。但 <code>check_acceptance_criteria</code> 和 <code>validate_ticket_format</code> 仍然以全域方式引用 <code>logger</code>——它們不知道 <code>logger</code> 已經不在全域作用域了。</p>
<h2 id="python-作用域規則回顧">Python 作用域規則回顧</h2>
<p>Python 的變數查找遵循 <strong>LEGB 規則</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">L - Local      : 函式內部
</span></span><span class="line"><span class="ln">2</span><span class="cl">E - Enclosing  : 外層函式（閉包）
</span></span><span class="line"><span class="ln">3</span><span class="cl">G - Global     : 模組級
</span></span><span class="line"><span class="ln">4</span><span class="cl">B - Built-in   : Python 內建</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 修改前：logger 在 G（Global）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook&#34;</span><span class="p">)</span>  <span class="c1"># Global scope</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="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;...&#34;</span><span class="p">)</span>  <span class="c1"># L 找不到 → E 找不到 → G 找到了</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"># 修改後：logger 在 main 的 L（Local）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;...&#34;</span><span class="p">)</span>  <span class="c1"># L 找不到 → E 找不到 → G 找不到 → NameError!</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;hook&#34;</span><span class="p">)</span>  <span class="c1"># main 的 Local scope</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">helper</span><span class="p">()</span>  <span class="c1"># helper 無法存取 main 的 Local</span></span></span></code></pre></div><p><code>main()</code> 的區域變數對 <code>helper()</code> 來說是<strong>不可見的</strong>。<code>helper()</code> 不是定義在 <code>main()</code> 內部（不是閉包），所以 Enclosing scope 也找不到。</p>
<h2 id="為什麼沒被立刻發現">為什麼沒被立刻發現</h2>
<p>這個 bug 最危險的地方是<strong>靜默失敗</strong>。原因是 <code>run_hook_safely</code> 的頂層例外處理：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">run_hook_safely</span><span class="p">(</span><span class="n">main_func</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">exit_code</span> <span class="o">=</span> <span class="n">main_func</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">KeyboardInterrupt</span><span class="p">,</span> <span class="ne">SystemExit</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">raise</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">tb_str</span> <span class="o">=</span> <span class="n">traceback</span><span class="o">.</span><span class="n">format_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">_log_exception</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">,</span> <span class="n">tb_str</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="n">EXIT_ERROR</span>  <span class="c1"># 返回錯誤碼，但不會 crash</span></span></span></code></pre></div><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">1. main() 被 run_hook_safely 呼叫
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. main() 內呼叫 check_acceptance_criteria()
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. check_acceptance_criteria() 引用 logger → NameError
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. NameError 是 Exception 的子類別
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. run_hook_safely 捕獲，寫入日誌檔案
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 返回 EXIT_ERROR（整數 1）
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. Hook 系統收到非零退出碼 → 顯示 &#34;hook success&#34;（suppressOutput）
</span></span><span class="line"><span class="ln">8</span><span class="cl">8. 用戶看不到任何異常</span></span></code></pre></div><p>7 個 Hook 就這樣在至少 2 個 session 中靜默失敗。直到有人手動觸發了一個受影響的 Hook 並檢查日誌，才發現問題。</p>
<blockquote>
<p>這也是為什麼 W25-005 後來在 <code>_log_exception</code> 加入了 stderr 輸出。詳見 <a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a>。</p></blockquote>
<h2 id="正確的修正方式">正確的修正方式</h2>
<h3 id="step-1影響範圍分析">Step 1：影響範圍分析</h3>
<p>修改變數作用域<strong>之前</strong>，先列出所有引用該變數的函式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 用 AST 分析找出所有引用 logger 的非 main 函式</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">import ast, sys
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">tree = ast.parse(open(sys.argv[1]).read())
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">for node in ast.walk(tree):
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    if isinstance(node, ast.FunctionDef) and node.name != &#39;main&#39;:
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">        for child in ast.walk(node):
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">            if isinstance(child, ast.Name) and child.id == &#39;logger&#39;:
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">                print(f&#39;  {node.name}() references logger&#39;)
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">                break
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">&#34;</span> acceptance-gate-hook.py</span></span></code></pre></div><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">  check_acceptance_criteria() references logger
</span></span><span class="line"><span class="ln">2</span><span class="cl">  validate_ticket_format() references logger
</span></span><span class="line"><span class="ln">3</span><span class="cl">  check_worklog_sections() references logger
</span></span><span class="line"><span class="ln">4</span><span class="cl">  ... (共 11 個函式)</span></span></code></pre></div><h3 id="step-2修改函式簽名">Step 2：修改函式簽名</h3>
<p>每個引用 <code>logger</code> 的函式都必須接收 <code>logger</code> 作為參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">check_acceptance_criteria</span><span class="p">(</span><span class="n">ticket_path</span><span class="p">,</span> <span class="n">logger</span><span class="p">):</span>  <span class="c1"># 加入 logger 參數</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Checking </span><span class="si">{</span><span class="n">ticket_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</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></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">def</span> <span class="nf">validate_ticket_format</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">logger</span><span class="p">):</span>  <span class="c1"># 加入 logger 參數</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;Validating format&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="c1"># ...</span></span></span></code></pre></div><h3 id="step-3更新所有呼叫端">Step 3：更新所有呼叫端</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">check_acceptance_criteria</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span>  <span class="c1"># 傳遞 logger</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">validate_ticket_format</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">logger</span><span class="p">)</span>            <span class="c1"># 傳遞 logger</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span></span></span></code></pre></div><h3 id="step-4驗證">Step 4：驗證</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># AST 驗證：確認沒有函式在引用全域 logger</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">python3 -c <span class="s2">&#34;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">import ast, sys
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">tree = ast.parse(open(sys.argv[1]).read())
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">issues = []
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">for node in ast.walk(tree):
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    if isinstance(node, ast.FunctionDef) and node.name != &#39;main&#39;:
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">        params = {arg.arg for arg in node.args.args}
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="s2">        if &#39;logger&#39; not in params:
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="s2">            for child in ast.walk(node):
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="s2">                if isinstance(child, ast.Name) and child.id == &#39;logger&#39;:
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="s2">                    issues.append(node.name)
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="s2">                    break
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="s2">if issues:
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="s2">    print(f&#39;FAIL: {issues} still reference global logger&#39;)
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="s2">else:
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="s2">    print(&#39;PASS: all functions receive logger as parameter&#39;)
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="s2">&#34;</span> acceptance-gate-hook.py</span></span></code></pre></div><h2 id="修正規模">修正規模</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>受影響 Hook</td>
          <td>7 個</td>
      </tr>
      <tr>
          <td>受影響函式</td>
          <td>41 個</td>
      </tr>
      <tr>
          <td>修正行數</td>
          <td>+143 / -81</td>
      </tr>
      <tr>
          <td>靜默失敗持續時間</td>
          <td>至少 2 個 session</td>
      </tr>
  </tbody>
</table>
<h2 id="為什麼-py_compile-抓不到這個-bug">為什麼 py_compile 抓不到這個 bug</h2>
<p>你可能會想：修改後跑一下語法檢查不就好了？</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">python3 -m py_compile acceptance-gate-hook.py
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 通過！沒有任何錯誤</span></span></span></code></pre></div><p><code>py_compile</code> 只檢查<strong>語法</strong>（syntax），不檢查<strong>作用域</strong>（scope）。<code>logger.info(&quot;...&quot;)</code> 在語法上完全正確——它是一個合法的「存取名稱 logger 的 info 屬性並呼叫」。只有在<strong>執行時</strong>，Python 才會查找 <code>logger</code> 這個名稱，發現找不到，拋出 <code>NameError</code>。</p>
<h3 id="驗證工具的能力比較">驗證工具的能力比較</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>能否偵測此 bug</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>py_compile</code></td>
          <td>否</td>
          <td>只檢查語法</td>
      </tr>
      <tr>
          <td><code>mypy</code></td>
          <td>可能</td>
          <td>型別檢查會分析名稱可見性</td>
      </tr>
      <tr>
          <td>AST 分析</td>
          <td>是</td>
          <td>可以追蹤名稱引用和定義</td>
      </tr>
      <tr>
          <td>實際執行</td>
          <td>是</td>
          <td>直接觸發 <code>NameError</code></td>
      </tr>
      <tr>
          <td><code>pylint</code></td>
          <td>是</td>
          <td>會警告 <code>undefined-variable</code></td>
      </tr>
  </tbody>
</table>
<h2 id="教訓作用域變更的強制檢查清單">教訓：作用域變更的強制檢查清單</h2>
<p>任何涉及<strong>變數作用域變更</strong>的重構（全域 → 區域、模組級 → 函式內、類別屬性 → 方法參數），都必須執行：</p>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>列出所有引用該變數的函式</td>
          <td><code>grep</code> 或 AST 分析</td>
      </tr>
      <tr>
          <td>2</td>
          <td>每個函式確認：透過參數接收還是依賴全域？</td>
          <td>逐一檢查函式簽名</td>
      </tr>
      <tr>
          <td>3</td>
          <td>依賴全域的函式必須新增參數</td>
          <td>修改函式簽名</td>
      </tr>
      <tr>
          <td>4</td>
          <td>所有呼叫端必須傳遞新參數</td>
          <td>修改所有 call site</td>
      </tr>
      <tr>
          <td>5</td>
          <td>驗證</td>
          <td>AST 分析或實際執行（不要只用 py_compile）</td>
      </tr>
  </tbody>
</table>
<h2 id="更廣泛的啟示">更廣泛的啟示</h2>
<p>這個案例不只適用於 <code>logger</code>。任何「移動變數定義位置」的重構都有同樣的風險：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="n">db</span> <span class="o">=</span> <span class="n">connect_database</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="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;SELECT * FROM users WHERE id = </span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">&#34;</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"># 修改後（有 bug）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>  <span class="c1"># NameError: db 不再是全域</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">db</span> <span class="o">=</span> <span class="n">connect_database</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">get_user</span><span class="p">(</span><span class="mi">123</span><span class="p">)</span></span></span></code></pre></div><p>同樣的模式，同樣的陷阱。解決方式也一樣：分析引用 → 修改簽名 → 傳遞參數 → 驗證。</p>
<h2 id="思考題">思考題</h2>
<ol>
<li>如果使用 <code>global logger</code> 宣告，能否解決這個問題？為什麼不推薦這種做法？</li>
<li>閉包（closure）能否解決這個問題？把 <code>helper</code> 定義在 <code>main()</code> 內部會怎樣？</li>
<li>這個 bug 在什麼條件下才會被發現？（提示：考慮測試覆蓋率和 Hook 觸發時機）</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>找一段使用全域變數的程式碼，嘗試將變數移入函式內部，並用 AST 分析驗證所有引用</li>
<li>寫一個腳本，掃描指定的 Python 檔案，找出所有「函式內引用但未定義、也不在參數中」的名稱</li>
<li>設計一個 pre-commit hook，在 <code>git diff</code> 中偵測「變數定義位置改變」的情況</li>
</ol>
<hr>
<p><em>上一章：<a href="/blog/python/07-refactoring/case-study/" data-link-title="完整案例回顧" data-link-desc="從超過 30 個 Hook 各自為政到系統化品質工程，三個階段的完整重構復盤">重構案例研究</a></em>
<em>相關：<a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a> — 本案例中 bug 被靜默吞掉的機制分析</em></p>
]]></content:encoded></item><item><title>重構陷阱與防護</title><link>https://tarrragon.github.io/blog/python/07-refactoring/refactoring-pitfalls/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/refactoring-pitfalls/</guid><description>&lt;p>「只是把變數移個位置」「只是搬個檔案」「只是加個參數」——這些聽起來無害的操作，在我們的專案中分別造成了 7 個 Hook 靜默失敗、5 個 Hook 啟動崩潰、以及使用者看到莫名其妙的 &amp;ldquo;hook error&amp;rdquo; 訊息。&lt;/p>
&lt;p>本章整合三個真實事故（IMP-003、IMP-005、IMP-006），分析它們的共通模式，並建立一套防護方法。如果你只帶走一句話，請記住：&lt;strong>修改了定義，就必須更新所有引用&lt;/strong>。&lt;/p>
&lt;h2 id="三個陷阱的概覽">三個陷阱的概覽&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>陷阱&lt;/th>
 &lt;th>重構類型&lt;/th>
 &lt;th>遺漏&lt;/th>
 &lt;th>靜默時間&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>作用域迴歸 (IMP-003)&lt;/td>
 &lt;td>變數從全域移入函式&lt;/td>
 &lt;td>引用該變數的函式未更新&lt;/td>
 &lt;td>2+ sessions&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Import 未同步 (IMP-005)&lt;/td>
 &lt;td>模組搬遷至子目錄&lt;/td>
 &lt;td>引用該模組的檔案未更新&lt;/td>
 &lt;td>直到下次啟動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>靜默故障 (IMP-006)&lt;/td>
 &lt;td>函式簽名變更&lt;/td>
 &lt;td>部分 call site 未更新&lt;/td>
 &lt;td>直到該路徑被執行&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者看似不同，但根本原因完全一致：&lt;strong>修改了定義，但沒有更新所有引用&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="陷阱一作用域迴歸">陷阱一：作用域迴歸&lt;/h2>
&lt;blockquote>
&lt;p>本節是概要。IMP-003 的完整分析（含 LEGB 規則詳解、AST 修正腳本）請見&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h3 id="事件摘要">事件摘要&lt;/h3>
&lt;p>W24 的任務是統一 16 個 Hook 的 logger 初始化風格：從模組級初始化（全域變數）改為 &lt;code>main()&lt;/code> 內初始化（區域變數）。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改前：logger 是全域變數，所有函式可存取&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;my-hook&amp;#34;&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="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;working...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># OK：LEGB 在 Global 層找到&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"># 修改後：logger 變成 main() 的區域變數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">helper&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;working...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># NameError! helper 看不到 main 的區域變數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&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="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;my-hook&amp;#34;&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="n">helper&lt;/span>&lt;span class="p">()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="為什麼危險">為什麼危險&lt;/h3>
&lt;p>兩個因素疊加讓這個 bug 特別難發現：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>py_compile&lt;/code> 抓不到&lt;/strong>：&lt;code>logger.info(...)&lt;/code> 語法完全合法，名稱解析要到執行時才發生&lt;/li>
&lt;li>&lt;strong>頂層例外處理吞掉了 &lt;code>NameError&lt;/code>&lt;/strong>：&lt;code>run_hook_safely()&lt;/code> 捕捉所有 &lt;code>Exception&lt;/code>，Hook 靜默失敗而非 crash（詳見 &lt;a href="https://tarrragon.github.io/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制&lt;/a>）&lt;/li>
&lt;/ol>
&lt;p>結果：7 個 Hook 在至少 2 個 session 中靜默失敗，41 個函式需要修正，+143/-81 行修改——全部源自一個「只是移動定義位置」的操作。&lt;/p>
&lt;h3 id="正確做法">正確做法&lt;/h3>
&lt;p>修改前用 grep 或 AST 列出所有引用，逐一加入 &lt;code>logger&lt;/code> 參數，再用 AST 驗證無遺漏。完整的四步修正流程見&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究&lt;/a>。&lt;/p>
&lt;hr>
&lt;h2 id="陷阱二import-未同步">陷阱二：Import 未同步&lt;/h2>
&lt;h3 id="背景">背景&lt;/h3>
&lt;p>W22 重構將 &lt;code>common_functions.py&lt;/code> 從 &lt;code>.claude/hooks/&lt;/code> 遷移至 &lt;code>.claude/hooks/lib/&lt;/code>。但只更新了部分 Hook 的 import 路徑。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 遷移前（模組在同目錄）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">insert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__file__&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parent&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">common_functions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">hook_output&lt;/span> &lt;span class="c1"># OK&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"># 遷移後（模組移到 lib/，但 import 未更新）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n">sys&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">insert&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Path&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__file__&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parent&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="kn">from&lt;/span> &lt;span class="nn">common_functions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">hook_output&lt;/span> &lt;span class="c1"># ModuleNotFoundError!&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="c1"># 正確的遷移後寫法&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">lib.common_functions&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">hook_output&lt;/span> &lt;span class="c1"># OK&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-why-分析">5 Why 分析&lt;/h3>
&lt;ol>
&lt;li>Hook 啟動時拋出 &lt;code>ModuleNotFoundError&lt;/code>&lt;/li>
&lt;li>&lt;code>from common_functions import ...&lt;/code> 找不到模組&lt;/li>
&lt;li>&lt;code>common_functions.py&lt;/code> 已遷移至 &lt;code>lib/&lt;/code> 子目錄&lt;/li>
&lt;li>遷移時只更新了&lt;strong>部分&lt;/strong> Hook 的 import 路徑&lt;/li>
&lt;li>&lt;strong>根本原因&lt;/strong>：模組遷移後缺乏「全量引用更新」步驟&lt;/li>
&lt;/ol>
&lt;p>5 個 Hook 受影響，涵蓋 SessionStart、PostToolUse、UserPromptSubmit 三種事件類型。&lt;/p></description><content:encoded><![CDATA[<p>「只是把變數移個位置」「只是搬個檔案」「只是加個參數」——這些聽起來無害的操作，在我們的專案中分別造成了 7 個 Hook 靜默失敗、5 個 Hook 啟動崩潰、以及使用者看到莫名其妙的 &ldquo;hook error&rdquo; 訊息。</p>
<p>本章整合三個真實事故（IMP-003、IMP-005、IMP-006），分析它們的共通模式，並建立一套防護方法。如果你只帶走一句話，請記住：<strong>修改了定義，就必須更新所有引用</strong>。</p>
<h2 id="三個陷阱的概覽">三個陷阱的概覽</h2>
<table>
  <thead>
      <tr>
          <th>陷阱</th>
          <th>重構類型</th>
          <th>遺漏</th>
          <th>靜默時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>作用域迴歸 (IMP-003)</td>
          <td>變數從全域移入函式</td>
          <td>引用該變數的函式未更新</td>
          <td>2+ sessions</td>
      </tr>
      <tr>
          <td>Import 未同步 (IMP-005)</td>
          <td>模組搬遷至子目錄</td>
          <td>引用該模組的檔案未更新</td>
          <td>直到下次啟動</td>
      </tr>
      <tr>
          <td>靜默故障 (IMP-006)</td>
          <td>函式簽名變更</td>
          <td>部分 call site 未更新</td>
          <td>直到該路徑被執行</td>
      </tr>
  </tbody>
</table>
<p>三者看似不同，但根本原因完全一致：<strong>修改了定義，但沒有更新所有引用</strong>。</p>
<hr>
<h2 id="陷阱一作用域迴歸">陷阱一：作用域迴歸</h2>
<blockquote>
<p>本節是概要。IMP-003 的完整分析（含 LEGB 規則詳解、AST 修正腳本）請見<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a>。</p></blockquote>
<h3 id="事件摘要">事件摘要</h3>
<p>W24 的任務是統一 16 個 Hook 的 logger 初始化風格：從模組級初始化（全域變數）改為 <code>main()</code> 內初始化（區域變數）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 修改前：logger 是全域變數，所有函式可存取</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</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="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>  <span class="c1"># OK：LEGB 在 Global 層找到</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"># 修改後：logger 變成 main() 的區域變數</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">helper</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&#34;working...&#34;</span><span class="p">)</span>  <span class="c1"># NameError! helper 看不到 main 的區域變數</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">helper</span><span class="p">()</span></span></span></code></pre></div><h3 id="為什麼危險">為什麼危險</h3>
<p>兩個因素疊加讓這個 bug 特別難發現：</p>
<ol>
<li><strong><code>py_compile</code> 抓不到</strong>：<code>logger.info(...)</code> 語法完全合法，名稱解析要到執行時才發生</li>
<li><strong>頂層例外處理吞掉了 <code>NameError</code></strong>：<code>run_hook_safely()</code> 捕捉所有 <code>Exception</code>，Hook 靜默失敗而非 crash（詳見 <a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a>）</li>
</ol>
<p>結果：7 個 Hook 在至少 2 個 session 中靜默失敗，41 個函式需要修正，+143/-81 行修改——全部源自一個「只是移動定義位置」的操作。</p>
<h3 id="正確做法">正確做法</h3>
<p>修改前用 grep 或 AST 列出所有引用，逐一加入 <code>logger</code> 參數，再用 AST 驗證無遺漏。完整的四步修正流程見<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a>。</p>
<hr>
<h2 id="陷阱二import-未同步">陷阱二：Import 未同步</h2>
<h3 id="背景">背景</h3>
<p>W22 重構將 <code>common_functions.py</code> 從 <code>.claude/hooks/</code> 遷移至 <code>.claude/hooks/lib/</code>。但只更新了部分 Hook 的 import 路徑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="n">sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># OK</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"># 遷移後（模組移到 lib/，但 import 未更新）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">from</span> <span class="nn">common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># ModuleNotFoundError!</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="c1"># 正確的遷移後寫法</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># OK</span></span></span></code></pre></div><h3 id="5-why-分析">5 Why 分析</h3>
<ol>
<li>Hook 啟動時拋出 <code>ModuleNotFoundError</code></li>
<li><code>from common_functions import ...</code> 找不到模組</li>
<li><code>common_functions.py</code> 已遷移至 <code>lib/</code> 子目錄</li>
<li>遷移時只更新了<strong>部分</strong> Hook 的 import 路徑</li>
<li><strong>根本原因</strong>：模組遷移後缺乏「全量引用更新」步驟</li>
</ol>
<p>5 個 Hook 受影響，涵蓋 SessionStart、PostToolUse、UserPromptSubmit 三種事件類型。</p>
<h3 id="第二次發生">第二次發生</h3>
<p>同一個模式在後續又發生了一次。W24 統一 <code>sys.path</code> 風格時，<code>task-dispatch-readiness-check.py</code> 的 <code>sys.path</code> 只包含 <code>.claude/hooks/</code>，缺少 <code>.claude/lib/</code>。</p>
<p>更危險的是，這次的 error 與另一個 Hook 的 error（plugin timeout）同時出現。移除 plugin 後以為問題解決了，實際上只消除了其中一個來源。</p>
<p><strong>教訓</strong>：多個不同來源的 error 同時存在時，修一個後不能假設全部修好了——必須逐一驗證每一個。</p>
<h3 id="正確的遷移步驟">正確的遷移步驟</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Step 1：列出所有引用</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">grep -r <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py
</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"># Step 2：列出所有直接 import</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">grep -r <span class="s2">&#34;import common_functions&#34;</span> .claude/hooks/*.py
</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"># Step 3：逐一更新 import 路徑（根據 Step 1-2 的清單）</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="c1"># Step 4：逐一驗證</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">for</span> f in .claude/hooks/*.py<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Testing </span><span class="nv">$f</span><span class="s2">...&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;{}&#39;</span> <span class="p">|</span> python3 <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> head -5
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><h3 id="與陷阱一的對比">與陷阱一的對比</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>陷阱一（作用域）</th>
          <th>陷阱二（Import）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修改了什麼</td>
          <td>變數定義的位置</td>
          <td>模組檔案的位置</td>
      </tr>
      <tr>
          <td>遺漏了什麼</td>
          <td>引用該變數的函式</td>
          <td>引用該模組的檔案</td>
      </tr>
      <tr>
          <td>py_compile 能偵測？</td>
          <td>否</td>
          <td>否</td>
      </tr>
      <tr>
          <td>grep 能找出？</td>
          <td>是</td>
          <td>是</td>
      </tr>
  </tbody>
</table>
<p>根本結構完全相同：<strong>移動了定義，沒有追蹤引用</strong>。</p>
<hr>
<h2 id="陷阱三靜默故障">陷阱三：靜默故障</h2>
<p>IMP-006 收錄了四個 Hook 隱性故障案例。這裡選取三個，分別代表不同的「部分更新」變體。</p>
<h3 id="案例-a函式參數遺漏">案例 A：函式參數遺漏</h3>
<p><code>save_check_log()</code> 需要 5 個參數，但某個 call site 只傳了 4 個：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 第 471 行（正確）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">save_check_log</span><span class="p">(</span><span class="n">prompt</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">is_dev</span><span class="p">,</span> <span class="n">count</span><span class="p">,</span> <span class="n">logger</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"># 第 453 行（早期返回路徑，遺漏 logger）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">save_check_log</span><span class="p">(</span><span class="n">prompt</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># TypeError: missing argument</span></span></span></code></pre></div><p>同一個函式在同一個檔案中呼叫了兩次。第二次是在早期返回（early return）路徑上，開發者 copy-paste 後漏掉了最後一個參數。</p>
<p>這跟陷阱一本質相同——函式簽名變更後（加入 <code>logger</code> 參數），沒有更新<strong>所有</strong> call site。</p>
<h3 id="案例-b語義分類錯誤">案例 B：語義分類錯誤</h3>
<p><code>command-entrance-gate-hook.py</code> 將「分析、調查、研究」等關鍵字歸入 <code>DEVELOPMENT_KEYWORDS</code>，導致分析命令被當作開發命令處理，被要求先建立 Ticket 才能執行。</p>
<p>但根據決策樹，分析類命令走「問題處理流程」，不需要 Ticket。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 錯誤：ANALYSIS_KEYWORDS 被放進 DEVELOPMENT_KEYWORDS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">DEVELOPMENT_KEYWORDS</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;implement&#34;</span><span class="p">,</span> <span class="s2">&#34;create&#34;</span><span class="p">,</span> <span class="s2">&#34;fix&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s2">&#34;analyze&#34;</span><span class="p">,</span> <span class="s2">&#34;investigate&#34;</span><span class="p">,</span> <span class="s2">&#34;research&#34;</span><span class="p">,</span>  <span class="c1"># 這些不是開發命令！</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"># 正確：分析類關鍵字應在白名單中</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">exploration_patterns</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;analyze&#34;</span><span class="p">,</span> <span class="s2">&#34;investigate&#34;</span><span class="p">,</span> <span class="s2">&#34;research&#34;</span><span class="p">,</span> <span class="s2">&#34;trace&#34;</span><span class="p">]</span></span></span></code></pre></div><p>這不是典型的「引用未更新」，但仍屬於<strong>部分更新</strong>問題：Hook 的語義分類與決策樹的語義定義不同步。修改了決策樹的行為分類，但沒有同步更新 Hook 的關鍵字分類。</p>
<h3 id="案例-c多路徑覆蓋不完整">案例 C：多路徑覆蓋不完整</h3>
<p><code>agent-ticket-validation-hook.py</code> 有兩條錯誤路徑：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 路徑 1：未預期異常（已有 stderr 輸出）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Error] </span><span class="si">{</span><span class="n">traceback</span><span class="o">.</span><span class="n">format_exc</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</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"># 路徑 2：有意阻止（遺漏 stderr 輸出）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="n">valid</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="mi">2</span>  <span class="c1"># exit code 2，但沒有 stderr 告訴使用者為什麼</span></span></span></code></pre></div><p>開發者只覆蓋了第一條路徑。第二條路徑（業務邏輯拒絕）執行時，使用者只看到 &ldquo;hook error&rdquo; 和 &ldquo;No stderr output&rdquo;，無法得知被拒絕的原因。</p>
<p><strong>教訓</strong>：一個函式的所有非成功路徑都需要相同等級的錯誤報告，不能只覆蓋 exception 路徑。</p>
<hr>
<h2 id="共通模式部分更新">共通模式：部分更新</h2>
<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">          修改了 A
</span></span><span class="line"><span class="ln">2</span><span class="cl">              |
</span></span><span class="line"><span class="ln">3</span><span class="cl">    A 有 N 個引用/依賴
</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">     只更新了其中 M 個
</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">       N - M 個壞掉了
</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></span></code></pre></div><p>不管 A 是變數定義（陷阱一）、模組路徑（陷阱二）、還是函式簽名（陷阱三），模式一致：</p>
<ol>
<li>修改了某個「被依賴的東西」</li>
<li>沒有找出<strong>所有</strong>依賴它的地方</li>
<li>遺漏的部分在<strong>執行時</strong>才爆炸</li>
<li>由於例外處理或 UI 限制，爆炸被吞掉</li>
</ol>
<h3 id="防護公式">防護公式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">安全的重構 = 修改定義 + 列出全部引用 + 逐一更新 + 逐一驗證</span></span></code></pre></div><p>四步少一步都會出事：</p>
<ul>
<li>少了「列出全部引用」 &ndash; 你不知道影響範圍（三個陷阱的共通原因）</li>
<li>少了「逐一更新」 &ndash; 知道但沒做完（陷阱二的第二次發生）</li>
<li>少了「逐一驗證」 &ndash; 做了但不確定對不對（陷阱一用 py_compile 驗證的盲點）</li>
</ul>
<h3 id="grep防護公式的第一步">grep：防護公式的第一步</h3>
<p>「列出全部引用」聽起來很簡單，但容易被跳過。以下是每種重構類型對應的 grep 命令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 變數作用域變更：找出所有引用某變數的位置</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rn <span class="s2">&#34;logger&#34;</span> hooks/*.py <span class="p">|</span> grep -v <span class="s2">&#34;def.*logger&#34;</span> <span class="p">|</span> grep -v <span class="s2">&#34;^#&#34;</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"># 模組遷移：找出所有 import 語句</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">grep -rn <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py
</span></span><span class="line"><span class="ln">6</span><span class="cl">grep -rn <span class="s2">&#34;import common_functions&#34;</span> .claude/hooks/*.py
</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">grep -rn <span class="s2">&#34;save_check_log(&#34;</span> .claude/hooks/*.py</span></span></code></pre></div><p>重點是<strong>養成習慣</strong>：修改定義之前，先跑一次搜尋，看看這個名稱出現在哪些地方。這一步花不到 30 秒，但能避免幾小時的除錯。</p>
<hr>
<h2 id="防護工具箱">防護工具箱</h2>
<p>不同的驗證工具能偵測不同層級的問題。沒有銀彈，但可以根據重構類型選擇正確的工具組合。</p>
<h3 id="工具能力對照表">工具能力對照表</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>語法錯誤</th>
          <th>作用域問題</th>
          <th>Import 問題</th>
          <th>參數數量</th>
          <th>語義正確性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>py_compile</code></td>
          <td>是</td>
          <td><strong>否</strong></td>
          <td><strong>否</strong></td>
          <td><strong>否</strong></td>
          <td><strong>否</strong></td>
      </tr>
      <tr>
          <td><code>grep</code> / 文字搜尋</td>
          <td>&ndash;</td>
          <td>找出引用</td>
          <td>找出引用</td>
          <td>找出 call site</td>
          <td>&ndash;</td>
      </tr>
      <tr>
          <td>AST 分析</td>
          <td>是</td>
          <td><strong>是</strong></td>
          <td>部分</td>
          <td><strong>是</strong></td>
          <td><strong>否</strong></td>
      </tr>
      <tr>
          <td><code>pylint</code></td>
          <td>是</td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>否</strong></td>
      </tr>
      <tr>
          <td><code>mypy</code></td>
          <td>是</td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>否</strong></td>
      </tr>
      <tr>
          <td>實際執行</td>
          <td>是</td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
          <td><strong>是</strong></td>
      </tr>
  </tbody>
</table>
<h3 id="關鍵發現">關鍵發現</h3>
<p><strong>py_compile 是必要但不充分的</strong>。它能確認「Python 能讀懂這個檔案」，但不能確認「這個檔案能正確執行」。三個陷阱中沒有一個能被 py_compile 偵測到。</p>
<p><strong>grep 是最可靠的第一步</strong>。不管是變數引用、import 路徑還是函式呼叫，文字搜尋都能找出所有使用處。它不聰明，但不會遺漏。</p>
<p><strong>實際執行是唯一能驗證語義的工具</strong>。案例 B 的語義分類錯誤，靜態工具全部無法偵測——因為程式碼邏輯上沒錯，錯的是<strong>業務語義</strong>。</p>
<h3 id="按重構類型選擇工具">按重構類型選擇工具</h3>
<table>
  <thead>
      <tr>
          <th>重構類型</th>
          <th>最低要求</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>移動變數定義</td>
          <td>grep + AST 分析</td>
          <td>+ 實際執行覆蓋所有路徑</td>
      </tr>
      <tr>
          <td>移動模組檔案</td>
          <td>grep + 逐一 import 驗證</td>
          <td>+ <code>echo '{}' | python3 file.py</code></td>
      </tr>
      <tr>
          <td>修改函式簽名</td>
          <td>grep + AST 參數檢查</td>
          <td>+ pylint + 測試覆蓋</td>
      </tr>
      <tr>
          <td>修改關鍵字/分類</td>
          <td>與設計文件交叉比對</td>
          <td>+ 手動場景測試</td>
      </tr>
      <tr>
          <td>統一風格（批量）</td>
          <td>先 grep 建立完整清單</td>
          <td>+ 逐一驗證，不依賴數量判斷</td>
      </tr>
  </tbody>
</table>
<h3 id="批量重構的特殊風險">批量重構的特殊風險</h3>
<p>統一化重構（如「把 16 個 Hook 的 logger 風格統一」）比單一檔案的修改危險得多，因為：</p>
<ol>
<li><strong>數量產生虛假信心</strong>：改了 13 個成功了，容易假設剩下 3 個也沒問題</li>
<li><strong>機械性動作降低警覺</strong>：重複相同操作 16 次，注意力會下降</li>
<li><strong>驗證疲勞</strong>：逐一驗證 16 個檔案很煩，容易偷懶跳過</li>
</ol>
<p>對策：建立完整清單，逐一打勾，用腳本自動化驗證。</p>
<hr>
<h2 id="建立自己的重構檢查清單">建立自己的重構檢查清單</h2>
<p>根據本章三個陷阱的經驗，任何涉及「移動或修改被引用物件」的重構，都應該執行以下清單：</p>
<h3 id="修改前強制">修改前（強制）</h3>
<ul>
<li><input disabled="" type="checkbox"> 用 <code>grep</code> 列出所有引用/使用處，建立完整清單</li>
<li><input disabled="" type="checkbox"> 評估每個引用是否需要同步更新</li>
<li><input disabled="" type="checkbox"> 確認驗證方法（不能只用 py_compile）</li>
</ul>
<h3 id="修改中">修改中</h3>
<ul>
<li><input disabled="" type="checkbox"> 按清單逐一更新每個引用</li>
<li><input disabled="" type="checkbox"> 每更新一個就在清單打勾，不跳過</li>
</ul>
<h3 id="修改後強制">修改後（強制）</h3>
<ul>
<li><input disabled="" type="checkbox"> 用 AST 分析或 pylint 驗證作用域和參數</li>
<li><input disabled="" type="checkbox"> 實際執行（或測試）覆蓋所有修改過的檔案</li>
<li><input disabled="" type="checkbox"> 如果是批量修改，逐一驗證每個檔案，不依賴數量判斷</li>
</ul>
<hr>
<h2 id="思考題">思考題</h2>
<ol>
<li>
<p>為什麼動態語言（Python、JavaScript）比靜態語言（Java、Dart）更容易出現這類問題？靜態語言的什麼機制能在編譯期偵測到陷阱一和陷阱二？</p>
</li>
<li>
<p>陷阱三案例 B（語義分類錯誤）無法被任何靜態工具偵測。你會如何設計一個測試來防護這類問題？</p>
</li>
<li>
<p>「頂層例外處理吞掉錯誤」既是安全機制（防止 crash），也是風險（隱藏 bug）。如何在這兩個需求之間取得平衡？（可參考 <a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a> 的設計方案）</p>
</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>
<p>選擇一個使用全域變數的 Python 專案，嘗試將一個全域變數移入函式內部。在修改前後分別用 py_compile、AST 分析、pylint 驗證，比較各工具的偵測能力。</p>
</li>
<li>
<p>寫一個 Python 腳本，接受一個 Python 檔案和一個變數名稱作為輸入，輸出「所有引用該變數但沒有在參數中接收它的函式」清單。</p>
</li>
<li>
<p>設計一個模組遷移的自動化腳本：接受舊路徑和新路徑，自動搜尋所有 <code>import</code> 語句並更新，最後逐一驗證每個修改過的檔案是否能成功 import。</p>
</li>
</ol>
<hr>
<p><em>上一章：<a href="/blog/python/07-refactoring/unified-infrastructure/" data-link-title="大規模統一化重構" data-link-desc="從 44 種不同實作到統一基礎設施：日誌、訊息、風格的三階段漸進式重構">大規模統一化重構</a></em>
<em>下一章：<a href="/blog/python/07-refactoring/non-code-refactoring/" data-link-title="非程式碼的重構" data-link-desc="用 Progressive Disclosure 精簡膨脹的規則文件，文件重構和程式碼重構是同一套思維">非程式碼的重構</a></em>
<em>相關：<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a> &ndash; 陷阱一的完整深入分析</em>
<em>相關：<a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">5.5 頂層例外處理機制</a> &ndash; 例外處理如何隱藏 bug 的機制分析</em></p>
]]></content:encoded></item><item><title>非程式碼的重構</title><link>https://tarrragon.github.io/blog/python/07-refactoring/non-code-refactoring/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/non-code-refactoring/</guid><description>&lt;p>前面幾章我們重構的對象都是程式碼：提取函式、消除魔法數字、分離配置。但你有沒有想過，&lt;strong>文件也會腐敗&lt;/strong>？&lt;/p>
&lt;p>當一份規則文件從 50 行長到 450 行，閱讀者要在腦中同時追蹤的概念數量跟&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/refactoring-strategy/" data-link-title="重構的動機與策略" data-link-desc="從 Hook 系統重構經驗出發，學習何時重構、何時不該重構，以及如何將大規模重構拆分成可管理的階段">第一章&lt;/a>提到的那個 858 行 Python 檔案沒有本質區別。認知負擔不只存在於程式碼中——任何需要人類閱讀和理解的東西都受它影響。&lt;/p>
&lt;h2 id="問題文件膨脹">問題：文件膨脹&lt;/h2>
&lt;p>v0.28.0 到 v0.31.0 之間，專案的規則文件經歷了 9 個版本的迭代。每次迭代都在解決真實的問題：補充遺漏的邊界情況、新增流程步驟、記錄決策理由。每一次修改都合理，但累積的結果是：&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">parallel-dispatch.md 280 行 ← 原本是「什麼時候可以並行」的簡單指南
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">task-splitting.md 230 行 ← 原本是「怎麼拆任務」的清單
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">ticket-lifecycle.md 220 行 ← 原本是「Ticket 狀態怎麼轉」的流程
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">version-progression.md 180 行 ← 原本是「什麼時候推進版本」的判斷
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">incident-response.md 170 行 ← 原本是「出錯了怎麼辦」的流程
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">query-vs-research.md 130 行 ← 原本是「查資料要不要派人」的二選一
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">plan-to-ticket.md 100 行 ← 原本是「計畫怎麼變成 Ticket」的流程
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">decision-tree.md 452 行 ← 核心決策樹，每次都在「補一個分支」&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>問題不是內容不正確——每一行都有存在的理由。問題是&lt;strong>讀不動&lt;/strong>。&lt;/p>
&lt;p>一個新加入的代理人需要閱讀 &lt;code>parallel-dispatch.md&lt;/code> 來決定能不能並行派發任務。它真正需要的資訊是：觸發條件（5 行）、安全檢查清單（6 行）、決策流程圖（5 行）。但它必須在 280 行中找到這些——剩下的 264 行是 5W1H 格式範例、分析任務的並行原則、Agent Teams 場景表、進度追蹤模板。&lt;/p>
&lt;p>這就像在一個 500 行的函式裡找那 20 行核心邏輯。&lt;/p>
&lt;h3 id="膨脹的過程">膨脹的過程&lt;/h3>
&lt;p>文件膨脹的方式和程式碼膨脹幾乎一模一樣。回顧 &lt;code>parallel-dispatch.md&lt;/code> 的成長軌跡：&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>v1.0&lt;/td>
 &lt;td>60 行&lt;/td>
 &lt;td>初始版本：觸發條件 + 決策流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>v1.1&lt;/td>
 &lt;td>90 行&lt;/td>
 &lt;td>補充：安全檢查清單（因為有人忘記檢查檔案衝突）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>v1.2&lt;/td>
 &lt;td>130 行&lt;/td>
 &lt;td>新增：Agent Teams 派發方式（新功能）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>v2.0&lt;/td>
 &lt;td>180 行&lt;/td>
 &lt;td>新增：5W1H 格式範例（因為有人不知道怎麼寫）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>v2.3&lt;/td>
 &lt;td>230 行&lt;/td>
 &lt;td>新增：分析任務並行原則、進度追蹤模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>v2.5&lt;/td>
 &lt;td>280 行&lt;/td>
 &lt;td>新增：並行派發後驗證流程（因為有人忘記驗證）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每一次新增都在解決真實問題。沒有人故意讓文件變長。但六個版本之後，一個簡單的「能不能並行」判斷指南變成了包羅萬象的操作手冊。&lt;/p>
&lt;p>這和程式碼中的「上帝函式」成因一樣：每次「加一點」都合理，但沒有人停下來問「這個函式是不是該拆了」。&lt;/p>
&lt;h2 id="解決方案progressive-disclosure">解決方案：Progressive Disclosure&lt;/h2>
&lt;p>程式碼重構有 Extract Method——把函式內部的細節提取到獨立函式中。文件重構有對應的技巧：&lt;strong>Progressive Disclosure&lt;/strong>（漸進式揭露）。&lt;/p>
&lt;p>核心思想：&lt;strong>常駐只保留決策入口和強制規則，細節放到參考文件中按需載入。&lt;/strong>&lt;/p>
&lt;p>這和函式設計的道理一樣。你不會把排序演算法的完整實作放在 &lt;code>main()&lt;/code> 裡面，你會呼叫 &lt;code>sort()&lt;/code>。同樣地，規則文件的讀者不需要在判斷「能不能並行」時看到「Agent Teams 的 3-4x 成本計算方式」。&lt;/p>
&lt;h3 id="精簡原則">精簡原則&lt;/h3>
&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;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資訊層級&lt;/td>
 &lt;td>決策入口 → 強制規則 → 參考細節&lt;/td>
 &lt;td>讀者按需深入，不強制閱讀全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DRY&lt;/td>
 &lt;td>細節只寫一次，放在 references/&lt;/td>
 &lt;td>多個文件引用同一份參考，不複製貼上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>量化驗證&lt;/td>
 &lt;td>精簡前後行數對比&lt;/td>
 &lt;td>有數字才知道改善了多少&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="具體做法">具體做法&lt;/h3>
&lt;p>以 &lt;code>parallel-dispatch.md&lt;/code> 為例，精簡過程分三步：&lt;/p>
&lt;h4 id="step-1識別決策入口和參考細節">Step 1：識別「決策入口」和「參考細節」&lt;/h4>
&lt;p>閱讀 280 行內容，對每一段問：「讀者在做『能不能並行』這個決策時需要這段嗎？」&lt;/p>
&lt;ul>
&lt;li>觸發條件表格 → 需要（決策入口）&lt;/li>
&lt;li>安全檢查清單 → 需要（強制規則）&lt;/li>
&lt;li>決策流程圖 → 需要（快查表）&lt;/li>
&lt;li>數量原則 → 需要（簡短規則）&lt;/li>
&lt;li>不適用場景 → 需要（負面清單）&lt;/li>
&lt;li>5W1H 格式範例 → 不需要（移出）&lt;/li>
&lt;li>Agent Teams 場景表 → 不需要（移出）&lt;/li>
&lt;li>進度追蹤模板 → 不需要（移出）&lt;/li>
&lt;/ul>
&lt;h4 id="step-2提取到參考文件">Step 2：提取到參考文件&lt;/h4>





&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"># 精簡前
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">parallel-dispatch.md (280 行，所有內容混在一起)
&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">parallel-dispatch.md (98 行，決策入口 + 強制規則)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> └→ references/parallel-dispatch-details.md (剩餘細節，按需查閱)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-3加入連結">Step 3：加入連結&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 並行派發後驗證（強制）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">所有並行代理人回報完成後，**必須**執行 &lt;span class="sb">`git diff --stat`&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">&amp;gt; 詳細驗證步驟和常見原因：.claude/references/parallel-dispatch-details.md&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀者看到這段就知道：「驗證是強制的，具體步驟在那個連結裡。」它可以選擇深入閱讀，也可以先完成手邊的決策。&lt;/p></description><content:encoded><![CDATA[<p>前面幾章我們重構的對象都是程式碼：提取函式、消除魔法數字、分離配置。但你有沒有想過，<strong>文件也會腐敗</strong>？</p>
<p>當一份規則文件從 50 行長到 450 行，閱讀者要在腦中同時追蹤的概念數量跟<a href="/blog/python/07-refactoring/refactoring-strategy/" data-link-title="重構的動機與策略" data-link-desc="從 Hook 系統重構經驗出發，學習何時重構、何時不該重構，以及如何將大規模重構拆分成可管理的階段">第一章</a>提到的那個 858 行 Python 檔案沒有本質區別。認知負擔不只存在於程式碼中——任何需要人類閱讀和理解的東西都受它影響。</p>
<h2 id="問題文件膨脹">問題：文件膨脹</h2>
<p>v0.28.0 到 v0.31.0 之間，專案的規則文件經歷了 9 個版本的迭代。每次迭代都在解決真實的問題：補充遺漏的邊界情況、新增流程步驟、記錄決策理由。每一次修改都合理，但累積的結果是：</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">parallel-dispatch.md     280 行  ← 原本是「什麼時候可以並行」的簡單指南
</span></span><span class="line"><span class="ln">2</span><span class="cl">task-splitting.md        230 行  ← 原本是「怎麼拆任務」的清單
</span></span><span class="line"><span class="ln">3</span><span class="cl">ticket-lifecycle.md      220 行  ← 原本是「Ticket 狀態怎麼轉」的流程
</span></span><span class="line"><span class="ln">4</span><span class="cl">version-progression.md   180 行  ← 原本是「什麼時候推進版本」的判斷
</span></span><span class="line"><span class="ln">5</span><span class="cl">incident-response.md     170 行  ← 原本是「出錯了怎麼辦」的流程
</span></span><span class="line"><span class="ln">6</span><span class="cl">query-vs-research.md     130 行  ← 原本是「查資料要不要派人」的二選一
</span></span><span class="line"><span class="ln">7</span><span class="cl">plan-to-ticket.md        100 行  ← 原本是「計畫怎麼變成 Ticket」的流程
</span></span><span class="line"><span class="ln">8</span><span class="cl">decision-tree.md         452 行  ← 核心決策樹，每次都在「補一個分支」</span></span></code></pre></div><p>問題不是內容不正確——每一行都有存在的理由。問題是<strong>讀不動</strong>。</p>
<p>一個新加入的代理人需要閱讀 <code>parallel-dispatch.md</code> 來決定能不能並行派發任務。它真正需要的資訊是：觸發條件（5 行）、安全檢查清單（6 行）、決策流程圖（5 行）。但它必須在 280 行中找到這些——剩下的 264 行是 5W1H 格式範例、分析任務的並行原則、Agent Teams 場景表、進度追蹤模板。</p>
<p>這就像在一個 500 行的函式裡找那 20 行核心邏輯。</p>
<h3 id="膨脹的過程">膨脹的過程</h3>
<p>文件膨脹的方式和程式碼膨脹幾乎一模一樣。回顧 <code>parallel-dispatch.md</code> 的成長軌跡：</p>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>行數</th>
          <th>新增原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>v1.0</td>
          <td>60 行</td>
          <td>初始版本：觸發條件 + 決策流程</td>
      </tr>
      <tr>
          <td>v1.1</td>
          <td>90 行</td>
          <td>補充：安全檢查清單（因為有人忘記檢查檔案衝突）</td>
      </tr>
      <tr>
          <td>v1.2</td>
          <td>130 行</td>
          <td>新增：Agent Teams 派發方式（新功能）</td>
      </tr>
      <tr>
          <td>v2.0</td>
          <td>180 行</td>
          <td>新增：5W1H 格式範例（因為有人不知道怎麼寫）</td>
      </tr>
      <tr>
          <td>v2.3</td>
          <td>230 行</td>
          <td>新增：分析任務並行原則、進度追蹤模板</td>
      </tr>
      <tr>
          <td>v2.5</td>
          <td>280 行</td>
          <td>新增：並行派發後驗證流程（因為有人忘記驗證）</td>
      </tr>
  </tbody>
</table>
<p>每一次新增都在解決真實問題。沒有人故意讓文件變長。但六個版本之後，一個簡單的「能不能並行」判斷指南變成了包羅萬象的操作手冊。</p>
<p>這和程式碼中的「上帝函式」成因一樣：每次「加一點」都合理，但沒有人停下來問「這個函式是不是該拆了」。</p>
<h2 id="解決方案progressive-disclosure">解決方案：Progressive Disclosure</h2>
<p>程式碼重構有 Extract Method——把函式內部的細節提取到獨立函式中。文件重構有對應的技巧：<strong>Progressive Disclosure</strong>（漸進式揭露）。</p>
<p>核心思想：<strong>常駐只保留決策入口和強制規則，細節放到參考文件中按需載入。</strong></p>
<p>這和函式設計的道理一樣。你不會把排序演算法的完整實作放在 <code>main()</code> 裡面，你會呼叫 <code>sort()</code>。同樣地，規則文件的讀者不需要在判斷「能不能並行」時看到「Agent Teams 的 3-4x 成本計算方式」。</p>
<h3 id="精簡原則">精簡原則</h3>
<p>我們制定了四條原則來指導文件重構，和程式碼重構的原則一一對應：</p>
<table>
  <thead>
      <tr>
          <th>程式碼原則</th>
          <th>文件原則</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一責任</td>
          <td>一個文件回答一個問題</td>
          <td>常駐文件只回答「怎麼判斷」，不回答「細節怎麼做」</td>
      </tr>
      <tr>
          <td>資訊層級</td>
          <td>決策入口 → 強制規則 → 參考細節</td>
          <td>讀者按需深入，不強制閱讀全部</td>
      </tr>
      <tr>
          <td>DRY</td>
          <td>細節只寫一次，放在 references/</td>
          <td>多個文件引用同一份參考，不複製貼上</td>
      </tr>
      <tr>
          <td>量化驗證</td>
          <td>精簡前後行數對比</td>
          <td>有數字才知道改善了多少</td>
      </tr>
  </tbody>
</table>
<h3 id="具體做法">具體做法</h3>
<p>以 <code>parallel-dispatch.md</code> 為例，精簡過程分三步：</p>
<h4 id="step-1識別決策入口和參考細節">Step 1：識別「決策入口」和「參考細節」</h4>
<p>閱讀 280 行內容，對每一段問：「讀者在做『能不能並行』這個決策時需要這段嗎？」</p>
<ul>
<li>觸發條件表格 → 需要（決策入口）</li>
<li>安全檢查清單 → 需要（強制規則）</li>
<li>決策流程圖 → 需要（快查表）</li>
<li>數量原則 → 需要（簡短規則）</li>
<li>不適用場景 → 需要（負面清單）</li>
<li>5W1H 格式範例 → 不需要（移出）</li>
<li>Agent Teams 場景表 → 不需要（移出）</li>
<li>進度追蹤模板 → 不需要（移出）</li>
</ul>
<h4 id="step-2提取到參考文件">Step 2：提取到參考文件</h4>





<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"># 精簡前
</span></span><span class="line"><span class="ln">2</span><span class="cl">parallel-dispatch.md (280 行，所有內容混在一起)
</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></span><span class="line"><span class="ln">5</span><span class="cl">parallel-dispatch.md (98 行，決策入口 + 強制規則)
</span></span><span class="line"><span class="ln">6</span><span class="cl">  └→ references/parallel-dispatch-details.md (剩餘細節，按需查閱)</span></span></code></pre></div><h4 id="step-3加入連結">Step 3：加入連結</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 並行派發後驗證（強制）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">所有並行代理人回報完成後，**必須**執行 <span class="sb">`git diff --stat`</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">&gt; 詳細驗證步驟和常見原因：.claude/references/parallel-dispatch-details.md</span></span></code></pre></div><p>讀者看到這段就知道：「驗證是強制的，具體步驟在那個連結裡。」它可以選擇深入閱讀，也可以先完成手邊的決策。</p>
<h3 id="精簡前後對比">精簡前後對比</h3>
<p>看看 <code>incident-response.md</code> 精簡前後的結構差異：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gh"># 精簡前 (170 行)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="gu">## 強制流程
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="gu"></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="gu">## 強制觸發條件
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="gu"></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="gu">## 派發對應表
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="gu"></span>（代理人對應表）
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gu">## 多視角分析原則             ← Level 3
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="gu"></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="gu">## 安全等級分類               ← Level 3
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="gu"></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="gu">## 報告格式範例               ← Level 3
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="gu"></span>（完整的報告模板）
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="gu">## 禁止行為
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="gu"></span>（禁止清單）</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gh"># 精簡後 (64 行)
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gh"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="gu">## 強制流程
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="gu"></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="gu">## 強制觸發條件
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="gu"></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="gu">## 派發對應表
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="gu"></span>（代理人對應表）
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="gu">## 禁止行為
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="gu"></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="gu">## 相關文件
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="gu"></span>&gt; 詳細規則：.claude/references/incident-response-details.md</span></span></code></pre></div><p>Level 3 的內容（多視角分析、安全等級、報告格式）整段移到 <code>references/</code> 目錄。常駐文件只剩下做決策需要的資訊。</p>
<h2 id="實際成果">實際成果</h2>
<p>7 個規則文件的精簡結果：</p>
<table>
  <thead>
      <tr>
          <th>文件</th>
          <th>精簡前</th>
          <th>精簡後</th>
          <th>縮減</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>parallel-dispatch.md</td>
          <td>280 行</td>
          <td>98 行</td>
          <td>-65%</td>
      </tr>
      <tr>
          <td>task-splitting.md</td>
          <td>230 行</td>
          <td>93 行</td>
          <td>-60%</td>
      </tr>
      <tr>
          <td>ticket-lifecycle.md</td>
          <td>220 行</td>
          <td>70 行</td>
          <td>-68%</td>
      </tr>
      <tr>
          <td>version-progression.md</td>
          <td>180 行</td>
          <td>69 行</td>
          <td>-62%</td>
      </tr>
      <tr>
          <td>incident-response.md</td>
          <td>170 行</td>
          <td>64 行</td>
          <td>-62%</td>
      </tr>
      <tr>
          <td>query-vs-research.md</td>
          <td>130 行</td>
          <td>46 行</td>
          <td>-65%</td>
      </tr>
      <tr>
          <td>plan-to-ticket.md</td>
          <td>100 行</td>
          <td>43 行</td>
          <td>-57%</td>
      </tr>
      <tr>
          <td><strong>合計</strong></td>
          <td><strong>1310 行</strong></td>
          <td><strong>483 行</strong></td>
          <td><strong>-63%</strong></td>
      </tr>
  </tbody>
</table>
<p>核心決策樹 <code>decision-tree.md</code> 也從 452 行精簡到 286 行（-37%）。它的縮減幅度較小，因為決策樹本身就是「決策入口」——大部分內容都是必要的分支判斷。</p>
<blockquote>
<p><strong>後記</strong>：上面的數字是 v3.0.0 精簡完成時的快照。精簡後不到兩週，<code>plan-to-ticket.md</code> 因為新增「執行中額外發現」流程從 43 行長回 87 行，<code>decision-tree.md</code> 因為新增 TDD Phase 路由從 286 行長回 434 行。這不代表精簡失敗——新增的內容都是必要的新功能。但它提醒我們：<strong>文件重構和程式碼重構一樣，是持續的紀律。</strong></p></blockquote>
<h2 id="通用原則">通用原則</h2>
<p>從這次文件重構中，我們提煉出四個可複用的原則：</p>
<h3 id="原則-1單一責任">原則 1：單一責任</h3>
<p>每份文件應該只回答一個核心問題。</p>
<table>
  <thead>
      <tr>
          <th>文件</th>
          <th>核心問題</th>
          <th>不該包含</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>parallel-dispatch.md</td>
          <td>能不能並行？</td>
          <td>Agent Teams 的完整操作手冊</td>
      </tr>
      <tr>
          <td>incident-response.md</td>
          <td>出錯了怎麼處理？</td>
          <td>每種錯誤的詳細分析範例</td>
      </tr>
      <tr>
          <td>ticket-lifecycle.md</td>
          <td>Ticket 狀態怎麼轉？</td>
          <td>Hook 的技術實作細節</td>
      </tr>
  </tbody>
</table>
<p>判斷方式和函式一樣：如果描述這份文件的用途需要「和」這個字，它可能需要拆分。</p>
<h3 id="原則-2資訊層級">原則 2：資訊層級</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">Level 0: 快查表（一眼就能找到答案）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Level 1: 強制規則（必須遵守的約束）
</span></span><span class="line"><span class="ln">3</span><span class="cl">Level 2: 決策流程（判斷邏輯）
</span></span><span class="line"><span class="ln">4</span><span class="cl">Level 3: 參考細節（範例、模板、歷史記錄）</span></span></code></pre></div><p>常駐文件只包含 Level 0-2，Level 3 放在 <code>references/</code> 目錄下。</p>
<p>對比程式碼的資訊層級：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Level 0: 函式簽名（一眼就知道做什麼）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">can_dispatch_parallel</span><span class="p">(</span><span class="n">tasks</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Task</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># Level 1: Guard clauses（強制規則）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="n">has_dependency</span><span class="p">(</span><span class="n">tasks</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="kc">False</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="c1"># Level 2: 核心邏輯（判斷邏輯）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="ow">not</span> <span class="n">has_file_overlap</span><span class="p">(</span><span class="n">tasks</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Level 3: 實作細節（在被呼叫的函式裡）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">def</span> <span class="nf">has_file_overlap</span><span class="p">(</span><span class="n">tasks</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="c1"># 50 行的具體實作...</span></span></span></code></pre></div><p>同一個概念，同一個結構。</p>
<h3 id="原則-3dry">原則 3：DRY</h3>
<p>文件之間也會出現重複。同一段「Wave 獨立性原則」如果在 <code>parallel-dispatch.md</code>、<code>task-splitting.md</code>、<code>version-progression.md</code> 三個地方都寫了，修改時就要改三處。</p>
<p>解決方式和程式碼一樣：抽到共用的參考文件，其他文件用連結引用。</p>
<h3 id="原則-4量化驗證">原則 4：量化驗證</h3>
<p>沒有數字的重構是自我感覺良好。精簡前後一定要量化比較：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 文件行數統計</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wc -l .claude/rules/flows/*.md .claude/rules/guides/*.md
</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="nb">echo</span> <span class="s2">&#34;精簡前: 1310 行&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;精簡後:  483 行&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;縮減率: 63%&#34;</span></span></span></code></pre></div><p>和程式碼重構的 <code>-65%</code> 行數對比一樣，行數本身不是目標，但它是認知負擔降低的代理指標。</p>
<h2 id="常見錯誤">常見錯誤</h2>
<p>文件重構也有自己的陷阱：</p>
<h3 id="錯誤-1過度精簡">錯誤 1：過度精簡</h3>
<p>把所有細節都移走，常駐文件只剩下標題和連結。讀者每做一個決策都要點開參考文件，跳轉次數太多反而增加認知負擔。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gh"># 錯誤：過度精簡
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gh"></span><span class="gu">## 並行派發
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gu"></span><span class="k">&gt; </span><span class="ge">詳見：references/parallel-dispatch-details.md
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="ge"></span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="gu">## 任務拆分
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="gu"></span>&gt; 詳見：references/task-splitting-details.md</span></span></code></pre></div><p>這等於一個函式裡全是 <code>call_other_function()</code>，讀者什麼都看不到。常駐文件至少要包含決策入口（判斷邏輯）和強制規則（不可違反的約束）。</p>
<h3 id="錯誤-2按章節拆分而非按層級拆分">錯誤 2：按「章節」拆分而非按「層級」拆分</h3>
<p>把文件按目錄拆成多個小文件，但每個小文件仍然混合了決策入口和參考細節。這只是把一個大問題變成了多個小問題。</p>
<p>正確的拆分維度是<strong>資訊層級</strong>，不是<strong>主題章節</strong>。</p>
<h3 id="錯誤-3沒有更新連結">錯誤 3：沒有更新連結</h3>
<p>精簡後忘記在常駐文件中加入參考文件的連結。讀者需要細節時找不到入口。這和重構後忘記更新 import 一樣危險。</p>
<h2 id="程式碼-vs-文件重構對照表">程式碼 vs 文件重構對照表</h2>
<p>程式碼重構和文件重構的手法其實是同一套思維的不同實踐：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>程式碼重構</th>
          <th>文件重構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>壞味道</td>
          <td>函式太長、巢狀過深</td>
          <td>文件太長、層級不分</td>
      </tr>
      <tr>
          <td>核心手法</td>
          <td>Extract Method</td>
          <td>Progressive Disclosure</td>
      </tr>
      <tr>
          <td>單一責任</td>
          <td>一個函式做一件事</td>
          <td>一份文件回答一個問題</td>
      </tr>
      <tr>
          <td>DRY</td>
          <td>提取共用模組</td>
          <td>提取共用參考文件</td>
      </tr>
      <tr>
          <td>量化指標</td>
          <td>行數、認知負擔指數</td>
          <td>行數、常駐 vs 參考比例</td>
      </tr>
      <tr>
          <td>驗證方式</td>
          <td>測試通過</td>
          <td>讀者能在 30 秒內找到答案</td>
      </tr>
      <tr>
          <td>失敗的重構</td>
          <td>過度拆分導致跳轉太多</td>
          <td>過度精簡導致資訊不足</td>
      </tr>
      <tr>
          <td>觸發條件</td>
          <td>函式超過 30 行</td>
          <td>文件超過 100 行且混合多個層級</td>
      </tr>
      <tr>
          <td>典型比例</td>
          <td>858 行 → 296 行 (-65%)</td>
          <td>280 行 → 98 行 (-65%)</td>
      </tr>
  </tbody>
</table>
<p>最後一行不是巧合。兩個案例的縮減率接近，是因為底層原理相同：大約 1/3 的內容是核心邏輯（決策入口），2/3 的內容是支撐細節（參考資料）。</p>
<h2 id="什麼時候該重構文件">什麼時候該重構文件</h2>
<p>和程式碼一樣，不是所有文件都需要重構。觸發條件：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>閾值</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>文件長度</td>
          <td>&gt; 100 行且混合多層級</td>
          <td>評估是否需要分離</td>
      </tr>
      <tr>
          <td>讀者反饋</td>
          <td>「找不到需要的資訊」</td>
          <td>重新組織資訊層級</td>
      </tr>
      <tr>
          <td>更新頻率</td>
          <td>每次更新都在不同段落</td>
          <td>考慮按變更頻率拆分</td>
      </tr>
      <tr>
          <td>重複內容</td>
          <td>同一段話出現在 2+ 份文件</td>
          <td>提取到共用參考</td>
      </tr>
  </tbody>
</table>
<h2 id="思考題">思考題</h2>
<ol>
<li>你的專案有沒有「什麼都寫在 README」的情況？如果有，試著用 Progressive Disclosure 原則拆分它。</li>
<li>為什麼 <code>decision-tree.md</code> 的縮減幅度（-37%）比其他文件（-57% 到 -68%）小？這說明了什麼？</li>
<li>文件重構有一個程式碼重構沒有的風險：「過度精簡導致讀者找不到需要的資訊」。你會怎麼驗證精簡後的文件仍然足夠完整？</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<h3 id="練習-1評估文件健康度">練習 1：評估文件健康度</h3>
<p>找一份超過 150 行的文件（你自己的專案 README、API 文件、團隊 Wiki），回答以下問題：</p>
<ul>
<li>這份文件回答幾個核心問題？（如果超過 1 個，考慮拆分）</li>
<li>讀者能在 30 秒內找到最常需要的資訊嗎？</li>
<li>有沒有段落只在特定情境下才需要閱讀？</li>
</ul>
<h3 id="練習-2執行-progressive-disclosure">練習 2：執行 Progressive Disclosure</h3>
<p>對那份文件執行三步精簡：</p>
<ol>
<li>標記每一段的資訊層級（Level 0-3）</li>
<li>將 Level 3 的內容提取到獨立的參考文件</li>
<li>在原文件中加入連結</li>
</ol>
<h3 id="練習-3量化驗證">練習 3：量化驗證</h3>
<p>計算精簡前後的行數和縮減率。如果縮減率低於 30%，思考是否原本的結構就已經不錯。如果超過 70%，檢查是否過度精簡了。</p>
<details>
<summary>參考範圍</summary>
<p>根據本章的實際案例，健康的縮減率大約在 55%-68% 之間。核心決策類文件（如 decision-tree）的縮減率通常較低（30%-40%），因為它本身就是決策入口。</p>
</details>
<h2 id="小結">小結</h2>
<p>重構不只是程式碼的事。任何需要人類閱讀的東西——規則文件、操作手冊、架構文件——都會隨著時間膨脹，累積認知負擔。</p>
<p>核心手法是 Progressive Disclosure：常駐只保留讀者<strong>當下需要</strong>的資訊，細節放到參考文件中<strong>按需載入</strong>。這和 Extract Method 的道理完全一樣：呼叫端只需要知道函式名稱和參數，不需要看到完整實作。</p>
<p>程式碼壞味道有 code smell，文件壞味道也有——只是比較少人談論。</p>
<hr>
<p><em>上一章：<a href="/blog/python/07-refactoring/refactoring-pitfalls/" data-link-title="重構陷阱與防護" data-link-desc="三個真實重構事故的共通模式：部分更新問題與系統性防護方法">重構陷阱與防護</a></em>
<em>下一章：<a href="/blog/python/07-refactoring/case-study/" data-link-title="完整案例回顧" data-link-desc="從超過 30 個 Hook 各自為政到系統化品質工程，三個階段的完整重構復盤">完整案例回顧</a></em></p>
]]></content:encoded></item><item><title>完整案例回顧</title><link>https://tarrragon.github.io/blog/python/07-refactoring/case-study/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/case-study/</guid><description>&lt;p>本章是模組七的總結。前面九章從動機判斷開始，依序教了壞味道識別、配置分離、DRY 原則、常數管理、消除魔法數字、大規模統一化、重構陷阱防護、作用域迴歸、以及非程式碼的重構——這些都是從同一個真實專案提煉的。現在把時間線拉開，看看這些技術在三個階段中如何逐步應用，以及過程中犯了哪些錯。&lt;/p>
&lt;h2 id="起點超過-30-個-hook-各自為政">起點：超過 30 個 Hook 各自為政&lt;/h2>
&lt;p>v0.28.0 之前的 Hook 系統是「有機生長」的典型案例。32 個 Hook 各自獨立開發，沒有共用程式庫、沒有統一風格、沒有測試。&lt;/p>
&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>task-dispatch 達 858 行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>函式重複&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>&lt;code>run_git_command&lt;/code> 等函式在多個檔案中複製貼上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配置硬編碼&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>代理人清單散落在程式碼各處&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>魔法數字&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>&lt;code>line[9:]&lt;/code> 這類寫法隨處可見&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>無測試&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>任何修改都是盲改&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些問題不是一天造成的。每次新增 Hook 時，最快的做法就是從現有 Hook 複製一份再改。沒人想著「先建共用模組」，因為每次都只是「加一個小功能」。第一個 Hook 50 行，第二個 80 行，到第十個 Hook 時，已經有三四份 &lt;code>run_git_command&lt;/code> 的副本了。但每次都覺得「下次再整理」。&lt;/p>
&lt;p>用 &lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道識別&lt;/a> 中教的 grep 方法掃描一次：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">grep -rh &lt;span class="s2">&amp;#34;^def &amp;#34;&lt;/span> .claude/hooks/*.py &lt;span class="p">|&lt;/span> sort &lt;span class="p">|&lt;/span> uniq -c &lt;span class="p">|&lt;/span> sort -rn &lt;span class="p">|&lt;/span> head -5&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>掃描後會發現同樣的函式定義散落在多個檔案中。&lt;code>run_git_command&lt;/code> 出現在 2 個以上的 Hook 裡，&lt;code>get_current_branch&lt;/code>、&lt;code>read_yaml_file&lt;/code> 等也有各自的副本。如果修復 &lt;code>run_git_command&lt;/code> 的一個 bug，你需要同時改每個有副本的檔案，而且不能漏掉任何一個。&lt;/p>
&lt;p>累積到 32 個 Hook 時，技術債務已經大到無法忽視。&lt;/p>
&lt;h2 id="第一階段v0280-結構性重構">第一階段：v0.28.0 結構性重構&lt;/h2>
&lt;p>第一階段的目標很明確：消除重複、建立結構。我們把工作拆成四個 Wave，每個 Wave 有獨立的交付物和驗證點。&lt;/p>
&lt;h3 id="wave-1建立共用程式庫">Wave 1：建立共用程式庫&lt;/h3>
&lt;p>先不動任何 Hook 檔案。第一步是把散落各處的重複邏輯抽取到獨立模組，並為每個模組寫測試。&lt;/p>
&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>config_loader&lt;/td>
 &lt;td>讀取 YAML 配置&lt;/td>
 &lt;td>ARCH-001 硬編碼配置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>git_utils&lt;/td>
 &lt;td>封裝 Git 命令&lt;/td>
 &lt;td>IMP-001 重複程式碼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hook_io&lt;/td>
 &lt;td>統一 Hook I/O 處理&lt;/td>
 &lt;td>IMP-001 重複程式碼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hook_logging&lt;/td>
 &lt;td>統一日誌設定&lt;/td>
 &lt;td>IMP-001 重複程式碼&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>為什麼先建程式庫而不是先改 Hook？因為如果直接改 Hook，會遇到雞生蛋問題——Hook A 需要共用函式，但共用函式還沒建立。先建好程式庫並通過測試，後續每改一個 Hook 都有安全網。&lt;/p>
&lt;p>建立模組時的關鍵決策：介面設計先於實作。我們先定義每個模組的公開函式簽名，寫測試驗證這些簽名的行為，最後才把各 Hook 中的重複邏輯搬進來。這確保了模組的介面是「為使用者設計」的，而不是「照搬原始碼」的。&lt;/p>
&lt;p>這個順序的思考方式在 &lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫&lt;/a> 中有詳細說明。&lt;/p>
&lt;h3 id="wave-2配置分離">Wave 2：配置分離&lt;/h3>
&lt;p>把 task-dispatch 中的硬編碼清單抽到 YAML 檔案：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c"># agents.yaml（節錄）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">agents&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">incident-responder&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">triggers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;test failed&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;compile error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;runtime error&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">priority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">system-analyst&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">triggers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;架構&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;設計&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;需求&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">priority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這一步的行數縮減最明顯。task-dispatch 中原本有大量的 if-elif 鏈在比對代理人名稱和觸發條件，類似這樣：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改前：60+ 行的 if-elif 鏈&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="s2">&amp;#34;test failed&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;compile error&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">agent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;incident-responder&amp;#34;&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">elif&lt;/span> &lt;span class="s2">&amp;#34;架構&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;設計&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">agent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;system-analyst&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">elif&lt;/span> &lt;span class="s2">&amp;#34;安全&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;auth&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&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="n">agent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;security-reviewer&amp;#34;&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"># ... 還有 20 幾個 elif&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>全部變成配置查表後，程式碼只剩下查表邏輯本身：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 修改後：配置驅動&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">agents&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">config_loader&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;agents.yaml&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">agent_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">config&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">agents&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&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="nb">any&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">trigger&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">message&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">trigger&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;triggers&amp;#34;&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="k">return&lt;/span> &lt;span class="n">agent_name&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>詳細的抽取過程在&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理&lt;/a>中說明。&lt;/p></description><content:encoded><![CDATA[<p>本章是模組七的總結。前面九章從動機判斷開始，依序教了壞味道識別、配置分離、DRY 原則、常數管理、消除魔法數字、大規模統一化、重構陷阱防護、作用域迴歸、以及非程式碼的重構——這些都是從同一個真實專案提煉的。現在把時間線拉開，看看這些技術在三個階段中如何逐步應用，以及過程中犯了哪些錯。</p>
<h2 id="起點超過-30-個-hook-各自為政">起點：超過 30 個 Hook 各自為政</h2>
<p>v0.28.0 之前的 Hook 系統是「有機生長」的典型案例。32 個 Hook 各自獨立開發，沒有共用程式庫、沒有統一風格、沒有測試。</p>
<p>具體問題：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>嚴重度</th>
          <th>量化</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單檔過大</td>
          <td>高</td>
          <td>task-dispatch 達 858 行</td>
      </tr>
      <tr>
          <td>函式重複</td>
          <td>高</td>
          <td><code>run_git_command</code> 等函式在多個檔案中複製貼上</td>
      </tr>
      <tr>
          <td>配置硬編碼</td>
          <td>中</td>
          <td>代理人清單散落在程式碼各處</td>
      </tr>
      <tr>
          <td>魔法數字</td>
          <td>中</td>
          <td><code>line[9:]</code> 這類寫法隨處可見</td>
      </tr>
      <tr>
          <td>無測試</td>
          <td>高</td>
          <td>任何修改都是盲改</td>
      </tr>
  </tbody>
</table>
<p>這些問題不是一天造成的。每次新增 Hook 時，最快的做法就是從現有 Hook 複製一份再改。沒人想著「先建共用模組」，因為每次都只是「加一個小功能」。第一個 Hook 50 行，第二個 80 行，到第十個 Hook 時，已經有三四份 <code>run_git_command</code> 的副本了。但每次都覺得「下次再整理」。</p>
<p>用 <a href="/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道識別</a> 中教的 grep 方法掃描一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">grep -rh <span class="s2">&#34;^def &#34;</span> .claude/hooks/*.py <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head -5</span></span></code></pre></div><p>掃描後會發現同樣的函式定義散落在多個檔案中。<code>run_git_command</code> 出現在 2 個以上的 Hook 裡，<code>get_current_branch</code>、<code>read_yaml_file</code> 等也有各自的副本。如果修復 <code>run_git_command</code> 的一個 bug，你需要同時改每個有副本的檔案，而且不能漏掉任何一個。</p>
<p>累積到 32 個 Hook 時，技術債務已經大到無法忽視。</p>
<h2 id="第一階段v0280-結構性重構">第一階段：v0.28.0 結構性重構</h2>
<p>第一階段的目標很明確：消除重複、建立結構。我們把工作拆成四個 Wave，每個 Wave 有獨立的交付物和驗證點。</p>
<h3 id="wave-1建立共用程式庫">Wave 1：建立共用程式庫</h3>
<p>先不動任何 Hook 檔案。第一步是把散落各處的重複邏輯抽取到獨立模組，並為每個模組寫測試。</p>
<p>建立的模組：</p>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>職責</th>
          <th>對應的壞味道</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>config_loader</td>
          <td>讀取 YAML 配置</td>
          <td>ARCH-001 硬編碼配置</td>
      </tr>
      <tr>
          <td>git_utils</td>
          <td>封裝 Git 命令</td>
          <td>IMP-001 重複程式碼</td>
      </tr>
      <tr>
          <td>hook_io</td>
          <td>統一 Hook I/O 處理</td>
          <td>IMP-001 重複程式碼</td>
      </tr>
      <tr>
          <td>hook_logging</td>
          <td>統一日誌設定</td>
          <td>IMP-001 重複程式碼</td>
      </tr>
  </tbody>
</table>
<p>為什麼先建程式庫而不是先改 Hook？因為如果直接改 Hook，會遇到雞生蛋問題——Hook A 需要共用函式，但共用函式還沒建立。先建好程式庫並通過測試，後續每改一個 Hook 都有安全網。</p>
<p>建立模組時的關鍵決策：介面設計先於實作。我們先定義每個模組的公開函式簽名，寫測試驗證這些簽名的行為，最後才把各 Hook 中的重複邏輯搬進來。這確保了模組的介面是「為使用者設計」的，而不是「照搬原始碼」的。</p>
<p>這個順序的思考方式在 <a href="/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a> 中有詳細說明。</p>
<h3 id="wave-2配置分離">Wave 2：配置分離</h3>
<p>把 task-dispatch 中的硬編碼清單抽到 YAML 檔案：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># agents.yaml（節錄）</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">agents</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">incident-responder</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">triggers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;test failed&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;compile error&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;runtime error&#34;</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 class="nt">priority</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">system-analyst</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">triggers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;架構&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;設計&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;需求&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nt">priority</span><span class="p">:</span><span class="w"> </span><span class="m">2</span></span></span></code></pre></div><p>這一步的行數縮減最明顯。task-dispatch 中原本有大量的 if-elif 鏈在比對代理人名稱和觸發條件，類似這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 修改前：60+ 行的 if-elif 鏈</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="s2">&#34;test failed&#34;</span> <span class="ow">in</span> <span class="n">message</span> <span class="ow">or</span> <span class="s2">&#34;compile error&#34;</span> <span class="ow">in</span> <span class="n">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">agent</span> <span class="o">=</span> <span class="s2">&#34;incident-responder&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">elif</span> <span class="s2">&#34;架構&#34;</span> <span class="ow">in</span> <span class="n">message</span> <span class="ow">or</span> <span class="s2">&#34;設計&#34;</span> <span class="ow">in</span> <span class="n">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">agent</span> <span class="o">=</span> <span class="s2">&#34;system-analyst&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">elif</span> <span class="s2">&#34;安全&#34;</span> <span class="ow">in</span> <span class="n">message</span> <span class="ow">or</span> <span class="s2">&#34;auth&#34;</span> <span class="ow">in</span> <span class="n">message</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">agent</span> <span class="o">=</span> <span class="s2">&#34;security-reviewer&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># ... 還有 20 幾個 elif</span></span></span></code></pre></div><p>全部變成配置查表後，程式碼只剩下查表邏輯本身：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="n">agents</span> <span class="o">=</span> <span class="n">config_loader</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="s2">&#34;agents.yaml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="n">agent_name</span><span class="p">,</span> <span class="n">config</span> <span class="ow">in</span> <span class="n">agents</span><span class="o">.</span><span class="n">items</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="nb">any</span><span class="p">(</span><span class="n">trigger</span> <span class="ow">in</span> <span class="n">message</span> <span class="k">for</span> <span class="n">trigger</span> <span class="ow">in</span> <span class="n">config</span><span class="p">[</span><span class="s2">&#34;triggers&#34;</span><span class="p">]):</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="n">agent_name</span></span></span></code></pre></div><p>詳細的抽取過程在<a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a>中說明。</p>
<h3 id="wave-3逐檔重構">Wave 3：逐檔重構</h3>
<p>有了共用程式庫和配置檔，開始逐一重構 Hook 檔案。策略是：</p>
<ol>
<li>選擇一個 Hook 檔案</li>
<li>用 <code>from lib.xxx import yyy</code> 替換重複程式碼</li>
<li>跑測試確認行為不變</li>
<li>確認通過後繼續下一個檔案</li>
</ol>
<p>逐檔處理的好處是認知負擔可控——每次只需要理解一個 Hook 的邏輯，不需要同時在腦中處理所有修改。即使最後在同一個 commit 中提交，工作過程中仍然是一個一個檔案獨立驗證的。這就是 Wave 作為安全網的具體體現。</p>
<h3 id="wave-4驗證與收尾">Wave 4：驗證與收尾</h3>
<p>28 個單元測試全部通過。主要檔案的變化：</p>
<table>
  <thead>
      <tr>
          <th>檔案</th>
          <th>重構前</th>
          <th>重構後</th>
          <th>縮減</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>task-dispatch-readiness-check.py</td>
          <td>858 行</td>
          <td>296 行</td>
          <td>-65%</td>
      </tr>
      <tr>
          <td>branch-verify-hook.py</td>
          <td>238 行</td>
          <td>109 行</td>
          <td>-54%</td>
      </tr>
      <tr>
          <td>branch-status-reminder.py</td>
          <td>167 行</td>
          <td>103 行</td>
          <td>-38%</td>
      </tr>
  </tbody>
</table>
<h3 id="第一階段成果">第一階段成果</h3>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>消除重複程式碼</td>
          <td>約 415 行</td>
      </tr>
      <tr>
          <td>新增共用模組</td>
          <td>4 個</td>
      </tr>
      <tr>
          <td>新增單元測試</td>
          <td>28 個</td>
      </tr>
      <tr>
          <td>建立 Error Patterns</td>
          <td>3 個（ARCH-001、IMP-001、IMP-002）</td>
      </tr>
  </tbody>
</table>
<p>第一階段解決了最顯眼的問題：重複和膨脹。但還有更深層的問題藏在下面。</p>
<h2 id="第二階段v0310-品質深化">第二階段：v0.31.0 品質深化</h2>
<p>第一階段建立了結構，但結構內部的品質仍然參差不齊。v0.31.0 的四個連續 Wave 處理的是「統一風格」這個看似簡單但實際上充滿陷阱的任務。</p>
<h3 id="w22統一日誌格式">W22：統一日誌格式</h3>
<p>Hook 的日誌格式不一致。有的用 <code>print</code>，有的用 <code>logging</code>，有的用自訂格式。同樣是輸出一行日誌，你可能看到三種寫法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 風格 A：直接 print</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[</span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2">] Processing ticket </span><span class="si">{</span><span class="n">tid</span><span class="si">}</span><span class="s2">&#34;</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"># 風格 B：logging 模組</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Processing ticket </span><span class="si">{</span><span class="n">tid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 風格 C：自訂 logger</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Processing ticket </span><span class="si">{</span><span class="n">tid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div><p>W22 統一為風格 C，讓所有 Hook 的日誌都通過 <code>hook_logging</code> 模組。這個 Wave 相對順利，因為只涉及輸出格式的統一，不改變程式邏輯。日誌行為改變不影響 Hook 的核心功能。</p>
<h3 id="w23統一錯誤訊息">W23：統一錯誤訊息</h3>
<p>把散落在各 Hook 中的硬編碼錯誤訊息提取到集中的 messages 模組。這對應的是 <a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a> 中「禁止硬編碼字串」的原則。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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="nb">print</span><span class="p">(</span><span class="s2">&#34;Error: ticket not found&#34;</span><span class="p">)</span>       <span class="c1"># Hook A</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;找不到 ticket&#34;</span><span class="p">)</span>                  <span class="c1"># Hook B</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Ticket does not exist&#34;</span><span class="p">)</span>          <span class="c1"># Hook C</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="kn">from</span> <span class="nn">lib.messages</span> <span class="kn">import</span> <span class="n">HookMessages</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">HookMessages</span><span class="o">.</span><span class="n">TICKET_NOT_FOUND</span><span class="p">)</span></span></span></code></pre></div><p>集中管理的好處不只是一致性。如果需要把所有訊息改成中文，只需要改 messages 模組，不需要搜尋散落在幾十個檔案中的字串。</p>
<h3 id="w24統一-logger-初始化風格imp-003-事故">W24：統一 Logger 初始化風格——IMP-003 事故</h3>
<p>這是第二階段最慘痛的一課。</p>
<p>目標很簡單：把所有 Hook 的 <code>logger = setup_hook_logging(...)</code> 從模組級移到 <code>main()</code> 內部。理由是 logger 不該在模組被 import 時就建立——這是 Python 社群的通用最佳實踐。</p>
<p>結果：<strong>7 個 Hook 靜默失敗，41 個函式受影響，至少 2 個 session 沒人發現。</strong></p>
<p>根本原因是<strong>作用域變更</strong>：<code>logger</code> 從全域變數變成 <code>main()</code> 的區域變數後，其他函式無法存取它。Python 的 LEGB 規則決定了 <code>main()</code> 的區域變數對同級的其他函式是不可見的。</p>
<p>更危險的是，<code>run_hook_safely</code> 的頂層例外處理把 <code>NameError</code> 吞掉了——它捕獲所有 <code>Exception</code>，只寫入檔案日誌而不輸出到 stderr 或 stdout。於是使用者端完全看不到任何異常。</p>
<p>這個事故的完整分析在<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a>中。</p>
<p>IMP-003 帶來兩個直接改善：</p>
<ol>
<li><strong>作用域變更檢查清單</strong>：任何涉及變數作用域變更的重構，都必須先用 AST 分析列出所有引用，逐一確認每個函式的存取方式</li>
<li><strong>stderr 輸出</strong>：<code>_log_exception</code> 在寫入檔案日誌後，額外輸出到 stderr，確保 Hook 失敗對使用者可見——再也不會有「靜默失敗」的情況</li>
</ol>
<h3 id="w25修復連鎖問題imp-005">W25：修復連鎖問題——IMP-005</h3>
<p>W22 的模組遷移留下了另一個隱患。把 <code>common_functions.py</code> 從 <code>hooks/</code> 遷移到 <code>hooks/lib/</code> 時，部分 Hook 的 import 路徑沒有同步更新，導致 <code>ModuleNotFoundError</code>。這就是 IMP-005（模組遷移後 Import 路徑未同步更新），影響了 5 個 Hook。</p>
<p>修正流程本身不複雜：更新 import 路徑就好。但問題是遷移時沒有系統性地掃描所有引用——只改了「知道有引用的」檔案，漏掉了幾個不常觸發的 Hook。</p>
<p>這裡學到的教訓是：<strong>批量修正必須機械化</strong>。應該用 grep 或 AST 分析列出所有引用點，再逐一修改並驗證，確認沒有遺漏。手動作業的錯誤率和修改數量成正比。</p>
<h3 id="第二階段成果">第二階段成果</h3>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>統一的風格規範</td>
          <td>日誌格式、錯誤訊息、初始化方式</td>
      </tr>
      <tr>
          <td>新增 Error Patterns</td>
          <td>2 個（IMP-003、IMP-005）</td>
      </tr>
      <tr>
          <td>受影響的事故</td>
          <td>7 Hook 靜默失敗（IMP-003）</td>
      </tr>
      <tr>
          <td>新增防護機制</td>
          <td>stderr 輸出、作用域檢查清單、AST 驗證</td>
      </tr>
  </tbody>
</table>
<p>第二階段的教訓比第一階段更有價值。結構性重構（消除重複、建立模組）相對直接，風險可控。但風格統一涉及的是「改變現有能運作的程式碼」，任何疏忽都可能引入迴歸。</p>
<h2 id="第三階段系統級改善">第三階段：系統級改善</h2>
<p>前兩個階段解決了程式碼層面的問題。第三階段的視角拉高到「系統如何自我保護」。以下按主題分組，部分改善與第二階段穿插進行。</p>
<h3 id="w9progressive-disclosure-精簡">W9：Progressive Disclosure 精簡</h3>
<p>隨著規則、方法論、指南越寫越多，文件系統本身的認知負擔也在增加。一份「並行派發指南」原本 200 行，因為不斷補充場景表、案例、FAQ，膨脹到 600 行。讀者只想知道「怎麼判斷能不能並行」，卻被淹沒在細節中。</p>
<p>W9 做了一次系統性的文件瘦身：</p>
<ul>
<li>主文件只保留核心規則和決策邏輯（判斷標準、流程圖、檢查清單）</li>
<li>詳細說明、範例、模板移到 <code>references/</code> 子目錄</li>
<li>每份主文件的 token 數量縮減 20-40%</li>
<li>需要深入了解時，透過連結跳到 references</li>
</ul>
<p>這不是程式碼重構，但思考方式完全一樣：識別膨脹 → 分析哪些是核心哪些是細節 → 職責分離 → 驗證可讀性。重構的對象不只是程式碼，任何隨時間膨脹的結構化資訊都適用同樣的方法。</p>
<h3 id="w28一致性審查">W28：一致性審查</h3>
<p>對所有 Hook、規則、方法論進行一致性審查。檢查項目包括：</p>
<ul>
<li>命名是否遵循統一規範（例如 Hook 檔名的 kebab-case）</li>
<li>錯誤處理是否都通過 <code>run_hook_safely</code></li>
<li>日誌格式是否統一（W22 的成果是否被維持）</li>
<li>配置是否都從 YAML 讀取（W2 的成果是否被維持）</li>
<li>新 Hook 是否使用共用程式庫（W1 的成果是否被維持）</li>
</ul>
<p>審查的結果是發現了幾個遺漏：v0.28.0 之後新建的 Hook 有部分沒有使用共用程式庫，而是又開始「複製貼上」。開發者說：「我只是從隔壁 Hook 複製了幾行，沒必要引入整個模組。」這正是 v0.28.0 之前所有問題的起點。</p>
<p>這說明<strong>制度化</strong>比一次性重構更重要。如果沒有持續的品質檢查，程式碼會自然退化回混亂狀態。一致性審查不是做一次就結束，它需要成為定期的衛生檢查。</p>
<h2 id="量化總結階段對比">量化總結：階段對比</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>第一階段 (v0.28.0)</th>
          <th>第二階段 (v0.31.0)</th>
          <th>第三階段 (系統級)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>目標</td>
          <td>消除重複、建立結構</td>
          <td>統一風格、深化品質</td>
          <td>系統自我保護</td>
      </tr>
      <tr>
          <td>方法</td>
          <td>抽取模組、配置分離</td>
          <td>風格統一、訊息集中</td>
          <td>文件精簡、一致性審查</td>
      </tr>
      <tr>
          <td>工作量</td>
          <td>Wave 1-4</td>
          <td>W22-W25</td>
          <td>W9、W28</td>
      </tr>
      <tr>
          <td>主要產出</td>
          <td>4 模組、28 測試</td>
          <td>統一風格、2 Error Patterns</td>
          <td>文件瘦身、檢查機制</td>
      </tr>
      <tr>
          <td>事故</td>
          <td>無</td>
          <td>IMP-003（7 Hook 靜默失敗）</td>
          <td>無</td>
      </tr>
      <tr>
          <td>認知負擔變化</td>
          <td>大幅降低（檔案縮減 38-65%）</td>
          <td>中度降低（風格一致）</td>
          <td>間接降低（文件可讀性）</td>
      </tr>
      <tr>
          <td>風險等級</td>
          <td>低（新建模組不影響現有）</td>
          <td>高（修改現有能運作的程式碼）</td>
          <td>低（不涉及程式邏輯）</td>
      </tr>
  </tbody>
</table>
<p>一個重要觀察：<strong>第二階段的風險最高</strong>。第一階段是「加法」（新增模組），第三階段是「非程式碼」（文件調整），而第二階段是「改動現有程式碼」。這正是 IMP-003 發生的背景。</p>
<p>重構的風險不取決於修改的「量」，而取決於修改的「性質」。W24 的每個修改都很小（移動一行 <code>logger = ...</code>），但每個修改都觸及了 Python 作用域這個容易被忽略的基礎機制。</p>
<h2 id="教訓">教訓</h2>
<h3 id="重構是持續過程不是一次性事件">重構是持續過程，不是一次性事件</h3>
<p>v0.28.0 做完時，我們以為重構結束了。結果 v0.31.0 又花了四個 Wave 處理品質問題，後面還有系統級的調整。</p>
<p>程式碼會自然退化。每次新增功能、修復 bug、趕進度，都可能引入新的技術債務。重構不是「做完就好」的專案，而是持續進行的衛生習慣——就像每天刷牙，不是做一次根管治療就可以不刷了。</p>
<h3 id="error-patterns-是知識累積">Error Patterns 是知識累積</h3>
<p>五個 Error Patterns（ARCH-001、IMP-001、IMP-002、IMP-003、IMP-005）不只是問題記錄。它們是團隊的「免疫記憶」：</p>
<ul>
<li><strong>ARCH-001</strong>（硬編碼配置）→ 以後新增配置時，自動想到用 YAML</li>
<li><strong>IMP-001</strong>（重複程式碼）→ 發現重複時，自動想到抽取模組</li>
<li><strong>IMP-002</strong>（魔法數字）→ 看到裸數字時，自動想到具名常數</li>
<li><strong>IMP-003</strong>（作用域迴歸）→ 移動變數定義時，自動想到影響範圍分析</li>
<li><strong>IMP-005</strong>（模組遷移後 Import 路徑未同步更新）→ 搬移模組時，自動想到掃描所有引用點</li>
</ul>
<p>每個 Error Pattern 都有明確的結構：觸發條件、根本原因、檢查清單、防護措施。新成員不需要親身經歷這些事故，讀文件就能獲得防護。這比「口耳相傳」可靠得多——口頭經驗會隨著人員流動而消失，文件化的 Error Pattern 是永久的。</p>
<h3 id="wave-是安全網">Wave 是安全網</h3>
<p>把大型重構拆成 Wave 的好處：</p>
<ol>
<li><strong>獨立驗證</strong>：每個 Wave 結束時都跑完整測試，確認沒改壞東西</li>
<li><strong>可回滾</strong>：如果 Wave 3 出問題，Wave 1 和 2 的成果不受影響</li>
<li><strong>認知管理</strong>：每次只需要理解一個 Wave 的範圍，不需要在腦中同時處理所有修改</li>
<li><strong>進度可見</strong>：每完成一個 Wave 就有具體的交付物，而不是「重構了三天但還沒完成」</li>
</ol>
<p>Wave 2（配置分離）如果和 Wave 3（逐檔重構）合併，認知負擔會超過上限——需要同時思考「配置怎麼設計」和「Hook 怎麼改」。拆開後每次只需要想一件事。</p>
<p>反過來說，Wave 也防止了「過度設計」。如果一開始就試圖設計完美的共用程式庫，可能會花太多時間在抽象設計上。Wave 1 先建立「夠用」的模組，Wave 3 在實際使用時再調整介面。實踐中的回饋比預先設計更可靠。</p>
<h3 id="風格統一比結構重構危險">風格統一比結構重構危險</h3>
<p>第一階段（結構重構）幾乎沒有事故。第二階段（風格統一）出了 IMP-003。</p>
<p>原因是：結構重構主要是「加法」——建立新模組、新測試。現有程式碼的修改量小，改壞的機率低。而風格統一是在「改動能運作的程式碼」，每一行修改都可能引入迴歸。</p>
<p>如果重來，W24 的做法會改成：</p>
<ol>
<li>先寫自動化腳本做 AST 分析，列出每個 Hook 的所有 logger 引用關係</li>
<li>用腳本自動修改函式簽名和呼叫端，而不是手動逐檔改</li>
<li>修改後立刻對每個 Hook 做隔離測試，不是改完全部再測</li>
<li>每改完一個 Hook 就提交一次，而不是改完全部再提交</li>
</ol>
<p>這些改善措施的共同主題是：<strong>縮小每次改動的影響範圍</strong>。一次改一個 Hook 比一次改 16 個 Hook 安全得多。</p>
<h2 id="章節知識地圖">章節知識地圖</h2>
<p>本模組各章對應到重構過程的哪個環節：</p>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>對應的壞味道</th>
          <th>對應的階段</th>
          <th>核心技能</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/python/07-refactoring/refactoring-strategy/" data-link-title="重構的動機與策略" data-link-desc="從 Hook 系統重構經驗出發，學習何時重構、何時不該重構，以及如何將大規模重構拆分成可管理的階段">重構的動機與策略</a></td>
          <td>全部</td>
          <td>起點（為什麼重構）</td>
          <td>認知負擔量化、階段分解</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道識別</a></td>
          <td>所有</td>
          <td>起點（識別問題）</td>
          <td>grep 分析、5 Why</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/dry-principle/" data-link-title="DRY 原則與共用程式庫" data-link-desc="學習識別重複程式碼並建立共用模組，含模組演進與漸進遷移策略">DRY 原則與共用程式庫</a></td>
          <td>IMP-001</td>
          <td>第一階段 W1</td>
          <td>模組抽取、介面設計</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a></td>
          <td>IMP-002, ARCH-001</td>
          <td>第一階段 W2 + 第二階段 W23</td>
          <td>三種硬編碼的系統性消除</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/unified-infrastructure/" data-link-title="大規模統一化重構" data-link-desc="從 44 種不同實作到統一基礎設施：日誌、訊息、風格的三階段漸進式重構">大規模統一化重構</a></td>
          <td>IMP-001, ARCH-001</td>
          <td>第二階段 W22-W24</td>
          <td>三階段統一化、漸進式重構</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/refactoring-pitfalls/" data-link-title="重構陷阱與防護" data-link-desc="三個真實重構事故的共通模式：部分更新問題與系統性防護方法">重構陷阱與防護</a></td>
          <td>IMP-003, IMP-005</td>
          <td>第二階段 W24-W25</td>
          <td>部分更新防護、AST 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a></td>
          <td>IMP-003</td>
          <td>第二階段 W24</td>
          <td>AST 分析、作用域規則</td>
      </tr>
      <tr>
          <td><a href="/blog/python/07-refactoring/non-code-refactoring/" data-link-title="非程式碼的重構" data-link-desc="用 Progressive Disclosure 精簡膨脹的規則文件，文件重構和程式碼重構是同一套思維">非程式碼的重構</a></td>
          <td>文件壞味道</td>
          <td>第三階段 W9</td>
          <td>Progressive Disclosure、文件精簡</td>
      </tr>
      <tr>
          <td>本章（完整案例回顧）</td>
          <td>全部</td>
          <td>全部</td>
          <td>系統性思考、Wave 規劃</td>
      </tr>
  </tbody>
</table>
<p>每一章都可以獨立閱讀，但它們來自同一個持續演進的真實專案。單獨學會「怎麼消除魔法數字」是基礎能力；理解「什麼時候該做、以什麼順序做、做的時候可能出什麼事故」才是重構的完整技能。</p>
<h2 id="重構前後的程式碼對比">重構前後的程式碼對比</h2>
<p>用一段典型的程式碼說明三個階段的累積效果：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># === 起點：v0.28.0 之前 ===</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 直接使用 subprocess，硬編碼分支名稱，魔法數字</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">[</span><span class="s2">&#34;git&#34;</span><span class="p">,</span> <span class="s2">&#34;branch&#34;</span><span class="p">,</span> <span class="s2">&#34;--show-current&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</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="n">branch</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">if</span> <span class="n">branch</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&#34;main&#34;</span><span class="p">,</span> <span class="s2">&#34;master&#34;</span><span class="p">,</span> <span class="s2">&#34;develop&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Error: protected branch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># === 第一階段後：v0.28.0 ===</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 使用共用模組，但訊息仍硬編碼</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.git_utils</span> <span class="kn">import</span> <span class="n">get_current_branch</span><span class="p">,</span> <span class="n">is_protected_branch</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="n">branch</span> <span class="o">=</span> <span class="n">get_current_branch</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">if</span> <span class="n">is_protected_branch</span><span class="p">(</span><span class="n">branch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Error: protected branch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># === 第二階段後：v0.31.0 ===</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># 訊息集中管理，logger 正確初始化</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.git_utils</span> <span class="kn">import</span> <span class="n">get_current_branch</span><span class="p">,</span> <span class="n">is_protected_branch</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.messages</span> <span class="kn">import</span> <span class="n">BranchMessages</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">def</span> <span class="nf">check_branch</span><span class="p">(</span><span class="n">logger</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">branch</span> <span class="o">=</span> <span class="n">get_current_branch</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">if</span> <span class="n">is_protected_branch</span><span class="p">(</span><span class="n">branch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="n">BranchMessages</span><span class="o">.</span><span class="n">PROTECTED_BRANCH</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">EXIT_ERROR</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">return</span> <span class="n">EXIT_SUCCESS</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="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="s2">&#34;branch-verify&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">return</span> <span class="n">check_branch</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span></span></span></code></pre></div><p>認知負擔的變化：</p>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>需要同時理解的概念</th>
          <th>認知負擔指數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>起點</td>
          <td>subprocess API、Git 命令語法、分支名稱列表、退出碼</td>
          <td>8</td>
      </tr>
      <tr>
          <td>第一階段後</td>
          <td>共用函式名稱、退出碼</td>
          <td>4</td>
      </tr>
      <tr>
          <td>第二階段後</td>
          <td>共用函式名稱、訊息常數名稱</td>
          <td>3</td>
      </tr>
  </tbody>
</table>
<p>每一階段都在降低閱讀者需要同時記住的東西。這就是 <a href="/blog/python/00-philosophy/cognitive-load/" data-link-title="認知負擔：程式碼設計的核心目的" data-link-desc="所有設計原則的統一視角：降低閱讀者的認知負擔">序章的認知負擔理論</a> 在實踐中的應用。</p>
<h2 id="小結">小結</h2>
<p>回顧整個過程，重構的節奏是：</p>
<ol>
<li><strong>先解決最痛的問題</strong>（第一階段：重複和膨脹）</li>
<li><strong>再提升內部品質</strong>（第二階段：風格和一致性）</li>
<li><strong>最後建立保護機制</strong>（第三階段：系統級防護）</li>
</ol>
<p>每個階段都需要前一個階段的基礎。沒有共用模組就無法統一風格，沒有統一風格就無法做一致性審查。</p>
<p>而貫穿三個階段的不變原則只有一個：<strong>這段程式碼讓讀者需要同時記住多少東西？</strong> 如果太多，就需要重構。不管是 858 行的單檔、散落各處的錯誤訊息、還是膨脹到 600 行的文件——認知負擔就是重構的指北針。</p>
<hr>
<p>上一章：<a href="/blog/python/07-refactoring/non-code-refactoring/" data-link-title="非程式碼的重構" data-link-desc="用 Progressive Disclosure 精簡膨脹的規則文件，文件重構和程式碼重構是同一套思維">非程式碼的重構</a></p>
<p>回到模組總覽：<a href="/blog/python/07-refactoring/" data-link-title="模組七：重構實戰" data-link-desc="基於 v0.28.0-v0.31.0 重構經驗的程式碼品質改善指南">模組七：重構實戰</a></p>
]]></content:encoded></item></channel></rss>