<?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>DRY on Tarragon</title><link>https://tarrragon.github.io/blog/tags/dry/</link><description>Recent content in DRY on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/dry/index.xml" rel="self" type="application/rss+xml"/><item><title>DRY 原則與共用程式庫</title><link>https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/07-refactoring/dry-principle/</guid><description>&lt;p>上一章：&lt;a href="https://tarrragon.github.io/blog/python/07-refactoring/code-smells/" data-link-title="程式碼壞味道偵測" data-link-desc="從三級分類系統到偵測工具鏈，建立系統化的程式碼品質防線">程式碼壞味道偵測&lt;/a>&lt;/p>
&lt;p>DRY (Don&amp;rsquo;t Repeat Yourself) 是軟體開發的核心原則之一。本章基於 Error Pattern IMP-001，學習如何識別重複程式碼並建立共用模組。後半部分以 v0.31.0 的模組演進和遷移實戰為例，示範共用庫如何隨系統成長持續演進。&lt;/p>
&lt;h2 id="問題背景">問題背景&lt;/h2>
&lt;h3 id="症狀">症狀&lt;/h3>
&lt;p>相同功能在多個檔案中重複實作：&lt;/p>





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





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





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





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





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





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





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





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





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





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





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





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





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





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





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





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





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





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





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 盤點</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">grep -rl <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py <span class="p">|</span> wc -l
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 2. 分批（按事件類型）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 第一批：SessionStart hooks（啟動就能看到）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 第二批：UserPromptSubmit hooks</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 第三批：PreToolUse / PostToolUse hooks</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. 逐批執行，每批完成後 commit</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 4. 全量掃描（不可省略！防止 IMP-005）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">grep -r <span class="s2">&#34;from common_functions import&#34;</span> .claude/hooks/*.py
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 預期輸出：空</span></span></span></code></pre></div></details>
<h2 id="小結">小結</h2>
<ul>
<li>DRY 原則要求每個知識只有單一權威來源，用 <code>grep</code> 識別重複的函式定義</li>
<li>不要過早抽象，至少重複兩次再抽取（Rule of Three）</li>
<li>建立結構清晰的共用程式庫，重構前先寫測試確保行為不變</li>
<li>共用庫隨系統成長持續演進，大規模遷移採用分批策略</li>
<li>模組搬家後必須全量 <code>grep</code> 引用並逐一驗證，防止 IMP-005 陷阱</li>
</ul>
<p>下一章：<a href="/blog/python/07-refactoring/constants-management/" data-link-title="配置分離與常數管理" data-link-desc="學習消除三種硬編碼問題：魔法數字、配置混合、散落訊息">配置分離與常數管理</a></p>
<hr>
<p><em>文件版本：v0.31.1</em>
<em>建立日期：2026-03-04</em></p>
]]></content:encoded></item><item><title>為什麼這個場景適合用高階函式？以 Flutter 設定更新為例，比較 typedef 改寫前後</title><link>https://tarrragon.github.io/blog/work-log/dart_hof_typedef_readability/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/dart_hof_typedef_readability/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心議題&lt;/strong>：高階函式是特定場景的自然解 — 當「流程固定、變化點單一且開放」時，把變化點抽成函式參數最省。要不要用它，由場景特徵決定。本文先論證這個場景為何適合 HOF，再比較同一 pattern 的兩種表達（裸函式型別 vs &lt;code>typedef&lt;/code>）各自的優缺點。
&lt;strong>案例骨幹&lt;/strong>：&lt;code>SettingsController.update(transform)&lt;/code> — 9 個設定欄位共用同一條「取值→算新值→去重→通知」流程，唯一的變化是「改哪個欄位」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-案例一個收函式的設定更新方法">1. 案例：一個收函式的設定更新方法&lt;/h2>
&lt;p>設定有 9 個欄位（字型、顏色、描邊、時間格式、目標螢幕、開機啟動…）。每個欄位變更都要走同一串流程：取當前設定 → 算出新設定 → 比對是否確實改變 → 賦回並通知 UI 重繪。把這串流程封裝成一個方法：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">SettingsController&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">ValueNotifier&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">SettingsModel&lt;/span>&lt;span class="o">&amp;gt;&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="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&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="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&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="p">(&lt;/span>&lt;span class="n">next&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">)&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">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&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="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="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="p">}&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-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">controller&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">copyWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">fillColor:&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">controller&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">((&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">copyWith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">fontSize:&lt;/span> &lt;span class="n">v&lt;/span>&lt;span class="p">));&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>update&lt;/code> 收的這個參數本身是「一個函式」 — 把函式當成可傳遞的值。這就是 higher-order function。&lt;/p>
&lt;h3 id="簽章的型別與名字拆解">簽章的型別與名字拆解&lt;/h3>
&lt;p>這個簽章的關鍵是分清「哪裡是型別、哪裡是名字」。它是一個普通的參數宣告，順序跟常見的 &lt;code>int count&lt;/code>、&lt;code>Color color&lt;/code> 一樣是 &lt;strong>&lt;code>型別 名字&lt;/code>&lt;/strong>，只是這次型別換成了較長的函式型別：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&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="o">//&lt;/span> &lt;span class="err">└────────────&lt;/span> &lt;span class="err">型別（函式型別）────────────┘&lt;/span> &lt;span class="err">└名字┘&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>mutate&lt;/code> 是&lt;strong>這個參數的名字&lt;/strong> — 方法內部靠它指涉傳進來的那個函式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&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="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// ← 用名字 mutate「呼叫」傳進來的函式
&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">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>容易混淆的是型別裡面那個 &lt;code>current&lt;/code>：它和 &lt;code>mutate&lt;/code> 不同層級 — &lt;code>current&lt;/code> 只是函式型別內標記參數的名字，&lt;strong>純文件性&lt;/strong>，寫成 &lt;code>SettingsModel Function(SettingsModel)&lt;/code> 行為完全一樣，只是讓型別讀起來更清楚。換句話說，前半的函式型別規定「這個名字必須是什麼形狀的函式」，最後的 &lt;code>mutate&lt;/code> 則是「這個函式參數叫什麼」。下一節先補 HOF 的基礎，第 4 節再回頭談「前半那串型別裸寫在簽章」造成的閱讀摩擦。&lt;/p>
&lt;hr>
&lt;h2 id="2-higher-order-function-是什麼最小定義">2. Higher-order function 是什麼（最小定義）&lt;/h2>
&lt;p>&lt;strong>把函式當資料處理的函式&lt;/strong> — 接收函式當參數，或回傳函式，符合其一即是。前提是語言把函式視為一等公民（first-class），能像變數一樣傳遞。Dart、JS、Kotlin、Swift 皆成立。&lt;/p>
&lt;p>常見的 &lt;code>list.map((x) =&amp;gt; x*2)&lt;/code>、&lt;code>list.where((x) =&amp;gt; x&amp;gt;0)&lt;/code>、&lt;code>onPressed: () =&amp;gt; ...&lt;/code> 都屬此類。&lt;code>update((s) =&amp;gt; ...)&lt;/code> 是同一家族。&lt;/p>
&lt;hr>
&lt;h2 id="3-為什麼這個場景適合用-hof">3. 為什麼這個場景適合用 HOF&lt;/h2>
&lt;p>這個場景有三個特徵，剛好對上 HOF 的強項 — HOF 適不適用，由這些特徵決定。&lt;/p>
&lt;h3 id="31-流程固定變化點單一">3.1 流程固定、變化點單一&lt;/h3>
&lt;p>9 個欄位的更新，&lt;strong>流程 100% 相同&lt;/strong>（取值、去重、賦回、通知），&lt;strong>唯一差異&lt;/strong>是中間那一步「&lt;code>copyWith(哪個欄位: 值)&lt;/code>」。&lt;/p>
&lt;p>當「共用流程」與「變化點」能這樣切乾淨時，HOF 正好對上這個結構：把固定流程寫死在 &lt;code>update&lt;/code> 裡，把變化點抽成函式參數 &lt;code>transform&lt;/code> 由呼叫端帶入。&lt;code>map&lt;/code> 對「走訪迴圈（固定）+ 元素變換（變化）」做的是同一件事。&lt;/p>
&lt;h3 id="32-模型不可變本來就是current--next">3.2 模型不可變，本來就是「current → next」&lt;/h3>
&lt;p>&lt;code>SettingsModel&lt;/code> 是不可變物件（&lt;code>@immutable&lt;/code> + 全 &lt;code>final&lt;/code>）：要改 &lt;code>fillColor&lt;/code>，得用 &lt;code>copyWith&lt;/code> 產生新副本、再把整個物件替換回去。&lt;/p>
&lt;p>也就是說，不可變模型下的更新，在語意上&lt;strong>就是一個 &lt;code>(current) =&amp;gt; next&lt;/code> 的函式&lt;/strong> — 拿舊值算出新值。用函式參數表達這件事，是最貼合的形狀。&lt;/p>
&lt;h3 id="33-變化點開放難以列舉">3.3 變化點開放、難以列舉&lt;/h3>
&lt;p>「未來會改哪些欄位、怎麼組合」是開放的（可能同時改兩個欄位、可能有條件邏輯）。函式參數能表達任意轉換；若改用「enum 指定欄位 + switch」則被固定的列舉鎖死，每加一種改法都要動 &lt;code>update&lt;/code> 內部。HOF 把「怎麼改」的決定權留在呼叫端，&lt;code>update&lt;/code> 不需要知道。&lt;/p>
&lt;p>反過來說，當「變化集合是封閉的、而且需要被序列化或跨層比對」時，enum + switch 反而較好 — 例如要把「使用者改了哪個欄位」存進 undo 堆疊、或透過網路傳給後端，列舉值是可序列化的資料，閉包不是。本案例的變化點純粹發生在呼叫端、不需要 persist，HOF 才站得住。所以「開放」算不算優點，要跟「變化是否需要被當資料搬運」一起看。&lt;/p>
&lt;blockquote>
&lt;p>判準：&lt;strong>流程固定 + 變化點單一 + 變化開放&lt;/strong> 三者同時成立時，HOF 幾乎總是比「列舉 + 分支」或「複製多個方法」更省。&lt;/p>&lt;/blockquote>
&lt;p>對照反例放進具體場景更清楚。假設一個只有「深色模式開關」單一布林設定的 controller，更新邏輯就是 &lt;code>value = !value&lt;/code>，既沒有共用流程、也沒有開放的變化點 — 這時把它包成收函式的 &lt;code>update&lt;/code>，只是逼讀者解析一串函式型別去做一件 &lt;code>toggleDarkMode()&lt;/code> 就講完的事，抽象成本大於收益。另一種反向情境是：9 個欄位看似共用流程，實際每個的更新路徑各不相同（有的要打 API、有的要寫檔、有的純記憶體），那麼「固定流程」的前提根本不成立，硬抽進 &lt;code>update&lt;/code> 反而把三條不同的路徑塞進同一個殼裡。三條件少一條，具名方法通常更省 — 場景不對時硬用，才是過度設計。&lt;/p>
&lt;hr>
&lt;h2 id="4-原始寫法的優缺點裸函式型別">4. 原始寫法的優缺點（裸函式型別）&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">Function&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">current&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">)&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="kd">final&lt;/span> &lt;span class="n">SettingsModel&lt;/span> &lt;span class="n">next&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mutate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&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">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">next&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">next&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="什麼是函式型別裸寫在簽章">什麼是「函式型別裸寫在簽章」&lt;/h3>
&lt;p>這是整個討論的起點，值得單獨講清楚。把術語拆三個詞：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>函式型別&lt;/strong>：描述「一個函式長什麼樣」的型別，例如 &lt;code>SettingsModel Function(SettingsModel current)&lt;/code> — 收一個 &lt;code>SettingsModel&lt;/code>、回傳一個 &lt;code>SettingsModel&lt;/code>。&lt;/li>
&lt;li>&lt;strong>裸寫&lt;/strong>：把完整型別&lt;strong>整串攤開寫出來&lt;/strong>，沒有先取名包裝（對比「裸數字 / magic number」直接寫 &lt;code>120&lt;/code> 而非具名常數）。&lt;/li>
&lt;li>&lt;strong>在簽章&lt;/strong>：寫在方法的參數列（signature）裡。&lt;/li>
&lt;/ul>
&lt;p>合起來就是：&lt;strong>把那串 &lt;code>SettingsModel Function(SettingsModel current)&lt;/code> 原封不動塞進參數位，而不是先用 &lt;code>typedef&lt;/code> 取個名字再引用。&lt;/strong>&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心議題</strong>：高階函式是特定場景的自然解 — 當「流程固定、變化點單一且開放」時，把變化點抽成函式參數最省。要不要用它，由場景特徵決定。本文先論證這個場景為何適合 HOF，再比較同一 pattern 的兩種表達（裸函式型別 vs <code>typedef</code>）各自的優缺點。
<strong>案例骨幹</strong>：<code>SettingsController.update(transform)</code> — 9 個設定欄位共用同一條「取值→算新值→去重→通知」流程，唯一的變化是「改哪個欄位」。</p></blockquote>
<hr>
<h2 id="1-案例一個收函式的設定更新方法">1. 案例：一個收函式的設定更新方法</h2>
<p>設定有 9 個欄位（字型、顏色、描邊、時間格式、目標螢幕、開機啟動…）。每個欄位變更都要走同一串流程：取當前設定 → 算出新設定 → 比對是否確實改變 → 賦回並通知 UI 重繪。把這串流程封裝成一個方法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">SettingsController</span> <span class="kd">extends</span> <span class="n">ValueNotifier</span><span class="o">&lt;</span><span class="n">SettingsModel</span><span class="o">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>呼叫端只描述「改哪個欄位」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">controller</span><span class="p">.</span><span class="n">update</span><span class="p">((</span><span class="n">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">fillColor:</span> <span class="n">c</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">controller</span><span class="p">.</span><span class="n">update</span><span class="p">((</span><span class="n">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="n">copyWith</span><span class="p">(</span><span class="nl">fontSize:</span> <span class="n">v</span><span class="p">));</span></span></span></code></pre></div><p><code>update</code> 收的這個參數本身是「一個函式」 — 把函式當成可傳遞的值。這就是 higher-order function。</p>
<h3 id="簽章的型別與名字拆解">簽章的型別與名字拆解</h3>
<p>這個簽章的關鍵是分清「哪裡是型別、哪裡是名字」。它是一個普通的參數宣告，順序跟常見的 <code>int count</code>、<code>Color color</code> 一樣是 <strong><code>型別 名字</code></strong>，只是這次型別換成了較長的函式型別：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span>  <span class="n">mutate</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">//</span>          <span class="err">└────────────</span> <span class="err">型別（函式型別）────────────┘</span>  <span class="err">└名字┘</span></span></span></code></pre></div><p><code>mutate</code> 是<strong>這個參數的名字</strong> — 方法內部靠它指涉傳進來的那個函式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>  <span class="c1">// ← 用名字 mutate「呼叫」傳進來的函式
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>容易混淆的是型別裡面那個 <code>current</code>：它和 <code>mutate</code> 不同層級 — <code>current</code> 只是函式型別內標記參數的名字，<strong>純文件性</strong>，寫成 <code>SettingsModel Function(SettingsModel)</code> 行為完全一樣，只是讓型別讀起來更清楚。換句話說，前半的函式型別規定「這個名字必須是什麼形狀的函式」，最後的 <code>mutate</code> 則是「這個函式參數叫什麼」。下一節先補 HOF 的基礎，第 4 節再回頭談「前半那串型別裸寫在簽章」造成的閱讀摩擦。</p>
<hr>
<h2 id="2-higher-order-function-是什麼最小定義">2. Higher-order function 是什麼（最小定義）</h2>
<p><strong>把函式當資料處理的函式</strong> — 接收函式當參數，或回傳函式，符合其一即是。前提是語言把函式視為一等公民（first-class），能像變數一樣傳遞。Dart、JS、Kotlin、Swift 皆成立。</p>
<p>常見的 <code>list.map((x) =&gt; x*2)</code>、<code>list.where((x) =&gt; x&gt;0)</code>、<code>onPressed: () =&gt; ...</code> 都屬此類。<code>update((s) =&gt; ...)</code> 是同一家族。</p>
<hr>
<h2 id="3-為什麼這個場景適合用-hof">3. 為什麼這個場景適合用 HOF</h2>
<p>這個場景有三個特徵，剛好對上 HOF 的強項 — HOF 適不適用，由這些特徵決定。</p>
<h3 id="31-流程固定變化點單一">3.1 流程固定、變化點單一</h3>
<p>9 個欄位的更新，<strong>流程 100% 相同</strong>（取值、去重、賦回、通知），<strong>唯一差異</strong>是中間那一步「<code>copyWith(哪個欄位: 值)</code>」。</p>
<p>當「共用流程」與「變化點」能這樣切乾淨時，HOF 正好對上這個結構：把固定流程寫死在 <code>update</code> 裡，把變化點抽成函式參數 <code>transform</code> 由呼叫端帶入。<code>map</code> 對「走訪迴圈（固定）+ 元素變換（變化）」做的是同一件事。</p>
<h3 id="32-模型不可變本來就是current--next">3.2 模型不可變，本來就是「current → next」</h3>
<p><code>SettingsModel</code> 是不可變物件（<code>@immutable</code> + 全 <code>final</code>）：要改 <code>fillColor</code>，得用 <code>copyWith</code> 產生新副本、再把整個物件替換回去。</p>
<p>也就是說，不可變模型下的更新，在語意上<strong>就是一個 <code>(current) =&gt; next</code> 的函式</strong> — 拿舊值算出新值。用函式參數表達這件事，是最貼合的形狀。</p>
<h3 id="33-變化點開放難以列舉">3.3 變化點開放、難以列舉</h3>
<p>「未來會改哪些欄位、怎麼組合」是開放的（可能同時改兩個欄位、可能有條件邏輯）。函式參數能表達任意轉換；若改用「enum 指定欄位 + switch」則被固定的列舉鎖死，每加一種改法都要動 <code>update</code> 內部。HOF 把「怎麼改」的決定權留在呼叫端，<code>update</code> 不需要知道。</p>
<p>反過來說，當「變化集合是封閉的、而且需要被序列化或跨層比對」時，enum + switch 反而較好 — 例如要把「使用者改了哪個欄位」存進 undo 堆疊、或透過網路傳給後端，列舉值是可序列化的資料，閉包不是。本案例的變化點純粹發生在呼叫端、不需要 persist，HOF 才站得住。所以「開放」算不算優點，要跟「變化是否需要被當資料搬運」一起看。</p>
<blockquote>
<p>判準：<strong>流程固定 + 變化點單一 + 變化開放</strong> 三者同時成立時，HOF 幾乎總是比「列舉 + 分支」或「複製多個方法」更省。</p></blockquote>
<p>對照反例放進具體場景更清楚。假設一個只有「深色模式開關」單一布林設定的 controller，更新邏輯就是 <code>value = !value</code>，既沒有共用流程、也沒有開放的變化點 — 這時把它包成收函式的 <code>update</code>，只是逼讀者解析一串函式型別去做一件 <code>toggleDarkMode()</code> 就講完的事，抽象成本大於收益。另一種反向情境是：9 個欄位看似共用流程，實際每個的更新路徑各不相同（有的要打 API、有的要寫檔、有的純記憶體），那麼「固定流程」的前提根本不成立，硬抽進 <code>update</code> 反而把三條不同的路徑塞進同一個殼裡。三條件少一條，具名方法通常更省 — 場景不對時硬用，才是過度設計。</p>
<hr>
<h2 id="4-原始寫法的優缺點裸函式型別">4. 原始寫法的優缺點（裸函式型別）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">mutate</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="什麼是函式型別裸寫在簽章">什麼是「函式型別裸寫在簽章」</h3>
<p>這是整個討論的起點，值得單獨講清楚。把術語拆三個詞：</p>
<ul>
<li><strong>函式型別</strong>：描述「一個函式長什麼樣」的型別，例如 <code>SettingsModel Function(SettingsModel current)</code> — 收一個 <code>SettingsModel</code>、回傳一個 <code>SettingsModel</code>。</li>
<li><strong>裸寫</strong>：把完整型別<strong>整串攤開寫出來</strong>，沒有先取名包裝（對比「裸數字 / magic number」直接寫 <code>120</code> 而非具名常數）。</li>
<li><strong>在簽章</strong>：寫在方法的參數列（signature）裡。</li>
</ul>
<p>合起來就是：<strong>把那串 <code>SettingsModel Function(SettingsModel current)</code> 原封不動塞進參數位，而不是先用 <code>typedef</code> 取個名字再引用。</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 裸寫：函式型別整串長在簽章裡
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">)</span> <span class="n">mutate</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="o">//</span>          <span class="err">└──────────</span> <span class="err">這一整串就是「裸寫的函式型別」──────────┘</span></span></span></code></pre></div><p>為什麼偏偏是「函式型別」會因為裸寫而卡住，一般型別卻不會？因為 <code>int</code>、<code>Color</code> 這類型別已經是短名稱，裸寫毫無負擔；而函式型別的完整語法 <code>X Function(Y)</code> 較長、巢狀時更難讀，<strong>讀者得當場在腦中解析「這是收什麼、回什麼的函式」</strong>。讀程式碼第一眼卡住的，正是這串裸寫的函式型別 — 它才是這篇要討論「要不要抽 typedef」的真正觸發點。下面的優缺點，都圍繞「裸寫 vs 取名」這個軸展開。</p>
<h3 id="優點">優點</h3>
<ul>
<li><strong>型別就地可見</strong>：函式的形狀（收什麼、回什麼）直接寫在簽章上，讀者不必跳到別處查定義。</li>
<li><strong>零額外宣告</strong>：不需要為了一個參數多定義一個型別別名。</li>
</ul>
<h3 id="缺點">缺點</h3>
<ul>
<li><strong>簽章冗長、語法門檻</strong>：<code>SettingsModel Function(SettingsModel current)</code> 對不熟函式型別語法的人構成解析負擔，一眼難消化。</li>
<li><strong>命名與語境矛盾</strong>：參數叫 <code>mutate</code>（變異／就地修改），但模型不可變、實際是「產生新副本」，名稱會誤導。</li>
<li><strong>缺使用錨點</strong>：簽章沒有範例，第一次用的人不知道該傳什麼形狀的 lambda。</li>
</ul>
<hr>
<h2 id="5-typedef-改寫後的優缺點">5. typedef 改寫後的優缺點</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 設定轉換規則：收當前設定、回傳改好的新設定（通常以 copyWith 實作）。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">typedef</span> <span class="n">SettingsMutator</span> <span class="o">=</span> <span class="n">SettingsModel</span> <span class="n">Function</span><span class="p">(</span><span class="n">SettingsModel</span> <span class="n">current</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// 套用一條「目前設定 → 新設定」的轉換規則 …
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">/// 範例：`controller.update((s) =&gt; s.copyWith(fillColor: c));`
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="n">SettingsMutator</span> <span class="n">transform</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="kd">final</span> <span class="n">SettingsModel</span> <span class="n">next</span> <span class="o">=</span> <span class="n">transform</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">next</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="優點-1">優點</h3>
<ul>
<li><strong>簽章簡潔、概念命名</strong>：<code>SettingsMutator</code> 把函式型別升格成領域詞彙，認知從「解析 <code>X Function(Y)</code>」降到「讀一個名詞」。</li>
<li><strong>命名精準</strong>：<code>transform</code>（轉換）貼合不可變語境，不再暗示就地修改。</li>
<li><strong>有錨點</strong>：doc comment 的範例讓第一次使用者立即知道怎麼傳。</li>
<li><strong>錯誤訊息更易讀</strong>：型別對不上時，編譯器印的是 <code>SettingsMutator</code> 這個名字，而不是整串 <code>SettingsModel Function(SettingsModel)</code>；裸寫版的錯誤訊息會把完整型別攤開，較難一眼定位。</li>
<li><strong>可重用</strong>：同一個 <code>SettingsMutator</code> 型別若日後被多個 API 共用，定義集中一處。</li>
</ul>
<h3 id="缺點-1">缺點</h3>
<ul>
<li><strong>多一層 indirection</strong>：想知道 <code>transform</code> 的確切型別，得跳到 <code>typedef</code> 定義；只看 <code>update</code> 簽章看不到形狀。</li>
<li><strong>多一個命名負擔</strong>：<code>SettingsMutator</code> 本身要取得好；命名不當反而多一層要理解的東西。</li>
<li><strong>對單一用途略顯重</strong>：若這個函式型別只在一處使用，typedef 的「集中重用」優點用不上，只剩「命名」一項收益。</li>
</ul>
<hr>
<h2 id="6-並排比較">6. 並排比較</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>原始（裸函式型別）</th>
          <th>typedef 改寫後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>簽章可讀性</td>
          <td>冗長、需解析語法</td>
          <td>簡潔、讀一個名詞</td>
      </tr>
      <tr>
          <td>型別形狀可見性</td>
          <td>就地可見（優）</td>
          <td>需跳到 typedef 定義（劣）</td>
      </tr>
      <tr>
          <td>命名語意</td>
          <td><code>mutate</code> 與不可變矛盾</td>
          <td><code>transform</code> 貼合</td>
      </tr>
      <tr>
          <td>使用門檻</td>
          <td>無範例</td>
          <td>有範例錨點</td>
      </tr>
      <tr>
          <td>額外宣告成本</td>
          <td>無</td>
          <td>多一個 typedef 要命名/維護</td>
      </tr>
      <tr>
          <td>多處共用時</td>
          <td>各自裸寫、重複</td>
          <td>集中定義、重用</td>
      </tr>
      <tr>
          <td>pattern / 行為</td>
          <td>HOF</td>
          <td>HOF（不變）</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>兩者是同一個 pattern（HOF + ValueNotifier）的兩種表達</strong>。取捨重點在「型別就地可見」對上「簽章簡潔 + 概念命名」—— 當函式型別會被多處使用、或語法門檻造成實際閱讀摩擦時，typedef 划算；若只用一次且團隊熟悉函式型別語法，裸寫也完全合理。</p>
<p>改寫的驗證也印證它停在「表達層」：呼叫端傳 lambda 不依賴參數名 → 零修改；行為不變；全套測試原封不動通過。</p>
<hr>
<h2 id="7-收斂">7. 收斂</h2>
<ul>
<li>HOF 適合的場景特徵：<strong>流程固定 + 變化點單一 + 變化開放</strong>。三者齊備時，把變化點抽成函式參數最省；場景不符（欄位少、流程各異）則具名方法更直白。</li>
<li>不可變模型的更新本質就是 <code>(current) =&gt; next</code>，用函式參數表達是語意上最貼合的形狀。</li>
<li>兩種寫法的取捨：裸函式型別型別就地可見、零宣告；typedef 簽章簡潔、命名成概念、可重用，但多一層 indirection。</li>
<li>選擇依據：函式型別是否多處共用、語法是否造成實際閱讀摩擦。摩擦明顯就抽 typedef，否則裸寫無妨。</li>
</ul>
<blockquote>
<p><strong>延伸</strong>：本文「模型不可變」段是整個 HOF 適配的前提之一。<code>SettingsModel</code> 那種 <code>@immutable</code> + <code>copyWith</code> 結構怎麼產生、以及更好懂的替代路徑，見 <a href="/blog/work-log/freezed-%E7%9A%84%E4%B8%89%E5%B1%A4%E7%B5%90%E6%A7%8B%E8%A7%A3%E5%89%96with_%E4%BB%A5%E5%8F%8A%E6%9B%B4%E5%A5%BD%E6%87%82%E7%9A%84%E6%9B%BF%E4%BB%A3%E8%B7%AF%E5%BE%91/" data-link-title="Freezed 的三層結構解剖：with、_$、以及更好懂的替代路徑" data-link-desc="freezed `class X with _$X implements Y` 的分層結構解剖：`with` 與 `_$` 各自的角色、沒有 freezed 怎麼手做、中間投影物件 vs DTO 直接 implements 的維護取捨。">Freezed 的三層結構解剖</a>。</p></blockquote>
]]></content:encoded></item></channel></rss>