iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護系列 第 11

Day 11|類別低於 100 行就只有一個責任嗎?用高優先逾期通知比較行數上限與內聚拆分

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天我先釐清 WorkItem、DTO 與 EF Entity 各自負責什麼。最後沒有建立第二套 Domain Model,只把指派、完成與逾期狀態轉換收回 WorkItem

物件角色清楚之後,新的問題也浮了出來:WorkItemsController 仍有 205 行,同時處理建立、指派、完成與逾期批次流程。

看到這種 Class,最直覺的做法通常是訂一條規則:超過 100 行就拆。對大量 AI Coding 而言,「每個類別不得超過 100 行」很好下指令,也很好自動驗收。不過,100 行只是我刻意設定的實驗門檻,並不是《無瑕的程式碼 第二版》提出的標準答案。

把 205 行拆成數個不到 100 行的檔案,只能證明它們變短了。若 Agent 為了達標順手新增 Mapper、Result、Service 與 Interface,下一次修改反而可能要追更多地方。

今天的實驗分成兩個階段。第一階段從相同起點產生兩份候選:一份只遵守 100 行上限,另一份依共同狀態、依賴與變更理由決定是否拆分。第二階段再把同一項高優先逾期通知需求交給控制組與兩份候選,各執行三次。

結果很值得注意:三種結構都把功能做對,內聚組的 Fresh Token 與工具呼叫中位數卻都是最高。我最後仍採用內聚候選,因為逾期流程已經有專屬依賴、失敗路徑與獨立變更理由。這次要判斷的是責任邊界,不是替 Token、行數或檔案數找一個總冠軍。

〈整潔的類別〉:行數只能拉警報,責任、內聚與封裝才決定邊界

當 Clean Code 從函式往上走到類別與模組,類別大小仍然重要,只是不能再單靠行數衡量。

Single Responsibility(單一責任)關心的是一組會因相同業務理由一起改變的責任,不以方法數量計算。完整的 SOLID 單一職責原則會留到後面的設計篇再深入討論;今天先聚焦在「哪些程式碼值得待在同一個類別」。

Cohesion(內聚)則檢查類別裡的狀態、方法與依賴,是否共同服務同一個目的。高內聚代表這些內容需要一起理解、測試,也經常因同一個理由修改;名稱看起來相關還不夠。

可以把類別想成辦公桌抽屜。發票、印章與報帳單常在同一個流程出現,放在同一格很合理;把滑鼠、護照和螺絲起子也塞進去,只因為抽屜還沒滿,就會很難找東西。反過來,每個物品各放一個抽屜,也會讓櫃子變多,拿一次東西得來回走動。

所以〈整潔的類別〉不是要求一律拆小,而是提醒我同時檢查幾件事:

  • 類別、模組與檔案不是同一件事,檔案變多不等於責任變清楚。
  • 理想類別會封閉內部實作,保持高內聚、容易測試,並適度分開政策與技術細節。
  • 行數、方法數、依賴數與複雜度都適合拉警報,不能直接替團隊決定邊界。
  • 看見一點未來可能性就建立大量抽象,會形成過度設計。
  • 真實的變更理由已經分開時,也不能拿「避免過度設計」當成永遠不整理的理由。

以 C# 來說,一個類別通常放在一個 .cs 檔案只是慣例,不代表拆檔就完成責任分離。真正的封裝,是讓公開入口說清楚類別提供什麼能力,欄位、查詢細節與輔助方法則留在內部。如此一來,人類與 Agent 都能先閱讀較小的公開表面,再決定是否深入實作。

回到目前的 WorkItemsController,四組入口碰到的資料與依賴並不相同:

方法群 共同狀態/資料 主要依賴 可能的變更理由
建立 Title、Priority、DueAtUtc、CreatedAtUtc DbContext、TimeProvider 建立驗證、預設值、建立契約
指派 Assignee DbContext 指派規則、權限、找不到資料
完成 Status DbContext 完成條件、狀態轉換
逾期 Status、DueAtUtc、Priority DbContext、TimeProvider、NotificationGateway 候選篩選、逾期政策、通知、統計、儲存順序

205 行只告訴我這個 Controller 很長。這張表才指出,逾期流程擁有其他入口沒有的通知 Gateway、批次統計、失敗路徑與副作用順序。它看起來已經像一個獨立責任,但我還不想只靠靜態觀察下結論,後面會用下一項需求直接施壓。

