安安~我是ChiYu~
前天談測試紀律,昨天接著確認整潔的測試與驗收測試能不能真的守住需求。今天,這個系列正式跨過 Part I〈程式碼〉,進入 Part II〈設計〉。
一進入設計,測試全綠就不再是最後答案。
同一個 Agent 交出三份都能建置、測試全綠的版本:一份只新增布林變數,一份抽出具名規則,另一份建立 Policy 與相依性注入(Dependency Injection,DI)。Minimal Structure 使用的 Token 最少;Pattern-ready 加入第二條規則時,甚至不用修改 Processor。
我最後選的,卻是中間那份具名規則。
原因是簡單設計不是「檔案最少」或「擴充點最多」,而是用目前最小的必要結構,替已知的變更留下清楚位置。這次兩條通知規則共享相同資料與政策責任,一個 RequiresEscalationNotification 已經足夠集中意圖;完整 Policy 還沒有第二個獨立生命週期或組合需求可以回收成本。
測試綠燈只負責讓候選取得比較資格;設計模式名稱、Diff 大小與 Token 用量,也都只能提供一部分線索。《無瑕的程式碼 第二版》的〈簡單設計〉接著要回答的是:
測試已經全綠之後,User 要怎麼判斷 AI Agent 留下的設計,對目前需求來說已經足夠簡單?
Simple Design(簡單設計)不是把程式碼壓到最短,而是只保留目前需求需要的結構,同時讓行為受測試保護、意圖容易理解、同一項知識不會散落,而且後續仍然容易修改。
《無瑕的程式碼 第二版》的〈簡單設計〉列出四個正式主題:YAGNI、由測試保護、最大化表達力與最小化重複。Kent Beck 更早期對 Simple Design 的討論,還提出在前面條件成立後,繼續移除沒有溝通或運算用途的類別與方法。
我把這些觀念放回今天的案例,整理成五個審查問題:
這五題要一起看。檔案最少、抽象最多、設計模式最完整或測試全部通過,都不能單獨代表設計最好。
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 就是所有候選共用、開始修改前不得漂移的起始版本:
eb95ef658d5ae5081c1be6e667f7d4e7a8399440
day-14-simple-design-baseline
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
只看目前程式碼,很容易把設計審查變成風格投票。有人偏好所有條件留在附近,有人看到業務規則就想抽方法,也有人習慣先建立設計模式。三份候選只要都全綠,每個人都能用自己的偏好解釋結果。
所以我把實驗拆成兩個階段:
模型、Baseline、行為契約、修改範圍與驗證方式全程固定,比較焦點是 User 給出的設計方向。不過,三份 Prompt 也同時改變允許的抽象、禁止事項與停止條件,因此結果屬於三種設計策略的重複觀察,不能解讀成單一句提示詞造成的嚴格因果。
三組都使用相同 Baseline、模型、測試與修改範圍。每組各執行三個彼此獨立、不沿用前次對話與修改結果的 Codex Session,共九次實驗。每組的第一次執行統一標為 Run 01,並在實驗前就固定為第二階段代表,避免看完結果才挑出最有利的版本。
第二階段再讓三份 Run 01 代表候選各執行三次,共九個新 Session;兩個階段合計十八次正式實驗。
三種實驗情境的共同邊界是:只能修改 src/WorkItems.Api/**/*.cs,不得修改測試、HTTP Contract、Database Schema、套件與文件,也不能改變通知先於 Save、失敗摘要、Exception、Cancellation 與重跑語意。
Minimal Structure、Intention-revealing Rule 與 Pattern-ready 都是我為這次實驗定義的名稱,不是書中的正式分類。我把〈簡單設計〉轉成三種受控輸入,沒有預設優勝順序:
| 實驗方向 | 刻意施加的限制 | 想回答的問題 |
|---|---|---|
| Minimal Structure | 只保留目前需求需要的結構 | Agent 能不能適時停手? |
| Intention-revealing Rule | 集中規則並替意圖命名 | 一個具名入口是否已經足夠? |
| Pattern-ready | 預先建立擴充邊界 | 提前抽象要付出多少成本? |
共同測試與行為 Gate 先落實 N — Non-Surprising Behavior 符合預期,排除偷偷改變系統答案的候選。N 在這裡只負責取得比較資格;今天主要使用 L — Localized Change 局部變更,檢查規則與組裝責任最後落在哪裡。
整理 OverdueWorkItemProcessor 目前的通知路由判斷,讓 High 且逾期滿
24 小時使用 Escalation、其他到期未完成項目使用一般 Overdue
Notification 的意圖更容易讀懂。
- 保留完成目前需求所需的最少概念。
- 不新增 Interface、Policy、Strategy、Factory、Resolver、Collection
或 Plugin 體系。
- 可以保留直接條件式,也可以在同一個類別內建立必要的具名方法。
- 如果目前結構已經足夠簡單,可以不修改 Production Code,並說明停止理由。
- 不為尚未提供的未來規則建立擴充點。
這項任務主要測試 YAGNI。它仍允許區域變數、同類別具名方法,甚至可以判斷目前程式碼已經足夠清楚而完全不修改。我要用它作為結構成本較低的對照,再觀察第二階段新增時間門檻時,規則會不會沿著處理流程繼續傳遞。
整理 OverdueWorkItemProcessor 目前的通知路由判斷,讓 High 且逾期滿
24 小時使用 Escalation、其他到期未完成項目使用一般 Overdue
Notification 的業務意圖能由名稱與結構直接讀出。
- 將「是否使用 Escalation」整理成一個有清楚名稱與單一責任的規則位置。
- 可以使用同類別內的具名方法,或新增一個小型且內聚的規則型別。
- 不建立 Factory、Resolver、Plugin 掃描、規則集合或多層 Strategy 體系。
- Priority、DueAtUtc 與目前時間要容易追蹤。
- 不為尚未提供的未來規則預留方法、列舉或空介面。
這項任務聚焦最大化表達力。Prompt 不指定方法名稱與逐行實作,只要求規則形成一個清楚的語意入口,並把設計停在完整設計模式之前。
我要觀察的是:呼叫端是否更容易讀、第二條規則能否集中修改,以及多一次方法跳轉換來的表達力是否值得。
把 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 公開實驗資料。
九次候選都通過相同的既有行為 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 中選出一個執行。
Pattern-ready 的三次執行都走向 Policy,但 Run 01、Run 02 與 Run 03 分別新增 93/64/50 行,刪除 12/10/10 行,Interface、規則組合與相容入口也不一致。
這次能確認的是:當 User 只要求「建立可擴充邊界」,卻沒有提供第二種使用情境、規則生命週期或組合需求時,Agent 需要自行補完架構。
這項結果不能推論 Pattern 本身不穩定,只能說明目前 Prompt 留下的設計空白太多。可擴充不是一句目的就能完成,還得先說清楚誰要擴充、何時組合、由誰部署。
第一階段只告訴 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 狀態維持既有語意。
先閱讀目前候選結構,再以最符合該結構的局部修改完成需求。
不得整包改寫目前設計,也不得換成另一種候選方向。
第二階段的九份候選,都通過 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 小時規則,才看得出修改落點、新增概念與重工成本。
每個階段、每種設計方向都有三個獨立 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:
fb22f445f383c7977dfac71f35cc028b0ca53e00
day-14-simple-design
讀者可以直接切換:
git switch --detach day-14-simple-design
也可以查看 Baseline 與接受版本的完整 Diff。
這個選擇來自目前 Repository 的需求與修改結果,沒有把其中一種方向宣布成通用答案:
| 設計方向 | 本次決定 | 適合採用的情境 |
|---|---|---|
| Minimal Structure | 不作為系列接續點 | 規則短、變動少,而且在流程附近就能完整理解 |
| Intention-revealing Rule | 本次採用 | 已知規則共享資料、政策責任與變更理由,需要一個集中語意入口 |
| Pattern-ready | 目前不採用 | 規則需要獨立替換、不同生命週期、執行期組合、團隊分工或部署 |
現在兩條規則都在回答同一件事:「這筆工作項目是否該改走升級通知?」它們讀取相同的 Priority、DueAtUtc 與 currentUtc,也會因為同一類通知政策而一起改變。
RequiresEscalationNotification 正好替這個問題提供明確落點。第二階段加入 72 小時規則時,呼叫端、方法簽章與處理流程都不用修改;Agent 只要找到這個具名方法,就能看見現有規則並加入新判斷。
這個名稱同時服務人類與 AI:先告訴讀者程式碼正在決定什麼,再讓他進去理解 24 小時與 72 小時的細節。
它比 Minimal 多付出一次方法跳轉,換到穩定的業務語意與修改位置;同時省下 Pattern-ready 的新型別、規則集合、註冊與組裝成本。測試已經全綠、意圖可以直接讀懂、同一項知識沒有散落,目前也找不到下一個必須建立的抽象責任,因此我選擇在具名方法停下來。
這就是本篇的停止條件:不是沒有更多結構可加,而是再加下去,已經沒有新的責任可以替它付費。
如果我只對 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。
共同測試與行為 Gate 已經落實 N — Non-Surprising Behavior 符合預期,它負責排除改變系統答案的候選。進入設計比較後,本篇主要使用的是 L。
L 會繼續檢查每行 Diff 所屬的責任、同一項業務知識的落點,以及失敗時能不能局部放棄:
L 比較的是需求知識的實際落點。Processor 零修改仍可能增加多個組裝位置;只修改一個正式程式碼檔案,也可能讓同一條規則穿過多層方法。
回到開頭,三份候選都全綠,卻分別優化了不同東西:Minimal 降低當下結構與 Agent 執行成本;具名規則集中業務意圖;Pattern-ready 則為獨立規則與組合方式預留邊界。
目前兩條通知規則仍共享相同資料、生命週期與修改理由,所以我停在 RequiresEscalationNotification。規則短而且長期穩定時,Minimal Structure 的直接寫法成本更低;需要執行期組合、不同生命週期、團隊分工或獨立部署時,Pattern-ready 才有足夠理由承擔額外結構。
測試在這裡是入場券,不是優勝判定。當行為受到保護、意圖能直接讀懂、同一項知識集中,而且剩下的類別與間接層都有明確責任時,設計就可以先停下來。
這次 Work Item API 還沒有出現 Rule 的獨立替換與生命週期需求,所以系列接著沿用具名方法。這是目前情境下的選擇,不是永久答案。
Clean Code 提供測試、表達力、知識重複與必要規模等設計判準;CLEAN 則要求 User 把已知情境、行為 Gate、修改邊界、停止條件與升級條件交代給 Agent。Agent 再依這些條件自行決定實作細節。
今天第二條規則仍能待在一個具名方法裡。明天,第三條規則會由另一個政策維護者提出。我會用 SRP 與 OCP 檢查:繼續擴張同一個方法是否仍然合理,還是新的抽象終於有了真實的變更理由?