<?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>Srp on Tarragon</title><link>https://tarrragon.github.io/blog/tags/srp/</link><description>Recent content in Srp on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 20 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/srp/index.xml" rel="self" type="application/rss+xml"/><item><title>案例文章跟跨公司比較是兩個分析責任</title><link>https://tarrragon.github.io/blog/report/article-srp-split-comparison-from-case/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/report/article-srp-split-comparison-from-case/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於八方雲集案例分析的結構拆分。初版文章同時承擔兩個分析責任：八方自身的商業模式與財報判讀，以及八方與揚秦的跨公司結構比較（八維比較表 + 獲利結構比較表 + 散落在各段的比較句）。拆分後三個效果立即可觀察。限制：觀察來自單一次拆分，但原則適用於所有「個體分析 + 跨個體比較」並存的文章結構。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>個體的深度分析和跨個體的結構比較是兩個獨立的分析責任——各自有不同的讀者需求、不同的擴充方向、不同的資料組織方式。&lt;/strong> 把兩者塞在同一篇文章裡，兩邊都做不深：案例分析的篇幅被比較內容佔用，比較內容被鎖在單一公司的敘事脈絡裡無法獨立擴充。&lt;/p>
&lt;p>拆分後的三個效果：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>案例文章深化&lt;/strong>。八方文章移除比較段後，騰出空間深化營業槓桿的雙面性（正向放大 5.2 倍 + 反向風險）、德州工廠 300 產能 vs 13 間店的閒置壓力、同店來客數 vs 營收成長率的判讀差異、丹堤和芳珍蔬食的定位問題——這些分析在原版被比較表格擠掉了。&lt;/li>
&lt;li>&lt;strong>比較文章可擴充&lt;/strong>。比較篇獨立後，之後加入三商餐飲（7705）或其他加盟母公司只需要在既有比較軸加行，不需要改動任何一篇案例文章。&lt;/li>
&lt;li>&lt;strong>被引用的文章也深化&lt;/strong>。揚秦文章原本用兩段解釋八方的模式來說明毛利率差距；拆分後改為引用比較篇，騰出空間深化揚秦自身的關係人交易判讀方法（年報附註查超秦採購比例 → 比對超秦毛利率 → 判斷改善來源）和展店落差的三面向分析。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="反模式在案例文章裡內嵌完整比較">反模式：在案例文章裡內嵌完整比較&lt;/h2>
&lt;p>案例文章裡出現「跟 X 的比較」段落有兩種情境：&lt;/p>
&lt;p>&lt;strong>比較服務案例的論點&lt;/strong>——用一句「同業 X 的毛利率 36%，差距 9pp」建立參考基準，讓讀者理解本案例的相對位置。這種引用是案例分析的一部分，佔一兩句，不需要拆分。&lt;/p>
&lt;p>&lt;strong>比較本身是獨立的分析&lt;/strong>——用完整的多維比較表、逐維度拆解差距原因、得出結構性結論。這已經不是「引用基準」而是「做比較分析」，承擔的是跨公司比較的分析責任。這種段落嵌在案例文章裡會造成兩個問題：案例文章的讀者被迫讀完比較才能繼續、比較內容的讀者要先進入一家公司的脈絡才能找到比較表。&lt;/p>
&lt;p>判別方式：刪掉比較段落後，案例文章的分析是否仍完整。如果完整，比較段應該獨立成篇。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他原則的關係">跟其他原則的關係&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>原則&lt;/th>
 &lt;th>關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../misplaced-content-needs-route-not-deletion/">#212 SRP 違反是路由訊號&lt;/a>&lt;/td>
 &lt;td>&lt;strong>同一個 SRP 原則在文章結構的應用&lt;/strong> — #212 說段落放錯位置時找出路不刪除、本卡說比較段放在案例裡時拆出去成獨立文章，兩者都是「內容沒錯、位置要調」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../analysis-opening-positions-question-not-narrative/">#229 分析開頭定位問題不講創辦敘事&lt;/a>&lt;/td>
 &lt;td>同一篇文章的同一輪修訂抽出兩個層次的原則 — #229 管引言段的功能、本卡管文章整體的責任邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是內部工具不是章節結構&lt;/a>&lt;/td>
 &lt;td>#141 管章節標題不該暴露 process、本卡管章節內容不該混合兩個分析責任 — 都是「一個 surface 一個功能」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>案例文章出現完整的跨公司比較表（3+ 維度）&lt;/td>
 &lt;td>評估比較段是否能獨立成篇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一段分析在兩篇案例文章中重複（A 文章解釋 B 的模式、B 也解釋 A）&lt;/td>
 &lt;td>比較內容應該只在比較篇出現一次，兩篇案例各自引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想加入第三家公司的比較，但不知道放在哪篇案例文章裡&lt;/td>
 &lt;td>比較內容已經超出任何單一案例的責任——需要獨立的比較篇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉比較段後案例文章的分析仍完整&lt;/td>
 &lt;td>比較段是附加的、不是案例的必要部分——拆出去&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>案例文章的某段分析沒有足夠空間深化&lt;/td>
 &lt;td>檢查是否有其他責任的段落佔了篇幅——可能是嵌入的比較、背景、或 WRAP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於八方雲集案例分析的結構拆分。初版文章同時承擔兩個分析責任：八方自身的商業模式與財報判讀，以及八方與揚秦的跨公司結構比較（八維比較表 + 獲利結構比較表 + 散落在各段的比較句）。拆分後三個效果立即可觀察。限制：觀察來自單一次拆分，但原則適用於所有「個體分析 + 跨個體比較」並存的文章結構。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>個體的深度分析和跨個體的結構比較是兩個獨立的分析責任——各自有不同的讀者需求、不同的擴充方向、不同的資料組織方式。</strong> 把兩者塞在同一篇文章裡，兩邊都做不深：案例分析的篇幅被比較內容佔用，比較內容被鎖在單一公司的敘事脈絡裡無法獨立擴充。</p>
