iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

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

Day 14|測試全綠後,怎麼判斷設計已經足夠簡單?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

前天談測試紀律,昨天接著確認整潔的測試與驗收測試能不能真的守住需求。今天,這個系列正式跨過 Part I〈程式碼〉,進入 Part II〈設計〉。

一進入設計,測試全綠就不再是最後答案。

同一個 Agent 交出三份都能建置、測試全綠的版本:一份只新增布林變數,一份抽出具名規則,另一份建立 Policy 與相依性注入(Dependency Injection,DI)。Minimal Structure 使用的 Token 最少;Pattern-ready 加入第二條規則時,甚至不用修改 Processor。

我最後選的,卻是中間那份具名規則。

原因是簡單設計不是「檔案最少」或「擴充點最多」,而是用目前最小的必要結構,替已知的變更留下清楚位置。這次兩條通知規則共享相同資料與政策責任,一個 RequiresEscalationNotification 已經足夠集中意圖;完整 Policy 還沒有第二個獨立生命週期或組合需求可以回收成本。

測試綠燈只負責讓候選取得比較資格;設計模式名稱、Diff 大小與 Token 用量,也都只能提供一部分線索。《無瑕的程式碼 第二版》的〈簡單設計〉接著要回答的是:

測試已經全綠之後,User 要怎麼判斷 AI Agent 留下的設計,對目前需求來說已經足夠簡單?

第二版用四個主題談簡單設計,Kent Beck 再補上必要規模

Simple Design(簡單設計)不是把程式碼壓到最短,而是只保留目前需求需要的結構,同時讓行為受測試保護、意圖容易理解、同一項知識不會散落,而且後續仍然容易修改。

《無瑕的程式碼 第二版》的〈簡單設計〉列出四個正式主題:YAGNI、由測試保護、最大化表達力與最小化重複。Kent Beck 更早期對 Simple Design 的討論,還提出在前面條件成立後,繼續移除沒有溝通或運算用途的類別與方法。

我把這些觀念放回今天的案例,整理成五個審查問題:

  1. 目前真的需要嗎? 每個 Interface、Policy 與組合位置,是否都有已經出現的需求負責?
  2. 行為有測試保護嗎? 測試全綠是所有候選都必須通過的門檻,無法單獨證明哪一份設計最好。
  3. 意圖能不能直接讀出來? 讀者是否看得出何時使用升級通知,不必重新解析一整串布林運算。
  4. 同一項知識有沒有散落? 真正需要消除的是同一條業務知識的重複,而非兩段碰巧長得相似的程式碼。
  5. 還有沒有不必要的結構? 在前四項成立後,再刪掉沒有承擔責任的類別、方法與間接層。

這五題要一起看。檔案最少、抽象最多、設計模式最完整或測試全部通過,都不能單獨代表設計最好。

YAGNI 是 You Aren’t Gonna Need It,提醒我們不要為尚未出現的需求先支付設計成本。它處理的是時機,不是永久禁止抽象:太早建立結構,現在就要付理解與維護成本;真實需求已經出現時,YAGNI 也不能成為拒絕整理的理由。

這點到了 AI Coding 更容易被忽略。Agent 幾分鐘就能生出 Interface、Policy 與 DI,User 後續仍要理解、測試並修改這些結構。Kent Beck 在 The Cost YAGNI Was Never About 把 YAGNI 定位成加入結構的時機問題;生成速度變快,不會自動讓提前建立的抽象變得合理。

就像一家目前只有兩道菜的小店,先蓋好能服務一百間分店的中央廚房,不代表設計比較成熟。可是訂單已經增加,兩道菜也開始共用相同備料規則時,仍堅持所有流程都擠在同一張工作檯,也不會比較簡單。

今天要找的,是目前剛好夠用,而且下一次真實變更來臨時仍容易修改的設計。

實驗起點是一條藏在逾期處理流程裡的通知升級規則

這次的固定起點已經有一條正式行為:

Priority == "High",而且工作項目逾期滿 24 小時時,使用 Escalation Notification;其他到期未完成項目使用一般 Overdue Notification。

這條判斷直接放在 OverdueWorkItemProcessor 的單筆處理流程裡:

var notificationSucceeded = string.Equals(
        workItem.Priority,
        "High",
        StringComparison.Ordinal) &&
    workItem.DueAtUtc <= escalationThresholdUtc
        ? await notificationGateway.SendEscalationAsync(workItem, cancellationToken)
        : await notificationGateway.SendOverdueAsync(workItem, cancellationToken);

這段程式碼與測試都沒有錯。現在要決定的不是要不要修 Bug,而是整理應該停在哪裡:取一個區域變數名稱、抽成具名規則方法,還是直接建立可擴充的 Policy?

為了讓三組實驗真的從同一點出發,我固定了一份 Baseline。Baseline 就是所有候選共用、開始修改前不得漂移的起始版本:

  • Commit:eb95ef658d5ae5081c1be6e667f7d4e7a8399440
  • Annotated Tag:day-14-simple-design-baseline
  • Model:Codex GPT-5.6-SOL-HIGH
  • Reasoning effort:high

讀者不必下載專案才能理解後面的結論;若想重現實驗,可以從公開的 AI-CleanCode-API-Demo 切到同一個起點:

git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
Set-Location AI-CleanCode-API-Demo
git switch --detach day-14-simple-design-baseline

兩階段實驗分別測目前成本與下一次修改成本

只看目前程式碼,很容易把設計審查變成風格投票。有人偏好所有條件留在附近,有人看到業務規則就想抽方法,也有人習慣先建立設計模式。三份候選只要都全綠,每個人都能用自己的偏好解釋結果。

所以我把實驗拆成兩個階段:

  • 第一階段:目前需求的設計成本。 三種 Prompt 都只整理既有的 High 24 小時規則,觀察 Agent 新增多少概念、依賴與組合位置。
  • 第二階段:下一次真實修改的成本。 第一階段完成後,再把事前固定、但沒有提前透露給 Agent 的 72 小時規則交給三份代表候選,觀察修改真正落在哪裡。

模型、Baseline、行為契約、修改範圍與驗證方式全程固定,比較焦點是 User 給出的設計方向。不過,三份 Prompt 也同時改變允許的抽象、禁止事項與停止條件,因此結果屬於三種設計策略的重複觀察,不能解讀成單一句提示詞造成的嚴格因果。

第一階段用三種設計指示,測試 Agent 會在哪裡停止抽象

三組都使用相同 Baseline、模型、測試與修改範圍。每組各執行三個彼此獨立、不沿用前次對話與修改結果的 Codex Session,共九次實驗。每組的第一次執行統一標為 Run 01,並在實驗前就固定為第二階段代表,避免看完結果才挑出最有利的版本。

第二階段再讓三份 Run 01 代表候選各執行三次,共九個新 Session;兩個階段合計十八次正式實驗。

三種實驗情境的共同邊界是:只能修改 src/WorkItems.Api/**/*.cs,不得修改測試、HTTP Contract、Database Schema、套件與文件,也不能改變通知先於 Save、失敗摘要、Exception、Cancellation 與重跑語意。

Minimal StructureIntention-revealing RulePattern-ready 都是我為這次實驗定義的名稱,不是書中的正式分類。我把〈簡單設計〉轉成三種受控輸入,沒有預設優勝順序:

實驗方向 刻意施加的限制 想回答的問題
Minimal Structure 只保留目前需求需要的結構 Agent 能不能適時停手?
Intention-revealing Rule 集中規則並替意圖命名 一個具名入口是否已經足夠?
Pattern-ready 預先建立擴充邊界 提前抽象要付出多少成本?

共同測試與行為 Gate 先落實 N — Non-Surprising Behavior 符合預期,排除偷偷改變系統答案的候選。N 在這裡只負責取得比較資格;今天主要使用 L — Localized Change 局部變更,檢查規則與組裝責任最後落在哪裡。