CLEAN 原則:C 提供判斷責任的情境,E 固定拆分與停止條件

延伸到 AI Coding,Agent 可以快速產生拆分方案,卻不一定知道真正的業務變更理由。這次需要 C 與 E 處理兩個不同問題:C 要求 User 提供判斷責任所需的 Repository 情境;E 則把拆分證據、禁止事項與停止條件寫清楚。

C — Context-Aware Code 情境感知:類別邊界要從 Repository 裡找

「Controller 有 205 行」只有尺寸資訊。Agent 還需要知道:

  • 哪些方法共享狀態與依賴?
  • 哪些行為有自己的失敗路徑與副作用順序?
  • 哪些規則在過去或下一項需求中會獨立改變?
  • 哪些 HTTP Contract、Database Schema 與既有行為不能順便更動?

若 User 只提供 100 行上限,Agent 最容易驗收、也最可能優先完成的目標,就是讓每個類別低於 100 行。只有補上逾期流程的真實依賴與變更理由,它才有足夠資訊判斷責任邊界。

E — Explicit Intent and Boundaries 意圖明確:要說清楚拆分證據,也要說清楚何時停止

「請符合單一責任」太抽象;「每個 Class 都要低於 100 行」雖然明確,卻可能把量測指標誤當成設計目的。

比較適合 AI Coding 的指示,應該明列:

  • 什麼證據足以拆出新 Class。
  • Interface、Mapper 與 Result 各自在什麼情況才能新增。
  • 哪些外部行為與資料邊界必須維持。
  • Agent 完成後要回報哪些新增成本。
  • 沒有第二個變更理由,或拆分只會增加跳轉時,要停在原地。

把情境與停止條件交代清楚之後,還要避免另一個陷阱:把可量測的數字直接當成設計答案。

行數、CA1502 與 CA1506 只能發出警報,不能替 Agent 決定怎麼拆

Code Metric(程式碼度量)是把程式結構轉成可觀察數字的方法,例如行數、複雜度或耦合。它很適合找出需要進一步審查的位置,卻無法理解業務責任。

.NET Code Analysis 裡有兩項常被拿來觀察大型 Class 的規則:

  • CA1502 檢查 Cyclomatic Complexity,也就是方法裡有多少條獨立控制路徑。啟用後的預設閾值是 25;在 .NET 10 中這條規則預設停用,專案要明確設定才會回報。
  • CA1506 檢查 Class Coupling(類別耦合),也就是一個型別或成員參考了多少個唯一型別。預設閾值為 Type 95、其他 Symbol 40,並可由 Repository 調整;和 CA1502 一樣,它在 .NET 10 預設不啟用。

CA1502 能提醒我某個方法的分支可能太難理解與測試;CA1506 能提醒我某段程式依賴了很多外部型別。兩者都回答不了「建立 Work Item」與「逾期通知」是否屬於同一個改變理由。

因此,這些數字在本篇只負責觸發下一輪判斷:

行數超標/Complexity 升高/Coupling 增加
                    │
                    ▼
          檢查共同狀態、依賴與變更理由
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
   同一責任的多個步驟      獨立改變的責任群組
      保持靠近            才考慮拆 Class

兩個 50 行的類別仍可能拆錯責任;一個 120 行的類別,也可能只是某個 Use Case 的完整流程。數字可以叫我們停下來看,不能直接接管設計。

實驗為什麼先產生兩份結構,再用同一項後續需求施壓

若我只請 Agent 重構一次,再看結果喜不喜歡,很容易把個人偏好誤寫成 Clean Code 結論。

因此,這次實驗分成兩個階段:

  1. 先改變 User 的拆分判準:從同一個 Commit 產生「每個 Production Class 低於 100 行」與「依內聚、依賴、變更理由決定」兩份候選,觀察 Prompt 如何改變 Agent 的結構選擇。
  2. 再固定一項後續需求:把相同的高優先逾期升級需求,分別交給未拆分控制組、行數上限組與內聚組;每組重跑三次,觀察同一種結構能否穩定把需求放在相同位置。

所有實驗固定使用 Codex GPT-5.6-SOL-HIGH。第二階段重複三次,只觀察相同任務與起點下的輸出一致性,結果不代表其他業務需求或 Repository。

三種結構都從同一份 Controller 行為開始