<p>拆分後的三個效果：</p>
<ol>
<li><strong>案例文章深化</strong>。八方文章移除比較段後，騰出空間深化營業槓桿的雙面性（正向放大 5.2 倍 + 反向風險）、德州工廠 300 產能 vs 13 間店的閒置壓力、同店來客數 vs 營收成長率的判讀差異、丹堤和芳珍蔬食的定位問題——這些分析在原版被比較表格擠掉了。</li>
<li><strong>比較文章可擴充</strong>。比較篇獨立後，之後加入三商餐飲（7705）或其他加盟母公司只需要在既有比較軸加行，不需要改動任何一篇案例文章。</li>
<li><strong>被引用的文章也深化</strong>。揚秦文章原本用兩段解釋八方的模式來說明毛利率差距；拆分後改為引用比較篇，騰出空間深化揚秦自身的關係人交易判讀方法（年報附註查超秦採購比例 → 比對超秦毛利率 → 判斷改善來源）和展店落差的三面向分析。</li>
</ol>
<hr>
<h2 id="反模式在案例文章裡內嵌完整比較">反模式：在案例文章裡內嵌完整比較</h2>
<p>案例文章裡出現「跟 X 的比較」段落有兩種情境：</p>
<p><strong>比較服務案例的論點</strong>——用一句「同業 X 的毛利率 36%，差距 9pp」建立參考基準，讓讀者理解本案例的相對位置。這種引用是案例分析的一部分，佔一兩句，不需要拆分。</p>
<p><strong>比較本身是獨立的分析</strong>——用完整的多維比較表、逐維度拆解差距原因、得出結構性結論。這已經不是「引用基準」而是「做比較分析」，承擔的是跨公司比較的分析責任。這種段落嵌在案例文章裡會造成兩個問題：案例文章的讀者被迫讀完比較才能繼續、比較內容的讀者要先進入一家公司的脈絡才能找到比較表。</p>
<p>判別方式：刪掉比較段落後，案例文章的分析是否仍完整。如果完整，比較段應該獨立成篇。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../misplaced-content-needs-route-not-deletion/">#212 SRP 違反是路由訊號</a></td>
          <td><strong>同一個 SRP 原則在文章結構的應用</strong> — #212 說段落放錯位置時找出路不刪除、本卡說比較段放在案例裡時拆出去成獨立文章，兩者都是「內容沒錯、位置要調」</td>
      </tr>
      <tr>
          <td><a href="../analysis-opening-positions-question-not-narrative/">#229 分析開頭定位問題不講創辦敘事</a></td>
          <td>同一篇文章的同一輪修訂抽出兩個層次的原則 — #229 管引言段的功能、本卡管文章整體的責任邊界</td>
      </tr>
      <tr>
          <td><a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是內部工具不是章節結構</a></td>
          <td>#141 管章節標題不該暴露 process、本卡管章節內容不該混合兩個分析責任 — 都是「一個 surface 一個功能」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>案例文章出現完整的跨公司比較表（3+ 維度）</td>
          <td>評估比較段是否能獨立成篇</td>
      </tr>
      <tr>
          <td>同一段分析在兩篇案例文章中重複（A 文章解釋 B 的模式、B 也解釋 A）</td>
          <td>比較內容應該只在比較篇出現一次，兩篇案例各自引用</td>
      </tr>
      <tr>
          <td>想加入第三家公司的比較，但不知道放在哪篇案例文章裡</td>
          <td>比較內容已經超出任何單一案例的責任——需要獨立的比較篇</td>
      </tr>
      <tr>
          <td>刪掉比較段後案例文章的分析仍完整</td>
          <td>比較段是附加的、不是案例的必要部分——拆出去</td>
      </tr>
      <tr>
          <td>案例文章的某段分析沒有足夠空間深化</td>
          <td>檢查是否有其他責任的段落佔了篇幅——可能是嵌入的比較、背景、或 WRAP</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><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>