Minimal Structure:測試 Agent 能不能在目前需求停手

整理 OverdueWorkItemProcessor 目前的通知路由判斷,讓 High 且逾期滿
24 小時使用 Escalation、其他到期未完成項目使用一般 Overdue
Notification 的意圖更容易讀懂。

- 保留完成目前需求所需的最少概念。
- 不新增 Interface、Policy、Strategy、Factory、Resolver、Collection
  或 Plugin 體系。
- 可以保留直接條件式,也可以在同一個類別內建立必要的具名方法。
- 如果目前結構已經足夠簡單,可以不修改 Production Code,並說明停止理由。
- 不為尚未提供的未來規則建立擴充點。

這項任務主要測試 YAGNI。它仍允許區域變數、同類別具名方法,甚至可以判斷目前程式碼已經足夠清楚而完全不修改。我要用它作為結構成本較低的對照,再觀察第二階段新增時間門檻時,規則會不會沿著處理流程繼續傳遞。

Intention-revealing Rule:測試一個具名規則能否說清楚業務意圖

整理 OverdueWorkItemProcessor 目前的通知路由判斷,讓 High 且逾期滿
24 小時使用 Escalation、其他到期未完成項目使用一般 Overdue
Notification 的業務意圖能由名稱與結構直接讀出。

- 將「是否使用 Escalation」整理成一個有清楚名稱與單一責任的規則位置。
- 可以使用同類別內的具名方法,或新增一個小型且內聚的規則型別。
- 不建立 Factory、Resolver、Plugin 掃描、規則集合或多層 Strategy 體系。
- Priority、DueAtUtc 與目前時間要容易追蹤。
- 不為尚未提供的未來規則預留方法、列舉或空介面。

這項任務聚焦最大化表達力。Prompt 不指定方法名稱與逐行實作,只要求規則形成一個清楚的語意入口,並把設計停在完整設計模式之前。

我要觀察的是:呼叫端是否更容易讀、第二條規則能否集中修改,以及多一次方法跳轉換來的表達力是否值得。

Pattern-ready:測試提前建立擴充邊界要付出多少成本

把 OverdueWorkItemProcessor 目前的 Escalation Notification 判斷整理成
明確的 Policy/Strategy 擴充邊界。現在只有一條規則:High 且逾期滿
24 小時使用 Escalation。

- 建立能承接未來額外 Escalation 規則的抽象邊界。
- 規則判斷不得繼續散落在 Processor 的通知呼叫條件式裡。
- 使用 ASP.NET Core DI 組合依賴,不使用 Service Locator、Reflection 掃描
  或新增套件。
- 目前只有一條規則,不得虛構第二條規則、通知 Provider 或設定格式。
- 說明新增 Interface、實作、集合或組合點各自承擔的責任。

Strategy Pattern(策略模式)讓呼叫端能在執行期間選擇遵守相同契約的演算法。Pattern-ready 則是我為這次實驗定義的廣義方向,Prompt 同時允許 Policy 或 Strategy;它不代表 Agent 最後一定會產生經典 Strategy Pattern。書中的〈簡單設計〉也沒有要求這次一定採用它。

Prompt 要求 Agent 建立 Policy/Strategy 邊界與 ASP.NET Core DI,卻不允許虛構第二條規則、通知 Provider 或設定格式。這會迫使 Agent 在需求仍少時,自行決定 Interface、規則集合、生命週期與相容入口,正好能測出 User 提早要求擴充性時,留下多少設計空白要由模型補完。

Prompt 裡還刻意排除幾種組裝機制:

機制 它負責什麼 本次為何排除
Factory 集中建立物件,隱藏複雜的建構流程 目前沒有複雜的建構需求
Resolver 依名稱、設定或執行期條件選擇實作 會多出執行期選擇問題
Service Locator 讓程式碼自行向容器索取依賴 會隱藏物件真正需要的相依性
Reflection 掃描 在執行時尋找符合條件的型別 會加入動態發現與額外除錯成本
Plugin 載入 讓外部擴充在執行時接入系統 需要處理版本、生命週期與部署問題