實驗起點固定為:

  • Commit:6693431f2bc917b4cdeeacaade4b94d3be8a03e0
  • Annotated Tag:day-11-clean-classes-baseline

控制組的 WorkItemsController 整個 .cs 檔案共有 205 個實際文字行,包含 using、XML 文件與空白;若只計算 Class 內非空白、非純註解的內容,約為 169 行。它的呼叫路徑很短,也只注入三個合作物件:

public sealed class WorkItemsController(
    WorkItemsDbContext database,
    INotificationGateway notificationGateway,
    TimeProvider timeProvider) : ControllerBase
{
    public Task<ActionResult<WorkItemResponse>> Create(...);
    public Task<ActionResult<WorkItemResponse>> Assign(...);
    public Task<ActionResult<WorkItemResponse>> Complete(...);
    public Task<ActionResult<ProcessOverdueResponse>> ProcessOverdue(...);
}

它的優點是從 HTTP 入口就能看到流程,缺點是四種入口與逾期批次政策集中在同一個 Class。接著只改變 User 提供的拆分判準。

未拆分控制組的類別責任

圖:控制組把四個 HTTP 入口與逾期政策留在同一個 Controller;路徑短,但責任集中。

實驗情境一:把 100 行寫成唯一主要拆分判準

請先閱讀 AGENTS.md、README、Production Code 與既有測試。
這次只做結構重構,不新增產品功能。

唯一主要拆分判準:
將 src/WorkItems.Api 內每一個 Production Class 的宣告範圍
控制在 100 個實體行以內,從 Class 宣告到結尾大括號都要計算。

不得用 partial class、#region、移除必要 XML 文件,
或把多個 Class 塞進同一行規避上限。
可以依行數限制新增 Class、Interface、Service、Result、Mapper 或檔案。
不要求先建立變更理由或內聚矩陣;
本 Run 刻意觀察固定大小上限會把 Agent 帶到哪裡。

必須保留 HTTP Contract、SQLite Schema、建立、指派、完成、
逾期、重跑、通知失敗、Exception、Cancellation、通知順序
與 EF Core 資料庫端查詢。

不得預先實作下一階段的高優先升級通知,
不得修改測試期待值配合行為漂移。

完成後執行 Restore、Build、Test、Format、git diff --check,
建立一個不 Push 的 Commit,並回報每個 Production Class 行數、
新增型別、重複依賴、主要跳轉、測試與停止理由。

這項指示把數字直接寫進驗收條件。我預期 Agent 會優先讓所有 Class 達標,並觀察它為此新增多少型別與跳轉。

查看行數上限候選的完整 Prompt

實驗情境二:先找內聚與獨立變更理由

請先閱讀 AGENTS.md、README、Production Code 與既有測試。
這次只做結構重構,不新增產品功能。

先建立 Production Class 的方法/狀態/依賴/變更理由矩陣,
再決定是否拆分。

不設行數或方法數上限。
只有當一組方法共享相同狀態或依賴,
並具有可獨立說明、可獨立測試的變更理由時,才拆出新 Class。
若方法只是同一個 Use Case 的不同步驟,優先保持靠近。
只有出現替換、多實作或必要測試邊界時才能新增 Interface。
只有需要隔離資料表示時才能新增 Mapper/Result。
允許判斷現況已足夠內聚而不拆分。

必須保留 HTTP Contract、SQLite Schema、建立、指派、完成、
逾期、重跑、通知失敗、Exception、Cancellation、通知順序
與 EF Core 資料庫端查詢。

不得預先實作下一階段的高優先升級通知,
不得修改測試期待值配合行為漂移。

完成後執行 Restore、Build、Test、Format、git diff --check。
有實際變更才建立 Commit,並回報每個 Class 的變更理由、
新增型別、重複依賴、主要跳轉、保留成本與停止理由。

沒有第二個獨立變更理由,
或拆分只會增加跳轉與重複依賴時停止。

這項指示沒有替 Agent 指定 OverdueWorkItemProcessor。它只提供足以新增 Class 的證據、禁止事項與停止點,讓實際結構成為實驗結果。

查看內聚候選的完整 Prompt

第一階段結果:行數上限拆開四個入口,內聚候選只抽出逾期流程

兩份候選都完成結構重構,方向卻明顯不同。

