<?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>Function-Decomposition on Tarragon</title><link>https://tarrragon.github.io/blog/tags/function-decomposition/</link><description>Recent content in Function-Decomposition on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/function-decomposition/index.xml" rel="self" type="application/rss+xml"/><item><title>88 行拆成 13 個函式、90 行決定不拆 — 函式長度是症狀、職責才是診斷</title><link>https://tarrragon.github.io/blog/work-log/flutter_function_decomposition_split_vs_keep/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter_function_decomposition_split_vs_keep/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：Flutter 書籍管理 App 的兩份重構評估、相隔一個版本：&lt;code>_executeImport()&lt;/code> 88 行被拆成 13 個項目、主函式剩 17 行；&lt;code>executeBatchImport()&lt;/code> 約 90 行、評估結論是&lt;strong>不拆&lt;/strong>。同一個團隊、同一條「函式 5-10 行」規範
&lt;strong>疑問來源&lt;/strong>：兩個幾乎同規模的函式、兩個相反的處置、而且讀完兩份記錄會同意兩個都對——那「5-10 行原則」到底在判什麼？
&lt;strong>整理目的&lt;/strong>：把「拆 / 不拆」的實際判準從行數規則裡拆出來、記下兩個 case 各自的診斷依據
&lt;strong>本文邊界&lt;/strong>：素材是該專案 v0.26 的拆分記錄（含事後逐函式評估）與 v0.25.1 的保留評估；「5-10 行」是該專案的 house rule、判準本身不綁定這個數字&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="拆的-case五種職責五層巢狀">拆的 case：五種職責、五層巢狀&lt;/h2>
&lt;p>&lt;code>_executeImport()&lt;/code> 的 88 行不是「一件事寫得長」、是&lt;strong>五件事擠在一起&lt;/strong>：狀態初始化、主迴圈、重複檢測（skip / overwrite / merge / cancel 四種策略的 switch）、儲存、結果統計。巢狀深度五層——for 迴圈裡有 try-catch、裡面有 if、再裡面 switch-case。讀它的人要同時持有五個心智堆疊。&lt;/p>
&lt;p>拆完的形狀：主函式 17 行、變成讀得像流程描述的協調者（initialize → process → finalize）；12 個子函式全部動詞開頭、各答一個「它在做什麼」（&lt;code>_handleExistingBook&lt;/code>、&lt;code>_saveNewBook&lt;/code>、&lt;code>_recordBookError&lt;/code>……）；最深巢狀從 5 層降到 2 層。&lt;/p>
&lt;p>兩個工法值得單獨記。&lt;strong>&lt;code>_ImportProgress&lt;/code> 輔助類別&lt;/strong>：拆函式最常見的副作用是子函式之間要傳一長串計數器參數，這裡把進度狀態（成功數、失敗數、處理索引）收進一個 6 行的私有類別、參數列縮成一個物件——拆函式前先看有沒有該收斂的「隱形狀態群」。&lt;strong>測試零改動&lt;/strong>：209 個測試全過、一個都不用改，因為測試斷言的是匯入行為（結果、事件、狀態）而不是內部結構——這也是反向的檢驗：重構會逼你改測試、通常代表測試耦合了結構。&lt;/p>
&lt;p>事後評估同樣誠實：13 個項目裡 7 個完全符合五行原則、4 個在 10-15 行（協調函式與 switch 結構、判可接受）、1 個 20 行標「需關注」。&lt;strong>拆完也不是教條達標&lt;/strong>——原則是方向、不是驗收線。&lt;/p>
&lt;h2 id="不拆的-case步驟多但答案只有一個">不拆的 case：步驟多、但答案只有一個&lt;/h2>
&lt;p>&lt;code>executeBatchImport()&lt;/code> 約 90 行、同樣超標，評估卻判「不拆、優先級低」。理由寫在記錄裡：&lt;/p>
&lt;blockquote>
&lt;p>executeBatchImport 雖超過 10 行，但為完整業務流程；已適當拆分子函式（_updateProgress、_rollbackImportedBooks）；強制拆分可能降低可讀性，不符合「可讀性優於簡潔性」原則。&lt;/p>&lt;/blockquote>
&lt;p>看它的結構就懂了：狀態初始化、發布開始事件、然後是一個 40 行的主迴圈——每一輪做取消檢查、暫停等待、處理一本書。它的長度來自&lt;strong>流程的步驟數&lt;/strong>、不是職責的混雜：問「這個函式在做什麼」只有一個答案（執行可中斷的批量匯入）、而且這些步驟的順序與交織正是這個功能的本體——取消檢查必須在每輪迴圈裡、暫停等待必須在處理之前。強拆的結果是把「一眼看完整條流程」變成在五個函式之間跳讀、每跳一次丟一次上下文。&lt;/p>
&lt;p>值得補的是它&lt;strong>已經拆過該拆的&lt;/strong>：進度更新跟回滾各自成函式。保留的是流程骨幹、不是偷懶的整坨。&lt;/p>
&lt;h2 id="判準先診斷成因再決定處置">判準：先診斷成因、再決定處置&lt;/h2>
&lt;p>兩個 case 並排、判準自己浮出來。函式長是&lt;strong>症狀&lt;/strong>，處置取決於成因：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>成因&lt;/th>
 &lt;th>診斷訊號&lt;/th>
 &lt;th>處置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>職責混雜&lt;/td>
 &lt;td>「在做什麼」有多個答案；巢狀深；段落之間可以獨立理解&lt;/td>
 &lt;td>拆——每個答案一個函式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>流程完整&lt;/td>
 &lt;td>答案只有一個、步驟多；步驟的順序與交織是功能本體&lt;/td>
 &lt;td>留——拆掉骨幹反而難讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>輔助判讀有兩個：&lt;strong>巢狀深度&lt;/strong>比行數誠實（5 層巢狀幾乎必然是職責混雜；90 行但迴圈內平鋪、常是流程）；&lt;strong>試拆測試&lt;/strong>——想像拆出來的子函式，它有獨立的名字跟意義（&lt;code>_saveNewBook&lt;/code>）就該拆，它只能叫 &lt;code>_executeBatchImportPart2&lt;/code> 就不該拆。&lt;/p>