這些做法都有適合的情境。今天先排除,是為了把比較範圍留在規則本身的放置方式。

三組都使用前文介紹過的事前 Oracle,避免 Agent 只靠自己新增或修改的測試宣告完成。完整 Prompt、Oracle 與原始 Session 紀錄都保留在 Day 14 公開實驗資料

第一階段結果:全綠候選分別留下區域變數、具名方法與完整 Policy

九次候選都通過相同的既有行為 Oracle 與主流程驗證。這代表三種方向都守住目前答案,可以進入設計比較;綠燈本身沒有替任何一組加分。

事前固定的 Run 01 代表結果如下。正文先聚焦設計差異,完整檔案數與 Diff 行數留在公開實驗資料:

設計方向 規則放在哪裡 讀者需要理解的概念 立即成本
Minimal Structure 原方法裡的 requiresEscalationNotification 區域變數 單筆處理流程與條件式 沒有新增型別,規則仍屬於流程細節
Intention-revealing Rule 同一類別的 RequiresEscalationNotification 方法 一個具名規則與一次方法跳轉 新增一個可搜尋的業務語意入口
Pattern-ready Rule Interface、規則實作與 Policy Interface、規則集合、DI 與組合方式 現在就要維護完整擴充邊界

Minimal Structure 把難讀的三元條件改成有名稱的區域變數:

var requiresEscalationNotification = string.Equals(
    workItem.Priority,
    "High",
    StringComparison.Ordinal) &&
    workItem.DueAtUtc <= escalationThresholdUtc;

var notificationSucceeded = requiresEscalationNotification
    ? await notificationGateway.SendEscalationAsync(workItem, cancellationToken)
    : await notificationGateway.SendOverdueAsync(workItem, cancellationToken);

它的優點很直接:不用跳轉、不新增型別,閱讀通知流程時就能看到判斷。若這條規則長期不會成長,這可能已經是最好的答案。

Intention-revealing Rule 則把規則移到具名方法:

private static bool RequiresEscalationNotification(
    string priority,
    DateTimeOffset dueAtUtc,
    DateTimeOffset currentUtc) =>
    string.Equals(priority, "High", StringComparison.Ordinal) &&
    dueAtUtc <= currentUtc.AddHours(-24);

呼叫端只留下「是否需要 Escalation」這個決策名稱,規則需要的三項資料仍能從參數讀出。代價是多一次方法跳轉,好處是得到一個能直接搜尋的業務規則位置。

Pattern-ready 新增的結構最多:

public interface IEscalationNotificationRule
{
    bool IsSatisfiedBy(WorkItem workItem, DateTimeOffset currentUtc);
}

public sealed class HighPriorityAtLeast24HoursOverdueRule
    : IEscalationNotificationRule
{
    public bool IsSatisfiedBy(WorkItem workItem, DateTimeOffset currentUtc) =>
        string.Equals(workItem.Priority, "High", StringComparison.Ordinal) &&
        workItem.DueAtUtc <= currentUtc.Subtract(TimeSpan.FromHours(24));
}

public sealed class EscalationNotificationPolicy(
    IEnumerable<IEscalationNotificationRule> rules)
{
    public bool RequiresEscalation(WorkItem workItem, DateTimeOffset currentUtc) =>
        rules.Any(rule => rule.IsSatisfiedBy(workItem, currentUtc));
}

Pattern-ready 把規則從 Processor 隔離,也讓下一條規則有固定的加入方式。代價是新增 Interface、集合、Policy、DI 註冊與相容建構入口,這些程式碼現在就要閱讀、驗證與維護。

Run 01 實際建立的是 Rule 集合與 Policy:Policy 依序檢查多條規則,只要其中一條成立便使用升級通知。它比較接近規則集合的聚合,而不是從多個 Strategy 中選出一個執行。

