<?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>Constants on Tarragon</title><link>https://tarrragon.github.io/blog/tags/constants/</link><description>Recent content in Constants 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/constants/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>