&lt;p>行數規則的正確角色是&lt;strong>觸發器、不是判決&lt;/strong>：超標的函式值得被看一眼，看的結論可以是拆、也可以是「完整流程、保留、記錄理由」。這個專案把保留理由寫進評估記錄——下一個看到 90 行函式的人讀得到「為什麼它被允許長」，而不是以為前人漏了。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>函式超長且巢狀超過三層——職責混雜的高機率訊號、優先拆&lt;/li>
&lt;li>拆分後子函式之間要傳一長串狀態參數——先收斂成輔助類別、再拆&lt;/li>
&lt;li>重構函式結構逼得測試要跟著改——測試耦合了結構、先修測試的斷言對象&lt;/li>
&lt;li>「不拆」的決定沒有留下理由——下一輪 review 會重新爭論一次；把「完整業務流程、強拆降可讀性」寫進記錄&lt;/li>
&lt;/ul>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>同專案的可讀性家族：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_hof_typedef_readability/" data-link-title="高階函式的適用判準：流程固定、變化點單一且開放——以 Flutter 設定更新比較 typedef 改寫前後" data-link-desc="高階函式的適用判準（流程固定、變化點單一且開放）與裸函式型別 vs typedef 的可讀性取捨並排比較。">typedef 改寫前後比較&lt;/a>——那篇是型別層的可讀性手法、本文是結構層&lt;/li>
&lt;li>「規則是觸發器不是判決」的原則層：&lt;a href="https://tarrragon.github.io/blog/report/keyword-bank-hit-is-candidate-not-verdict/" data-link-title="字句層 review：keyword bank 命中是候選、不是判決" data-link-desc="跑了字句層 grep keyword bank、命中『不是 A 而是 B』、卻把它判成『可接受的反例對照』放行；違規由讀者 catch。揭露 keyword bank 的第二層盲點：偵測（grep 命中）跟判定（這個命中是不是違規）是兩個認知步驟、reviewer 容易把命中合理化成合規而放行。判定準則：否定句構若在『建立核心概念』就改正向、只有在『明示反例段落』才保留。另有一類贅語（訴諸群體『很多人卡在』）連關鍵詞都沒有、keyword bank 結構上抓不到、要靠 reader-simulation 語意 pass。">#149 keyword bank 命中是候選、不是判決&lt;/a>——寫作 lint 與函式長度規則同構：偵測可機械化、判定要看語意&lt;/li>
&lt;li>測試守護重構的前提：&lt;a href="https://tarrragon.github.io/blog/work-log/testing_three_layer_strategy/" data-link-title="192 個測試全過、實機全壞：Mock 遮蔽真實行為的三層測試策略" data-link-desc="unit test 全綠、實機部署後功能整片壞掉。mock-only 策略的結構盲區（text vs binary frame、缺 auth handshake、ANSI 多樣性被 FakeWebSocketChannel 遮蔽），以及分層測試各抓什麼、各遮蔽什麼。">192 個測試全過、實機全壞&lt;/a>——測試耦合行為才有守護力，本文的 209 個零改動測試是正面樣本&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：Flutter 書籍管理 App 的兩份重構評估、相隔一個版本：<code>_executeImport()</code> 88 行被拆成 13 個項目、主函式剩 17 行；<code>executeBatchImport()</code> 約 90 行、評估結論是<strong>不拆</strong>。同一個團隊、同一條「函式 5-10 行」規範
<strong>疑問來源</strong>：兩個幾乎同規模的函式、兩個相反的處置、而且讀完兩份記錄會同意兩個都對——那「5-10 行原則」到底在判什麼？
<strong>整理目的</strong>：把「拆 / 不拆」的實際判準從行數規則裡拆出來、記下兩個 case 各自的診斷依據
<strong>本文邊界</strong>：素材是該專案 v0.26 的拆分記錄（含事後逐函式評估）與 v0.25.1 的保留評估；「5-10 行」是該專案的 house rule、判準本身不綁定這個數字</p></blockquote>
<hr>
<h2 id="拆的-case五種職責五層巢狀">拆的 case：五種職責、五層巢狀</h2>
<p><code>_executeImport()</code> 的 88 行不是「一件事寫得長」、是<strong>五件事擠在一起</strong>：狀態初始化、主迴圈、重複檢測（skip / overwrite / merge / cancel 四種策略的 switch）、儲存、結果統計。巢狀深度五層——for 迴圈裡有 try-catch、裡面有 if、再裡面 switch-case。讀它的人要同時持有五個心智堆疊。</p>
<p>拆完的形狀：主函式 17 行、變成讀得像流程描述的協調者（initialize → process → finalize）；12 個子函式全部動詞開頭、各答一個「它在做什麼」（<code>_handleExistingBook</code>、<code>_saveNewBook</code>、<code>_recordBookError</code>……）；最深巢狀從 5 層降到 2 層。</p>
<p>兩個工法值得單獨記。<strong><code>_ImportProgress</code> 輔助類別</strong>：拆函式最常見的副作用是子函式之間要傳一長串計數器參數，這裡把進度狀態（成功數、失敗數、處理索引）收進一個 6 行的私有類別、參數列縮成一個物件——拆函式前先看有沒有該收斂的「隱形狀態群」。<strong>測試零改動</strong>：209 個測試全過、一個都不用改，因為測試斷言的是匯入行為（結果、事件、狀態）而不是內部結構——這也是反向的檢驗：重構會逼你改測試、通常代表測試耦合了結構。</p>
<p>事後評估同樣誠實：13 個項目裡 7 個完全符合五行原則、4 個在 10-15 行（協調函式與 switch 結構、判可接受）、1 個 20 行標「需關注」。<strong>拆完也不是教條達標</strong>——原則是方向、不是驗收線。</p>
<h2 id="不拆的-case步驟多但答案只有一個">不拆的 case：步驟多、但答案只有一個</h2>
<p><code>executeBatchImport()</code> 約 90 行、同樣超標，評估卻判「不拆、優先級低」。理由寫在記錄裡：</p>
<blockquote>
<p>executeBatchImport 雖超過 10 行，但為完整業務流程；已適當拆分子函式（_updateProgress、_rollbackImportedBooks）；強制拆分可能降低可讀性，不符合「可讀性優於簡潔性」原則。</p></blockquote>
<p>看它的結構就懂了：狀態初始化、發布開始事件、然後是一個 40 行的主迴圈——每一輪做取消檢查、暫停等待、處理一本書。它的長度來自<strong>流程的步驟數</strong>、不是職責的混雜：問「這個函式在做什麼」只有一個答案（執行可中斷的批量匯入）、而且這些步驟的順序與交織正是這個功能的本體——取消檢查必須在每輪迴圈裡、暫停等待必須在處理之前。強拆的結果是把「一眼看完整條流程」變成在五個函式之間跳讀、每跳一次丟一次上下文。</p>
<p>值得補的是它<strong>已經拆過該拆的</strong>：進度更新跟回滾各自成函式。保留的是流程骨幹、不是偷懶的整坨。</p>
<h2 id="判準先診斷成因再決定處置">判準：先診斷成因、再決定處置</h2>
<p>兩個 case 並排、判準自己浮出來。函式長是<strong>症狀</strong>，處置取決於成因：</p>
<table>
  <thead>
      <tr>
          <th>成因</th>
          <th>診斷訊號</th>
          <th>處置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>職責混雜</td>
          <td>「在做什麼」有多個答案；巢狀深；段落之間可以獨立理解</td>
          <td>拆——每個答案一個函式</td>
      </tr>
      <tr>
          <td>流程完整</td>
          <td>答案只有一個、步驟多；步驟的順序與交織是功能本體</td>
          <td>留——拆掉骨幹反而難讀</td>
      </tr>
  </tbody>