同一份「請做成可擴充架構」,三次 Agent 仍補出了不同邊界

Pattern-ready 的三次執行都走向 Policy,但 Run 01、Run 02 與 Run 03 分別新增 93/64/50 行,刪除 12/10/10 行,Interface、規則組合與相容入口也不一致。

這次能確認的是:當 User 只要求「建立可擴充邊界」,卻沒有提供第二種使用情境、規則生命週期或組合需求時,Agent 需要自行補完架構。

這項結果不能推論 Pattern 本身不穩定,只能說明目前 Prompt 留下的設計空白太多。可擴充不是一句目的就能完成,還得先說清楚誰要擴充、何時組合、由誰部署。

第二階段用相同的 72 小時規則,檢查每份設計要修改哪裡

第一階段只告訴 Agent 現有的 High 24 小時規則。完成後,我才公開事前固定的第二條需求:不分 Priority,逾期滿 72 小時都改走升級通知。

我選這條規則,是因為它和原本規則共享「逾期時間決定通知路由」的業務知識,卻不依賴 Priority。若第一階段找到的邊界合理,第二條規則應該能落在同一個知識位置;如果邊界切錯,修改就會往方法參數、組合入口或其他無關位置擴散。

同一份任務指令交給三份 Run 01 代表候選,各跑三個新 Session:

在目前 High 且逾期滿 24 小時使用 Escalation Notification 的規則之外,新增:

不分 Priority,工作項目逾期滿 72 小時時,都使用 Escalation Notification。

邊界與回歸行為:

- Normal 剛好逾期 72 小時,使用 Escalation。
- Normal 距離 72 小時仍差 1 ms,使用一般 Overdue Notification。
- High 剛好逾期 24 小時,維持 Escalation。
- 已是 Overdue 的項目重跑時,仍依相同規則再次通知,ProcessedCount 不重複增加。
- 通知回傳 false 時,失敗摘要與最終 Overdue 狀態維持既有語意。

先閱讀目前候選結構,再以最符合該結構的局部修改完成需求。
不得整包改寫目前設計,也不得換成另一種候選方向。

第二階段結果:Pattern-ready 不用修改 Processor,卻要同步兩個規則組裝位置

第二階段的九份候選,都通過 72 小時等號、少 1 ms、High 24 小時、重跑、通知失敗與既有行為等六類驗證。Run 01 的修改如下:

設計方向 正式程式碼修改位置 新增測試要保護什麼 Agent 必須追蹤的內容 新增的結構成本
Minimal Structure Processor 的兩層方法簽章與區域條件 72 小時等號與少 1 ms 邊界 兩個時間門檻如何一路傳到通知判斷 方法參數與條件持續增加
Intention-revealing Rule 既有 RequiresEscalationNotification 相同時間邊界與既有 24 小時規則 一個具名方法內的兩條條件 一次方法跳轉,沒有新增組合位置
Pattern-ready 新規則類別與兩個組裝位置 新 Rule 是否真的參與通知決策 Rule 實作、正式 DI 註冊與基準建構入口 Processor 不變,但組裝責任增加

三組都必須為相同的 72 小時契約新增測試,這部分屬於共同驗證成本。接下來比較設計差異時,我只看 Production Code 的修改位置;完整測試 Diff 仍保留在公開實驗資料。

Minimal Structure 沒有新增型別,但兩個門檻開始沿著方法簽章往下傳:

var processingSummary = await ProcessDueIncompleteWorkItemsAsync(
    dueIncompleteWorkItems,
    currentUtc.AddHours(-24),
    currentUtc.AddHours(-72),
    cancellationToken);

最後再組合成:

var requiresEscalationNotification =
    workItem.DueAtUtc <= allPrioritiesEscalationThresholdUtc ||
    (string.Equals(workItem.Priority, "High", StringComparison.Ordinal) &&
     workItem.DueAtUtc <= highPriorityEscalationThresholdUtc);