比較項目 行數上限候選 內聚候選
主要判準 每個 Production Class 低於 100 行 共同狀態、依賴與獨立變更理由
主要結構 四個 Controller、Mapper、Result 保留原 Controller,新增一個逾期 Processor
Production 修改 6 個檔案,新增 204 行、刪除 143 行 3 個檔案,新增 97 行、刪除 74 行
包含測試與驗證的完整 Diff 新增 297 行、刪除 143 行 新增 126 行、刪除 76 行
額外自動規則 ProductionClassSizeTests 固定 100 行上限 沒有固定行數 Gate
新增呼叫跳轉 HTTP Route 分散到四個 Controller 逾期 Action 轉交 Processor

行數上限候選如何滿足 100 行

Agent 把四個 Action 分成四個 Controller:

WorkItemCreationController       61 個實體行
WorkItemAssignmentsController    48 個實體行
WorkItemCompletionsController    37 個實體行
WorkItemsController              81 個實體行,只處理逾期
WorkItemResponseMapper           23 個實體行
DueIncompleteWorkItemProcessingResult  11 個實體行

建立、指派、完成與逾期各自有清楚的 HTTP 入口,這份結構並非毫無價值。若不同入口有各自的授權政策、發布節奏或維護團隊,拆開 Controller 可能相當合理;Mapper 也可能在兩種資料表示真的需要隔離時派上用場。

本次 Repository 沒有這些額外需求。WorkItemResponseMapper 只有一項靜態轉換,Result 也沒有第二個使用者;它們出現的直接原因,是 Agent 需要守住 100 行。完整 Diff 連同測試 Gate 共新增 297 行、刪除 143 行。

查看行數上限候選 Diff

依 100 行上限拆分類別

圖:100 行上限把入口、Mapper 與 Result 分散出去,符合尺寸 Gate,卻新增多個跳轉與型別。

內聚候選如何隔離逾期批次責任

內聚導向 Agent 保留建立、指派與完成入口,只新增一個具體的 OverdueWorkItemProcessor。Processor 在這裡就是集中執行逾期 Use Case 的類別:

public sealed class OverdueWorkItemProcessor(
    WorkItemsDbContext database,
    INotificationGateway notificationGateway,
    TimeProvider timeProvider)
{
    public async Task<ProcessOverdueResponse> ProcessAsync(
        CancellationToken cancellationToken)
    {
        var items = await FindDueIncompleteWorkItemsAsync(cancellationToken);
        var summary = await ProcessDueIncompleteWorkItemsAsync(
            items,
            cancellationToken);

        await database.SaveChangesAsync(cancellationToken);
        return summary;
    }
}

Controller 的逾期 Action 只負責轉交。候選沒有新增 Interface、Mapper、Repository 或新的頂層 Result;它用一個具名 Class 與一次跳轉,集中候選查詢、狀態轉換、通知、失敗統計與儲存順序。

這份結構不是因為多了一個 Processor 名稱就更乾淨,而是這個名稱正好包住其他入口沒有的依賴與失敗路徑。

查看內聚候選 Diff

依內聚拆出逾期批次責任

圖:內聚候選只抽出可命名的逾期批次責任;Controller 負責 HTTP 轉交,Processor 承接查詢、狀態、通知與儲存。

第一階段只確認兩份重構保留了目前已知行為

兩份候選都通過 Release Build、完整測試、格式檢查、Diff 檢查與 API 基本流程驗證,HTTP JSON 與 SQLite Schema 也維持不變。

這些結果表示兩份候選都能維持目前已驗收的行為,還不足以判斷哪一份比較容易承接下一次修改。類別邊界是否合適,必須等新的變更理由進來後再觀察。

為什麼用「高優先項目逾期 24 小時」測試三種結構

第二階段加入同一項需求:PriorityHigh,而且逾期至少 24 小時的 Work Item,改走 SendEscalationAsync

我選這項需求有三個原因:

  1. 它明確屬於逾期政策,不會合理地落在建立、指派或完成流程。
  2. 它同時碰到時間等號邊界、通知入口、重跑、失敗統計與儲存順序,能檢查類別是否保留完整責任。
  3. 它只需要增加一條通知選擇規則,不需要 Strategy、Plugin 或第二套模型;若 Agent 大幅擴充架構,就能看出是結構或指示把它推得太遠。

正式需求固定為:

High 且 DueAtUtc <= currentUtc - 24 小時:走升級通知。
High 但未滿 24 小時:走一般通知。
Normal 即使超過 24 小時:走一般通知。
Overdue 的 High 項目重跑:仍再次升級通知,
但 ProcessedCount 不重複增加。

