觸發場景:Flutter 書籍管理 App 的兩份重構評估、相隔一個版本:_executeImport() 88 行被拆成 13 個項目、主函式剩 17 行;executeBatchImport() 約 90 行、評估結論是不拆。同一個團隊、同一條「函式 5-10 行」規範 疑問來源:兩個幾乎同規模的函式、兩個相反的處置、而且讀完兩份記錄會同意兩個都對——那「5-10 行原則」到底在判什麼? 整理目的:把「拆 / 不拆」的實際判準從行數規則裡拆出來、記下兩個 case 各自的診斷依據 本文邊界:素材是該專案 v0.26 的拆分記錄(含事後逐函式評估)與 v0.25.1 的保留評估;「5-10 行」是該專案的 house rule、判準本身不綁定這個數字


拆的 case:五種職責、五層巢狀

_executeImport() 的 88 行不是「一件事寫得長」、是五件事擠在一起:狀態初始化、主迴圈、重複檢測(skip / overwrite / merge / cancel 四種策略的 switch)、儲存、結果統計。巢狀深度五層——for 迴圈裡有 try-catch、裡面有 if、再裡面 switch-case。讀它的人要同時持有五個心智堆疊。

拆完的形狀:主函式 17 行、變成讀得像流程描述的協調者(initialize → process → finalize);12 個子函式全部動詞開頭、各答一個「它在做什麼」(_handleExistingBook_saveNewBook_recordBookError……);最深巢狀從 5 層降到 2 層。

兩個工法值得單獨記。_ImportProgress 輔助類別:拆函式最常見的副作用是子函式之間要傳一長串計數器參數,這裡把進度狀態(成功數、失敗數、處理索引)收進一個 6 行的私有類別、參數列縮成一個物件——拆函式前先看有沒有該收斂的「隱形狀態群」。測試零改動:209 個測試全過、一個都不用改,因為測試斷言的是匯入行為(結果、事件、狀態)而不是內部結構——這也是反向的檢驗:重構會逼你改測試、通常代表測試耦合了結構。

事後評估同樣誠實:13 個項目裡 7 個完全符合五行原則、4 個在 10-15 行(協調函式與 switch 結構、判可接受)、1 個 20 行標「需關注」。拆完也不是教條達標——原則是方向、不是驗收線。

不拆的 case:步驟多、但答案只有一個

executeBatchImport() 約 90 行、同樣超標,評估卻判「不拆、優先級低」。理由寫在記錄裡:

executeBatchImport 雖超過 10 行,但為完整業務流程;已適當拆分子函式(_updateProgress、_rollbackImportedBooks);強制拆分可能降低可讀性,不符合「可讀性優於簡潔性」原則。

看它的結構就懂了:狀態初始化、發布開始事件、然後是一個 40 行的主迴圈——每一輪做取消檢查、暫停等待、處理一本書。它的長度來自流程的步驟數、不是職責的混雜:問「這個函式在做什麼」只有一個答案(執行可中斷的批量匯入)、而且這些步驟的順序與交織正是這個功能的本體——取消檢查必須在每輪迴圈裡、暫停等待必須在處理之前。強拆的結果是把「一眼看完整條流程」變成在五個函式之間跳讀、每跳一次丟一次上下文。

值得補的是它已經拆過該拆的:進度更新跟回滾各自成函式。保留的是流程骨幹、不是偷懶的整坨。

判準:先診斷成因、再決定處置

兩個 case 並排、判準自己浮出來。函式長是症狀,處置取決於成因:

成因診斷訊號處置
職責混雜「在做什麼」有多個答案;巢狀深;段落之間可以獨立理解拆——每個答案一個函式
流程完整答案只有一個、步驟多;步驟的順序與交織是功能本體留——拆掉骨幹反而難讀

輔助判讀有兩個:巢狀深度比行數誠實(5 層巢狀幾乎必然是職責混雜;90 行但迴圈內平鋪、常是流程);試拆測試——想像拆出來的子函式,它有獨立的名字跟意義(_saveNewBook)就該拆,它只能叫 _executeBatchImportPart2 就不該拆。

行數規則的正確角色是觸發器、不是判決:超標的函式值得被看一眼,看的結論可以是拆、也可以是「完整流程、保留、記錄理由」。這個專案把保留理由寫進評估記錄——下一個看到 90 行函式的人讀得到「為什麼它被允許長」,而不是以為前人漏了。

判讀徵兆

  • 函式超長且巢狀超過三層——職責混雜的高機率訊號、優先拆
  • 拆分後子函式之間要傳一長串狀態參數——先收斂成輔助類別、再拆
  • 重構函式結構逼得測試要跟著改——測試耦合了結構、先修測試的斷言對象
  • 「不拆」的決定沒有留下理由——下一輪 review 會重新爭論一次;把「完整業務流程、強拆降可讀性」寫進記錄

相關閱讀