這份程式碼通過全部測試,但 Processor 的流程方法現在要傳遞兩個業務門檻。Minimal 確實沒有新增型別,業務知識卻開始變成多層方法都要知道的參數;第三條規則若沿用相同做法,條件與簽章還會繼續成長。

Intention-revealing Rule 的呼叫端完全不用改,Agent 只擴充既有規則方法:

private static bool RequiresEscalationNotification(
    string priority,
    DateTimeOffset dueAtUtc,
    DateTimeOffset currentUtc)
{
    var reachedPriorityIndependentEscalationThreshold =
        dueAtUtc <= currentUtc.AddHours(-72);
    var reachedHighPriorityEscalationThreshold =
        string.Equals(priority, "High", StringComparison.Ordinal) &&
        dueAtUtc <= currentUtc.AddHours(-24);

    return reachedPriorityIndependentEscalationThreshold ||
           reachedHighPriorityEscalationThreshold;
}

兩條規則仍放在同一個方法,但名稱、資料與判斷都集中。現在要理解通知路由,不必追蹤兩個時間門檻如何穿過多層方法,也不用先理解 DI 與規則集合。

Pattern-ready 的 Processor 在第二階段完全不需要修改。Agent 新增 AtLeast72HoursOverdueRule,再把它加入 DI:

builder.Services.AddScoped<
    IEscalationNotificationRule,
    AtLeast72HoursOverdueRule>();

Composition Root(組合根)是應用程式集中把 Interface 與具體實作組裝起來的位置;這個 ASP.NET Core API 的正式組合根是 Program.cs。加入 AtLeast72HoursOverdueRule 時,Agent 不用修改 Processor,卻必須在 Program.cs 註冊新 Rule。

候選還保留原本的三參數相容建構式。這個入口會呼叫 EscalationNotificationPolicy.CreateSeriesBehaviorBaseline(),在沒有 Host 與 DI Container 的情況下手動建立預設 Policy。它不是第二個正式應用程式組合根,卻是另一個實際組裝規則的地方;新增 Rule 時,也必須同步修改這個 Helper。

所以「Processor 零修改」不是完整成本。規則雖然被隔離,組裝知識卻分散到兩個位置;只看核心類別的 Diff,反而會漏掉真正的維護點。

最小設計、意圖揭露與預先模式化在加入 72 小時規則後的修改成本比較

圖:三份設計在第一階段都能通過測試,直到加入 72 小時規則,才看得出修改落點、新增概念與重工成本。

Minimal 使用的 Token 最少,但生成成本不能直接代表設計品質

每個階段、每種設計方向都有三個獨立 Session。下表每一列都是該階段三次執行的平均值;兩個階段合計十八次正式實驗。Fresh Input Token 沿用前文定義,表示扣除快取後重新讀入的輸入量,Tool Calls 則是 Agent 呼叫檔案、終端機與其他工具的次數。

階段 設計方向 Fresh Input Token Output Token Tool Calls
整理目前規則 Minimal Structure 65,856 7,769 23.3
整理目前規則 Intention-revealing Rule 77,267 9,665 28.3
整理目前規則 Pattern-ready 97,044 17,383 33.7
加入 72 小時規則 Minimal Structure 85,781 10,498 30.7
加入 72 小時規則 Intention-revealing Rule 96,946 12,947 37.3
加入 72 小時規則 Pattern-ready 106,131 12,997 43.7

Minimal Structure 在兩個階段的 Fresh Input Token 與 Tool Calls 都最低,Pattern-ready 最高。Pattern-ready 同時新增較多檔案、型別、DI 與組裝位置,這和較高的 Fresh Input Token、Output Token 與 Tool Calls 一起出現。

不過,Session 的探索路徑、Cache、工具選擇與模型輸出都會波動。這次只能記錄相關現象,不能把額外結構視為唯一原因,也不能換算成「少一個 Interface 固定省多少 Token」。

我只會在候選通過相同品質 Gate 後,用 Token 比較工作成本。如果兩份設計同樣容易理解、同樣容易修改,而且風險相近,我會選擇成本較低的版本。