兩種通知共用既有 Attempt/Failure 計數與失敗 ID 順序。
通知回傳 false:仍儲存 Overdue。
通知拋出 Exception/Cancellation:不儲存。
通知必須先於 SaveChangesAsync。

不得新增 Strategy、Plugin、第二套 Persistence Model、
套件、Migration、新 Endpoint 或 Response 欄位。

查看完整升級通知 Prompt

每種結構重跑三次,只觀察相同任務下的修改一致性

控制組、行數上限組與內聚組各跑三個全新 Session,共九次。每次都從該結構的固定 Commit 建立隔離 Worktree,不沿用前一次對話或修改結果;模型、推理等級、Prompt 與驗收條件保持一致。

單次結果可能受 Agent 搜尋順序或工具選擇影響,三次重跑用來觀察修改位置是否穩定,不代表遇到付款、權限或另一個 Domain 時仍會走相同路徑。

九次實驗都修改逾期規則與通知 Gateway,差別在規則落在哪個類別

九次實驗都只修改兩個 Production 檔案:一個負責逾期規則,一個替通知 Gateway 增加升級入口。

結構 逾期規則的修改位置 三種結構都必須修改的通知入口 每次修改的 Production 檔案 新增 Class/Service/Mapper/Result
控制組 同時處理四種入口的 WorkItemsController NotificationGateway.cs 2 0
行數上限組 只處理逾期的 WorkItemsController NotificationGateway.cs 2 0
內聚組 OverdueWorkItemProcessor NotificationGateway.cs 2 0

三組 Agent 都認為一條明確條件就能決定通知路徑,沒有建立 Strategy、Factory 或 Plugin。三組也都修改兩個正式程式檔案,因此檔案數無法區分設計差異。真正要比較的是:Agent 進入檔案後,要排除多少無關內容,以及新規則是否落在已命名的責任裡。

  • 控制組要進入同時處理四種入口的 Controller,再找出逾期區塊。
  • 行數上限組已經有專用逾期 Controller,修改位置直接,但仍受到 100 行硬門檻約束。
  • 內聚組進入具名 Processor,不受任意尺寸限制,逾期查詢、規則、通知與儲存順序都留在同一責任內。

完成需求後,行數上限組的逾期 Controller 整個 .cs 檔案共有 101 行;但這項 Gate 計算的是 Class 宣告範圍,扣除檔案外的 using 等內容後為 87 行,因此仍符合門檻。未來再多幾段 XML、條件或 Helper,它可能因數字超標再次拆分,即使沒有出現新的變更理由。

內聚組三次的 Processor Class 本體約為 106、88、100 行。數字沒有行數上限組整齊,三份輸出卻都把相同責任留在同一位置。

行數上限回答的是「何時強制拆」,內聚判準回答的是「哪些內容應該一起改」。兩者解決的是不同問題。

九份候選都由主流程重跑建置、測試與 API 行為驗證

我沒有直接採信 Agent 自己說「完成」。九份輸出都由主流程重新執行相同的 Restore、Release Build、完整測試、格式檢查、Commit Diff 檢查與 HTTP 基本流程驗證。

結果是九份候選全部通過,HTTP Contract、資料庫結構、24 小時等號邊界、重跑、一般通知與升級通知混合成功/失敗時的統計、通知順序、未知例外與取消行為都維持要求,也不需要人工修改 Production Code 或測試期待值。

這個驗證讓三組先跨過相同的行為門檻。它仍然沒有替我回答哪一種結構最好,只是避免拿功能不完整的候選和正確候選比較設計。

查看九次壓力實驗與主流程驗證紀錄

內聚候選沒有節省 Token,而且本次資料不足以推論長期效率

把每組三次實驗取中位數後,結果如下。Fresh Token 沿用昨天的計算方式:未命中 Cache 的 Input Token 加上 Output Token。

結構 主流程驗證 Fresh Token 中位數(範圍) Tool Calls 中位數(範圍) 三次 Production 修改路徑種類數
控制組 三次全部通過 159,431(153,222~222,336) 38(35~57) 1 種
行數上限組 三次全部通過 166,257(136,178~171,529) 35(32~38) 1 種
內聚組 三次全部通過 189,130(173,930~247,419) 46(44~57) 1 種

