<?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>Code-Smell on Tarragon</title><link>https://tarrragon.github.io/blog/tags/code-smell/</link><description>Recent content in Code-Smell on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/code-smell/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>