這次 Minimal 雖然最省,第二條規則加入後,兩個業務門檻已經開始穿過 Processor 的多層方法。它節省的是這兩次 Agent 執行成本,沒有替業務知識建立更穩定的修改位置,因此不能只靠 Token 決定。

本次採用具名規則,另外兩種方向保留各自的適用情境

本系列接下來沿用 Intention-revealing Rule 的第二階段 Run 01:

  • Commit:fb22f445f383c7977dfac71f35cc028b0ca53e00
  • Annotated Tag:day-14-simple-design

讀者可以直接切換:

git switch --detach day-14-simple-design

也可以查看 Baseline 與接受版本的完整 Diff

這個選擇來自目前 Repository 的需求與修改結果,沒有把其中一種方向宣布成通用答案:

設計方向 本次決定 適合採用的情境
Minimal Structure 不作為系列接續點 規則短、變動少,而且在流程附近就能完整理解
Intention-revealing Rule 本次採用 已知規則共享資料、政策責任與變更理由,需要一個集中語意入口
Pattern-ready 目前不採用 規則需要獨立替換、不同生命週期、執行期組合、團隊分工或部署

現在兩條規則都在回答同一件事:「這筆工作項目是否該改走升級通知?」它們讀取相同的 PriorityDueAtUtccurrentUtc,也會因為同一類通知政策而一起改變。

RequiresEscalationNotification 正好替這個問題提供明確落點。第二階段加入 72 小時規則時,呼叫端、方法簽章與處理流程都不用修改;Agent 只要找到這個具名方法,就能看見現有規則並加入新判斷。

這個名稱同時服務人類與 AI:先告訴讀者程式碼正在決定什麼,再讓他進去理解 24 小時與 72 小時的細節。

它比 Minimal 多付出一次方法跳轉,換到穩定的業務語意與修改位置;同時省下 Pattern-ready 的新型別、規則集合、註冊與組裝成本。測試已經全綠、意圖可以直接讀懂、同一項知識沒有散落,目前也找不到下一個必須建立的抽象責任,因此我選擇在具名方法停下來。

這就是本篇的停止條件:不是沒有更多結構可加,而是再加下去,已經沒有新的責任可以替它付費。

單次 Prompt 交代本次需求,Repository Policy 保存長期判斷規則

如果我只對 Agent 說「請保持簡單」,它可能把所有判斷留在 Processor;直接要求 Strategy Pattern,又會替它預先選定目前尚未成立的架構。

單次 Prompt 應該交代這次已知規則、不能改變的行為,以及什麼情況可以升級設計:

目前只有兩條通知升級規則,兩者使用相同資料,並因同一項通知政策而改變。
請把判斷集中在一個能揭露業務意圖的具名方法,保留既有呼叫流程、HTTP Contract 與測試行為。
除非你能指出第二個獨立使用者、不同生命週期、執行期組合或部署邊界,否則不要新增 Rule Interface、Strategy、Policy 或 DI 註冊。
完成後說明規則的唯一修改落點,以及停止繼續抽象的理由。

這種寫法不會替 Agent 指定每一行程式碼,卻把決策空間限制在目前證據支持的範圍。Agent 仍能自行整理實作;未來再加入通知規則時,也可以先搜尋 RequiresEscalationNotification,不用重新猜測時間門檻散落在哪一層。

單次任務留在 Prompt,跨任務反覆適用的判斷則整理成一份可以放進 AGENTS.md 的 Policy 範例:

## Simple Design Policy