內聚組的 Fresh Token 與工具呼叫中位數都是最高,本次沒有證據支持「內聚拆分會比較省 Token」。

控制組的數值範圍分別與另外兩組重疊;行數上限組的最高值 171,529,則低於內聚組的最低值 173,930。這只能描述本次九個 Session,不能直接建立結構與 Token 的因果關係。

每組只有三次,而且其中兩次還包含設計確認或額外審查等待;快取、工具重試與 Agent 執行流程也都會影響數字。

這張表能支持的結論只有兩個:九次輸出都通過已定義的行為 Gate;本次內聚候選沒有換到較低的 AI 執行成本。類別責任仍要回到 Repository 情境判斷。

這次接受內聚候選,因為逾期流程已經形成可命名的獨立責任

本次選擇的核心問題,是逾期流程是否已經形成值得命名的獨立責任。Fresh Token 最低的是控制組,最容易自動驗收的是行數上限組;這兩項都不能取代共同依賴、失敗路徑與變更理由。

三種結構都完成需求後,我用相同條件重新比較:

Repository 情境 較合適的方向 主要驗收
只有少量簡單入口,沒有獨立規則或專屬依賴 保留控制組 下一次修改能否直接落在原 Class,新增型別是否只增加跳轉
法規、程式碼產生器、教學或審查工具要求固定尺寸 採用行數上限 門檻來源、超標時的拆分規則與例外是否明確
一組方法共享專屬狀態或依賴,且已有獨立變更理由 採用內聚拆分 新 Class 是否對應真實責任,後續需求能否穩定落在同一位置

目前的逾期流程有專屬的時間來源、通知 Gateway、失敗統計、重跑規則與儲存順序;升級通知也只改變這一組政策。獨立變更理由已經實際發生。

行數上限候選同樣把逾期入口拆開,卻額外建立 Mapper、Result 與硬性尺寸 Gate。控制組的結構成本最低,但每次處理逾期需求都得先從四種入口中辨認相關區塊。

內聚候選的成本也很具體:一個 Processor、一次呼叫跳轉,以及一筆相依性注入(Dependency Injection,DI)註冊。相依性注入是由外部把 DbContext、Gateway 等合作物件交給類別,而不是讓類別自行建立它們。這些成本都能追到已存在的逾期責任,而且三次後續實驗都把新規則放回同一個 Processor。

因此,本系列接受:

  • Commit:fc06aa4f02840c0afb5008043b5f219787db8fb9
  • Annotated Tag:day-11-clean-classes

AI Coding 下目前沿用內聚拆分,符合重新評估條件時才改回較簡單結構

這次選擇帶有明確條件。若 Repository 只是簡單 CRUD,逾期流程也沒有專屬規則或依賴,我會保留控制組;若團隊真的受法規、程式碼產生器或審查工具的固定尺寸規範約束,行數上限候選也可能更適合。

未來模型的 Coding 能力變強,確實可能讓控制組重新成為更適合 AI Coding 的方向;不過,不能只憑「模型應該看得懂大 Class」就改變決策。我會重新比較以下四項證據:

  1. Agent 是否能多次把後續需求直接放進正確位置,沒有遺漏同一 Class 裡的其他入口。
  2. 行為 Gate 是否足以攔住跨入口的狀態、例外與副作用漂移。
  3. 減少型別與跳轉後,Agent 的讀取範圍、修改路徑與執行成本是否真的下降。
  4. 原本獨立的依賴、失敗路徑與變更理由,是否已經消失或重新合併。

四項都成立時,控制組較少的型別與跳轉會更有吸引力。反過來說,拆出更多抽象也不會自動提高 Agent 的情境感知;名稱若只剩 ServiceManager 或多層 Wrapper,AI 反而要跨更多檔案還原流程。行數上限則只在外部規範真的存在時採用,不把它當成 Clean Code 或 AI Coding 的通用捷徑。

這次涵蓋一個 API、兩份結構候選,以及一項需求在三種結構上的九次壓力測試。這些資料足以決定本系列下一篇沿用哪個起點,仍不足以替所有專案制定通則。專案規模、類型、測試完整度、人工 Review 比例與模型能力改變時,都應使用相同後續需求重新驗證。

把三種採用條件整理成 Class Cohesion Policy 範例

Class Cohesion Policy(類別內聚規範)是跟著 Repository 保存的共同規則,讓後續 Agent 能重複使用相同的審查訊號、結構選擇、邊界與停止條件。