</table>
<p>輔助判讀有兩個：<strong>巢狀深度</strong>比行數誠實（5 層巢狀幾乎必然是職責混雜；90 行但迴圈內平鋪、常是流程）；<strong>試拆測試</strong>——想像拆出來的子函式，它有獨立的名字跟意義（<code>_saveNewBook</code>）就該拆，它只能叫 <code>_executeBatchImportPart2</code> 就不該拆。</p>
<p>行數規則的正確角色是<strong>觸發器、不是判決</strong>：超標的函式值得被看一眼，看的結論可以是拆、也可以是「完整流程、保留、記錄理由」。這個專案把保留理由寫進評估記錄——下一個看到 90 行函式的人讀得到「為什麼它被允許長」，而不是以為前人漏了。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>函式超長且巢狀超過三層——職責混雜的高機率訊號、優先拆</li>
<li>拆分後子函式之間要傳一長串狀態參數——先收斂成輔助類別、再拆</li>
<li>重構函式結構逼得測試要跟著改——測試耦合了結構、先修測試的斷言對象</li>
<li>「不拆」的決定沒有留下理由——下一輪 review 會重新爭論一次；把「完整業務流程、強拆降可讀性」寫進記錄</li>
</ul>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>同專案的可讀性家族：<a href="/blog/work-log/dart_hof_typedef_readability/" data-link-title="高階函式的適用判準：流程固定、變化點單一且開放——以 Flutter 設定更新比較 typedef 改寫前後" data-link-desc="高階函式的適用判準（流程固定、變化點單一且開放）與裸函式型別 vs typedef 的可讀性取捨並排比較。">typedef 改寫前後比較</a>——那篇是型別層的可讀性手法、本文是結構層</li>
<li>「規則是觸發器不是判決」的原則層：<a href="/blog/report/keyword-bank-hit-is-candidate-not-verdict/" data-link-title="字句層 review：keyword bank 命中是候選、不是判決" data-link-desc="跑了字句層 grep keyword bank、命中『不是 A 而是 B』、卻把它判成『可接受的反例對照』放行；違規由讀者 catch。揭露 keyword bank 的第二層盲點：偵測（grep 命中）跟判定（這個命中是不是違規）是兩個認知步驟、reviewer 容易把命中合理化成合規而放行。判定準則：否定句構若在『建立核心概念』就改正向、只有在『明示反例段落』才保留。另有一類贅語（訴諸群體『很多人卡在』）連關鍵詞都沒有、keyword bank 結構上抓不到、要靠 reader-simulation 語意 pass。">#149 keyword bank 命中是候選、不是判決</a>——寫作 lint 與函式長度規則同構：偵測可機械化、判定要看語意</li>
<li>測試守護重構的前提：<a href="/blog/work-log/testing_three_layer_strategy/" data-link-title="192 個測試全過、實機全壞：Mock 遮蔽真實行為的三層測試策略" data-link-desc="unit test 全綠、實機部署後功能整片壞掉。mock-only 策略的結構盲區（text vs binary frame、缺 auth handshake、ANSI 多樣性被 FakeWebSocketChannel 遮蔽），以及分層測試各抓什麼、各遮蔽什麼。">192 個測試全過、實機全壞</a>——測試耦合行為才有守護力，本文的 209 個零改動測試是正面樣本</li>
</ul>
]]></content:encoded></item></channel></rss>