- 所有設計候選先通過相同的行為測試、Contract Gate 與邊界案例。
- 先實作目前已知需求;不得為未提供的規則、Provider、部署方式或設定格式建立擴充點。
- 新增 Interface、Policy、Strategy、Factory 或 Collection 前,先列出目前已存在的獨立使用情境、變更來源或組合需求。
- 類別數、檔案數、Diff 行數與設計模式名稱都是審查線索,不得單獨決定設計優劣。
- 只有表達同一項知識的重複才抽象;外觀相似但變更理由不同的程式碼保持分開。
- 以已知的下一次變更檢查修改落點;若抽象只增加跳轉與組合成本,保留較直接的設計。
- 當多條已知規則共享政策責任、資料來源與變更理由,而且條件開始穿過多層方法時,優先集中到揭露意圖的具名方法。
- 只有出現獨立替換、不同生命週期、執行期組合、團隊分工或部署需求時,才升級成 Rule 型別與 Policy。
- 測試通過、意圖可讀、同一知識沒有散落,而且不存在無責任的結構時停止重構。
- 完成後回報新增概念、依賴、組合入口、修改位置、刻意沒有建立的抽象與停止理由。

Repository Policy 固定的是判斷流程,主要限制兩種 Agent 反應:看到「未來可能擴充」就建立完整設計模式,以及只憑一句「保持簡單」把所有規則留在流程裡。

它沒有規定幾條規則一定要抽象;最後仍要看變更理由、資料流、組合與部署情境。

這份 Simple Design Policy 目前是文章中的示範,尚未寫入 API Demo 的 AGENTS.md。未來規則累積到一定程度,再抽成可重複使用、能明確觸發的 Skill 會更合適;系列最後也會把這些 Clean Code Policy 集中整理成公開 Skill。

CLEAN 原則

共同測試與行為 Gate 已經落實 N — Non-Surprising Behavior 符合預期,它負責排除改變系統答案的候選。進入設計比較後,本篇主要使用的是 L。

L — Localized Change 局部變更:檔案數只是線索,規則落點才是判準

L 會繼續檢查每行 Diff 所屬的責任、同一項業務知識的落點,以及失敗時能不能局部放棄:

  • Minimal 沒有新增型別,兩個時間門檻卻穿過兩層方法簽章。
  • Intention 的正式規則只修改一個具名方法。
  • Pattern-ready 沒有修改 Processor,卻新增 Rule,還要同步正式 DI 與基準建構入口。

L 比較的是需求知識的實際落點。Processor 零修改仍可能增加多個組裝位置;只修改一個正式程式碼檔案,也可能讓同一條規則穿過多層方法。

簡單設計沒有固定答案,但一定要有停止條件與升級條件

回到開頭,三份候選都全綠,卻分別優化了不同東西:Minimal 降低當下結構與 Agent 執行成本;具名規則集中業務意圖;Pattern-ready 則為獨立規則與組合方式預留邊界。

目前兩條通知規則仍共享相同資料、生命週期與修改理由,所以我停在 RequiresEscalationNotification。規則短而且長期穩定時,Minimal Structure 的直接寫法成本更低;需要執行期組合、不同生命週期、團隊分工或獨立部署時,Pattern-ready 才有足夠理由承擔額外結構。

測試在這裡是入場券,不是優勝判定。當行為受到保護、意圖能直接讀懂、同一項知識集中,而且剩下的類別與間接層都有明確責任時,設計就可以先停下來。

這次 Work Item API 還沒有出現 Rule 的獨立替換與生命週期需求,所以系列接著沿用具名方法。這是目前情境下的選擇,不是永久答案。

Clean Code 提供測試、表達力、知識重複與必要規模等設計判準;CLEAN 則要求 User 把已知情境、行為 Gate、修改邊界、停止條件與升級條件交代給 Agent。Agent 再依這些條件自行決定實作細節。

今天第二條規則仍能待在一個具名方法裡。明天,第三條規則會由另一個政策維護者提出。我會用 SRP 與 OCP 檢查:繼續擴張同一個方法是否仍然合理,還是新的抽象終於有了真實的變更理由?

參考資料


上一篇
Day 13|測試全綠,為什麼仍漏掉「剛好 24 小時」?用整潔測試、驗收測試與受控 Mutation 檢查答案
下一篇
Day 15|第三條規則由不同負責人維護,SRP/OCP 要求 AI 把責任拆到哪裡?
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言