我不希望每次遇到大型 Class,都由人逐一指定哪三個方法要搬、要建立什麼 Service。因此,我先把今天校準出的判斷方式整理成一段可以放進 AGENTS.md 的範例:

## Class Cohesion Policy

### 審查訊號

- 類別行數、方法數、CA1502 與 CA1506 只用來觸發審查,不得直接當成拆分命令。
- 重構前列出公開入口、共同狀態、外部依賴與可獨立說明的變更理由。

### 結構決策

- Class 只有少量簡單入口,沒有獨立規則、專屬依賴或第二個變更理由時,保留現況。
- 法規、程式碼產生器、教學或審查工具存在硬性尺寸需求時,可以採用行數上限;記錄門檻來源、超標時的拆分規則與例外。
- 一組方法共享狀態或依賴,而且具有可獨立測試的變更理由時,才拆出新 Class。
- 同一個 Use Case 的主要流程與細節 Helper 優先保持靠近,不為縮短檔案增加跳轉。
- 只有出現替換、多實作,或測試時確實需要替換合作物件時才新增 Interface。
- 只有資料表示需要隔離時才新增 Mapper 或 Result,不為搬運相同資料建立新層。

### 邊界與停止條件

- HTTP Contract、Database Schema、錯誤語意、查詢執行位置與副作用順序不得順便修改。
- 拆分後檢查依賴是否重複注入、規則是否複製,以及主要流程增加多少跳轉。
- 沒有第二個獨立變更理由,或新增成本高於隔離價值時,保留現況並停止。
- 模型理解大型 Class 的穩定度、人工逐行 Review 比例或 Token 成本改變時,重新用相同後續需求比較三種方向。

### 完成回報

- 回報每個 Class 的責任、呼叫端、修改位置、新增型別、主要跳轉、行為 Gate 與停止理由。
- 用一項已知的後續需求施壓;先確認行為,再比較結構成本與 AI 執行資料。

這類 Policy 未來抽成獨立 SKILL,會更適合重複使用、版本化與開源。目前先以 AGENTS.md 範例呈現,方便讀者直接帶回自己的 Repository 調整。

如何切換實驗版本與查看完整紀錄

如果你已經 Clone AI-CleanCode-API-Demo,可以切到共同起點:

git fetch origin --tags
git switch --detach day-11-clean-classes-baseline
git rev-parse HEAD

最後一行應顯示:

6693431f2bc917b4cdeeacaade4b94d3be8a03e0

要查看本系列接受版本:

git switch --detach day-11-clean-classes
git rev-parse HEAD

最後一行應顯示:

fc06aa4f02840c0afb5008043b5f219787db8fb9

detached HEAD 只會讓目前工作目錄指向固定 Commit,不會移動你原本的本地 Branch。

完整實驗紀錄使用固定 Commit 連結,避免後續 Branch 更新讓文章引用內容漂移:

類別低於 100 行,只代表通過尺寸檢查

回到開頭,類別低於 100 行,只能證明它通過尺寸 Gate,不能證明責任已經分對。

這次兩份候選很清楚地顯示,User 提供的判準會改變 Agent 的拆分方向:只給尺寸時,它優先讓所有 Class 達標;補上共同狀態、依賴、變更理由與停止條件後,才形成可追溯的責任邊界。

我接受內聚候選,也不是因為它比較省 Token。它在本次 Fresh Token 與工具呼叫中位數反而最高,後續升級通知需求也和另外兩組一樣,都修改兩個正式程式檔案。真正的收益,是讓逾期政策落在已命名、也有專屬依賴與失敗路徑的責任中。

所以本系列接下來沿用內聚版本。這是今天的 Repository 預設,不是永久答案;控制組與行數上限都有明確的重新採用條件。

今天驗證的是結構與最後結果。修改途中是否一次碰太多內容、紅燈能否快速定位,以及失敗時有沒有安全的還原點,還沒有被比較。明天我會讓相同 Agent 分別用直接完成、TDD 與 TCR 三種節奏處理同一項需求,看看抵達相同功能結果的過程會差多少。

參考資料


上一篇
Day 10|WorkItem 要公開資料還是封裝行為?用新增業務種類與新增操作比較三種模型
下一篇
Day 12|AI 寫完再測,和先測再改有什麼差別?比較直接完成、TDD 與 TCR
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言