iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

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

Day 18|AI 一次完成三項需求,和逐步設計相比,加入背景 Worker 時有什麼差別?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天談完元件原則後,Work Item API 已經形成三個清楚的責任區域:API 處理 HTTP、資料與通知流程,WorkItems.ServiceLevel 保存共用的 SLA 規則,Reporter 則是只使用 SLA 規則的獨立 Consumer。

但這個結構不是某一天突然畫出來的。第二個 Actor、通知 Provider 與 Reporter 依序出現後,設計才一步一步長成現在的樣子。

這讓我想做一個反方向的比較:

如果一開始就把這三項需求全部交給 AI,和每次只加入當下已知需求相比,兩邊遇到下一項未知需求時,修改方式會有什麼差別?

結果不像「一次做完一定過度設計、逐步設計一定比較精簡」這麼整齊。三份 Big-bang 候選都通過行為 Gate,也都停在三個 Production Project;持續設計路線的累積 Diff 反而更大。真正的差異,要等兩邊都不知道的背景 Worker 出現後才看見。

Big-bang 代表版本讓 Controller 與 Worker 直接共用具體 Processor,修改檔案較少;持續設計版本則在第二個 Production 入口真的出現後,建立 IOverdueWorkItemProcessor,讓 Controller、Worker 與 Worker 測試替身共同使用。我最後接受介面版本,不是因為它用了較少 Token,而是契約、兩個正式 Consumer 與隔離測試接縫都已經成立。

今天要比較的不是哪一條路線永遠比較好,而是需求揭露順序如何影響抽象出現的時機,以及下一項未知需求進來時,現有設計要付出什麼修改與驗證成本。

〈持續設計〉:讓設計跟著真實需求與回饋調整

《無瑕的程式碼 第二版》的〈持續設計〉把設計放回每一次修改。專案開始時會做設計,需求、測試與回饋出現後,設計也要繼續調整。

Continuous Design(持續設計)不是不做事前思考,而是不把尚未出現的 Consumer、改變原因與部署方式直接生成成 Production Code。已知風險仍要先處理;未知未來則等到有真實壓力時再決定抽象。

書中以 Four Cs 觀察設計品質:

Four Cs 白話理解 檢查設計時先看什麼
Clarity(清晰性) 讀者能不能直接理解意圖 名稱、公開入口、依賴方向與執行路徑
Conciseness(簡潔性) 概念是否足夠,而且沒有多餘結構 每個 Interface、Class 與 Project 是否已有真實用途
Confirmability(可確認性) 結果能不能可靠、重複地驗證 行為 Oracle、測試、失敗訊息與可重現狀態
Cohesion(內聚性) 會因相同原因改變的程式碼是否放在一起 Actor、責任、共同修改原因與下一項需求的落點

替 Use Case 抽出介面,可能讓多個入口共用的能力更明確,也方便隔離測試;同時會增加一個檔案與一次跳轉。Four Cs 不會替某一種架構背書,而是讓這筆交換能被說清楚。

我也保留一條界線:Public API、Database Schema、資料移轉、權限、金流、跨服務契約與不可逆外部操作,一旦選錯就很難回復。這些已知風險仍要先完成相稱的事前設計,不能拿「持續演進」當成延後思考的理由。

現在的程式碼仍要靠名稱、契約、測試與依賴方向表達意圖。Git 歷程可以補充抽象出現的原因,不能替難懂的 Code 善後。

AI 降低修改成本後,重型事前規劃還值得嗎?

Uncle Bob 在近期訪談裡分享 Heavy Upfront Planning 的經驗:Agent 依照完整計畫一路往下做時,只要中途出現原先沒想到的細節,後續步驟就可能建立在錯誤前提上,最後得停下來重寫。

他用一棟「每次改動只要一美元」的房子做思想實驗。若移動牆面、樓梯與廚房都非常便宜,先做一小段、實際走走看,再依回饋調整,可能比先花大筆成本追求完美藍圖更划算。AI 確實降低了產生與修改 Code 的成本,也讓短週期更值得考慮。

不過,這個比喻沒有讓規格消失。他的流程仍會先把小範圍需求整理成 Gherkin 與 QA Procedure;被質疑的是一次規劃完所有細節,而不是需求、契約與驗收條件本身。Public API、Schema、權限、金流與不可逆副作用的修改成本也沒有接近一美元。

因此兩條路線都有規劃,差別在批次:第一條一次交付三項已知需求,第二條每次只加入一項真實需求。我要看的是,哪一條路線讓抽象的來歷更容易追查,也能在未知 Worker 出現時合理調整。

Four Cs 檢查設計結果,CLEAN 規範我怎麼操作 Agent

Four Cs 用來檢查 Code 與設計結果;CLEAN 則規範我如何提供情境、限制範圍、保存實據並守住既有行為。

今天主要使用兩項 CLEAN 原則:

  • C — Context-Aware Code 情境感知:把目前真實存在的需求、Consumer 與 Repository 狀態交代清楚,也把未知項目明確標成未知。
  • A — Auditable by Evidence 實據可審:讓 Prompt、Diff、Commit、Tag 與驗證結果都能對回同一項需求,抽象為什麼出現可以被追查。

從同一個簡單設計起點,比較一次揭露與逐步揭露需求

兩條路線都從 Day 14 接受的簡單設計版本開始:

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

起點 Commit 是:

fb22f445f383c7977dfac71f35cc028b0ca53e00

當時逾期流程已通過既有行為驗證,規則也足夠簡單、意圖明確;第二個 Actor、第二個 Provider 與 Reporter 都還沒出現。這是三項設計壓力進入 Repository 前,最後一個共同狀態。

路線 Agent 何時取得需求 執行方式 能回答什麼
一次到位式設計(Big-bang) 第一輪同時取得三項需求 從相同起點執行三個全新 Session 同一批已知需求一次交付時,Agent 會產生哪些結構
持續設計歷程 三項需求依序在 Day 15、16、17 出現 沿用系列實際接受的 Commit 序列 每個抽象首次被需要時,當下有哪些 Consumer 與變更原因

這裡的 Big-bang 是本系列對實驗路線的稱呼:User 一次提供目前已知的三項需求,讓 Agent 在同一批修改中完成。它不代表 Agent 已經看見所有未來,更不代表它能預測今天才公開的 Worker。

第一條路線一次取得:

  1. 第二個 Actor 維護「未指派項目一到期就升級」的規則。
  2. 通知 Provider 可以在 Demo 與 Queued 之間替換。
  3. 新增只需要共用 SLA 規則的 Reporter Consumer。

第二條路線則沿用本系列實際走過的歷程:

Day 14  fb22f445  目前只需要表達 SLA 規則
   ↓
Day 15  ccbc136d  第二個 Actor 出現,分開兩種改變原因
   ↓
Day 16  5f596a62  第二個通知 Provider 出現,建立 Consumer 所需契約
   ↓
Day 17  76de5b54  Reporter 出現,只抽出已被共同使用的 SLA 元件

這項比較有一個重要限制。Big-bang 使用三個全新 Session;持續設計路線則來自不同日期的三次實驗,其中包含不同 Prompt、候選比較與人工選擇。因此本文比較的是兩段實際設計歷程,無法把差異全部歸因於需求揭露時間。

完整 Prompt 與控制條件保存在公開 Repository:

兩邊共用與刻意隱藏的資訊如下:

固定:
- 相同 Repository 起點
- Codex GPT-5.6-SOL-HIGH
- 相同既有 HTTP Contract、Database Schema 與行為 Gate

Big-bang 第一階段公開:
- 第二個 Actor
- 第二個 Provider
- Reporter Consumer

第一階段刻意不公開:
- 背景 Worker
- 並行控制
- Outbox
- Idempotency

三次 Big-bang 都通過行為 Gate,內部設計仍不完全相同

三個 Session 的起點、Prompt、模型與主流程驗證相同。我先確認需求是否完成,再比較類別、測試與 Provider 接線的差異。

Run 主流程驗證 Production 邊界 修改範圍 實際設計差異
01 通過 API、ServiceLevel、Reporter 21 個檔案 使用 ServiceLevelNotificationRule,Reporter 邏輯放在 ReporterApplication,並建立獨立的 ServiceLevel 測試 Project
02 通過 API、ServiceLevel、Reporter 19 個檔案 使用 ServiceLevelEscalationRule,Reporter 邏輯直接留在 Program,相關測試集中於 WorkItems.Api.Tests
03 通過 API、ServiceLevel、Reporter 22 個檔案 使用 ServiceLevelEscalationPolicy,另外拆出 QueuedNotificationGatewayReporterApplication,Provider 選擇則放進設定檔

三份候選都能依 Lock File 還原套件並完成 Release Build,格式也符合規範。完整測試與 API 基本流程守住既有行為,Reporter 案例則確認第二個 Consumer 可以獨立執行。它們也都停在三個 Production Project,沒有為了「未來擴充」繼續拆出一長串架構層。

所以這次結果不支持「Big-bang 一定會過度設計」。需求、禁止事項與行為 Gate 足夠清楚時,Agent 一次取得多項真實需求,也能停在合理範圍。不過,完整需求沒有讓設計收斂成唯一答案,三份候選對類別、測試與 Provider 接線仍有不同選擇。

後續 Worker 壓力選用 Run 02。三份候選都保留完整能力並通過相同 Gate,而 Run 02 的修改範圍最小,適合當下一階段代表起點。這項選擇只服務本次壓力測試,不代表 Run 02 是 Big-bang 的平均輸出或普遍最佳架構。

讀者可以直接切換:

git switch --detach day-18-formal-big-bang-run-02

持續設計的累積 Diff 較大,但每個抽象都有需求來歷

從共同起點走到相近功能終點,持續設計路線累計修改 23 個檔案,增加 730 行、刪除 41 行;Big-bang Run 02 修改 19 個檔案,增加 474 行、刪除 24 行。

持續路線跨越三次不同日期的實驗,也包含不同測試與人工選擇,所以這些數字不能直接用來宣稱持續設計比較便宜。這次它的累積 Diff 反而更大。

持續路線真正留下的優勢,是每個抽象都能對回第一次需要它的壓力:

Commit 當時新增的真實情境 因此留下的設計
ccbc136d 派工管理與服務等級由不同 Actor 維護 分開兩種升級規則與改變原因
5f596a62 Demo 與 Queued 都要服務同一個逾期流程 由 Consumer 定義通知契約,Provider 反向實作
76de5b54 Reporter 只需要 SLA,不需要整個 API 抽出 WorkItems.ServiceLevel,並停止拆更多 Project

Commit 與 Tag 可以重現當時狀態,也能查出抽象第一次出現的原因。現在的 Code 仍要靠類別名稱、依賴方向、公開契約與測試說明責任;讀者不該先考古 Git History,才知道設計在做什麼。

刻意隱藏背景 Worker,才能觀察設計如何承接未知變更

兩條路線抵達相近功能終點後,我才公開相同的新需求:新增 ASP.NET Core BackgroundService,讓背景排程與既有 HTTP Action 共用逾期處理。

Worker 預設停用,啟用後每 60 秒觸發一次逾期流程,並把 Host 停止時的 Cancellation 往下傳遞。

我把它留到第二階段,讓兩個版本先在完全不知道這項需求的情況下完成。Worker 出現後,才比較:

  • 新入口會修改哪些 Production Code?
  • Controller 與 Worker 會不會複製逾期規則?
  • 既有邊界能不能支援隔離測試?
  • Agent 會新增哪些概念,又為什麼在這個時候新增?

這一階段只測第二個執行入口。Lock、Lease、Concurrency Token、Transaction、Outbox 與 Idempotency 都刻意不做,避免把「如何共用 Use Case」和「如何避免兩個流程重複處理」混成同一項實驗。

相同 Worker 需求,兩個版本採用不同的共用邊界

兩份候選最後都讓 Controller 與 Worker 進入同一份 ProcessAsync,沒有各自複製查詢、升級判斷、通知、統計與儲存流程。

Big-bang 版本直接共用具體 Processor

Big-bang Run 02 原本已有 OverdueWorkItemProcessor。加入 Worker 後,Controller 與 OverdueProcessingWorker 直接依賴同一個具體類別:

WorkItemsController ──┐
                     ├──> OverdueWorkItemProcessor.ProcessAsync
Background Worker ───┘

這個版本只修改 4 個檔案,新增 281 行。Worker 測試啟動 ASP.NET Core 測試 Host,接上測試資料庫與既有通知替身,驗證 Worker、DI、Database 與逾期流程組裝後能否一起運作。代價是每次測試都要建立較完整的環境。

它沒有為第二個入口再建立介面,呼叫路徑也少一層跳轉。若團隊本來就偏好完整整合驗證,這是一個合理答案。

持續設計版本為兩個入口建立 Use Case 介面

持續設計終點原本只有 Controller 使用 Processor。Worker 成為第二個入口後,Agent 新增 IOverdueWorkItemProcessor,再讓兩個入口都依賴這份 Use Case 契約:

WorkItemsController ──┐
                     ├──> IOverdueWorkItemProcessor
Background Worker ───┘              │
                                     └──> OverdueWorkItemProcessor

持續設計版本修改 8 個檔案,增加 187 行、刪除 3 行。Controller、Processor、DI 與測試都要改接介面,所以觸及檔案較多;Worker 測試不必啟動完整 API 與資料庫,因此新增測試程式碼反而較少。

這個介面目前有兩種實際用途:Controller 與 Worker 透過它共用 Use Case;Worker 測試則注入 RecordingOverdueWorkItemProcessor,只記錄呼叫次數與 Cancellation。這是一個已被正式入口與測試使用的接縫,不是替想像中的第三種 Processor 留空位。

版本 Production 共用方式 Worker 測試方式 目前代價
Big-bang Run 02 兩個入口依賴具體 Processor 以完整 API、Database 與通知替身做整合驗證 路徑直接,但測試需要較完整的執行環境
持續設計終點 兩個入口依賴 Use Case 介面 注入記錄型測試替身,隔離 Worker 生命週期 多一層介面與 DI 接線

兩種版本都通過相同主流程 Gate。檔案數與新增行數只描述修改成本,最後仍要看新增抽象是否有真實 Consumer,以及測試要回答哪一個問題。

Big-bang 一次揭露需求與持續設計逐步揭露需求的介面形成時間軸

圖:持續設計讓背景 Worker 成為第二個真實使用情境後再建立介面;Big-bang 是否適合,仍取決於需求不確定性。

Four Cs 如何解讀具體 Processor 與 Use Case 介面的取捨

Clarity:兩個入口都能找到同一份逾期流程

兩份候選都能從 HTTP 與背景排程走到同一份 ProcessAsync,沒有把業務流程藏在 Controller 或 Worker 裡。

具體 Processor 少一次跳轉,看見類別就能找到實作;Use Case 介面則直接命名兩個入口共同需要的能力,再由 DI 指向實作。前者直接,後者把契約寫得更明確,兩者目前都說得通。

Conciseness:介面是否已經有真實用途

Big-bang 版本沒有新增介面,適合實作唯一、專案不需要隔離替換的情境。

持續設計版本的介面已被 Controller、Worker 與測試替身使用,因此不是沒有 Consumer 的預先抽象。若未來這些用途消失,或每次修改都必須同時更動介面與實作,這層抽象仍應重新檢查。

Confirmability:整合測試與隔離測試回答不同問題

Big-bang 版本的測試會走過 DI、Database、通知與 Worker 的完整接線;持續設計版本則以測試替身隔離 Worker 生命週期。

整合測試回答組裝後能不能一起運作;隔離測試則能更快指出排程次數、預設開關或 Cancellation 哪一項行為失敗。Confirmability 看的是回饋是否可信、失敗是否對得上需求缺口,不是測試總數。

Cohesion:Worker 管排程,Processor 管逾期流程

兩份候選都把逾期查詢、升級規則、通知、統計與儲存留在 Processor;Worker 只負責排程週期、Scope、Cancellation 與錯誤紀錄。

逾期規則改變時,主要修改 Processor;執行週期或 Host 生命週期改變時,主要修改 Worker。這種修改落點比「都放一個 Project」或「每層拆一個 Project」更能說明內聚性。

持續設計沒有保證省 Token,單次 Worker 實驗只提供一筆觀察

持續設計要多次讀取 Repository、執行 Gate 並做出接受決策,不能預設它會省 Token。

這次 Worker 單次實驗中,持續設計版本使用 12,231 Output Token、23 次工具呼叫,約 330 秒完成;Big-bang 代表版本使用 19,756 Output Token、31 次工具呼叫,約 654 秒。

兩個起點的結構與測試不同,而且各自只跑一次。這筆資料只能描述本次 Worker 修改的成本,不能把 Token 下降歸因於持續設計。若少用 Token 的候選改變行為、漏掉失敗路徑或留下無法說明的抽象,它仍不是較好的答案。

CLEAN 原則

C — Context-Aware Code 情境感知:用真實需求決定現在需要多少設計

C — Context-Aware Code 情境感知 要求我完整提供已確認的需求、風險與 Repository 規則,並把尚未出現的 Consumer 與功能標成未知。

Big-bang 路線同時取得三項真實需求,因此一次處理並沒有違反 C;持續設計路線每一步只取得當時已知情境,也同樣成立。兩者都不能用「一般架構通常會需要」替 Repository 自行補完未被證實的未來。

A — Auditable by Evidence 實據可審:保留需求與抽象之間的時間線

A — Auditable by Evidence 實據可審 要求 Prompt、Diff、Commit、Tag、Tests 與主流程驗證可以對回同一項需求。這樣才能查出某個 Interface 或 Project 何時出現、當時解決什麼問題,以及後續是否還有存在理由。

本系列把能獨立 Build、測試與切換的 Commit/Tag 狀態稱為 Git Checkpoint。這是文章使用的實驗名稱,不是 Git 內建功能。

git revert 會建立新的 Commit,反轉指定 Commit 的原始碼 Patch;它不會收回已寄出的通知、完成的 Database Migration 或部署。後續 Commit 若依賴被撤回的結構,Revert 也可能衝突。因此回退後仍要重新 Build、Test,外部狀態則要另外處理。

L — Localized Change 局部變更 在這篇只作為修改範圍的輔助檢查:Worker 需求是否只新增第二個入口,還是順便重做 SLA、Provider 或 Reporter?檔案數與行數只是線索,不足以單獨選出答案。

AI Coding 該選持續設計、必要事前設計,還是一次完成?

情境 優先方向 判斷理由
需求仍在變,每一步都有可靠測試且可以安全回退 持續設計 讓抽象跟著真實 Consumer 與變更原因出現
Public API、Schema、資料移轉、資安、權限、金流或不可逆副作用 先完成必要事前設計 邊界選錯後的回復成本遠高於短時間分析
可丟棄 Prototype,只想驗證技術可行性 可以一次生成 重點是快速取得答案,不是長期維護結構
已有成熟模板、固定 Contract 與完整驗證器 可以一次套用 未知因素少,Agent 主要執行機械化轉換
一次修改混入多項無法獨立驗證的需求 先切小再做 失敗原因、審查與回退範圍已經混在一起

我會用三項條件選擇 Agent 的工作節奏:需求是否穩定、失敗是否包含不可逆風險,以及目前的驗證能力能不能接住修改。

用 AGENTS.md 格式整理 Continuous Design Policy

我先用 AGENTS.md 的格式整理 Continuous Design Policy,讓讀者可以直接參考。這裡是文章中的示範,尚未宣稱它已寫入公開 Demo。系列後段會再把成熟 Policy 抽成 Skill,不必一直擴充 Repository Instruction。

## Continuous Design Policy

- 先列出目前已證實的需求、Consumer、Actor、固定契約與未知項目,
  不得把假想未來直接生成為 Production Interface、Project、Provider 或架構層。
- 新抽象必須回報首次需要它的需求、實際 Consumer、替代方案與停止理由;
  沒有真實用途時,優先保留較便宜的函式、類別、Namespace 或具體實作。
- 每個演進步驟必須保持可 Build、可測試、可審查,
  不得為追求小 Commit 留下無法執行或語意錯誤的中途版本。
- 評估結果時,以 Clarity、Conciseness、Confirmability、Cohesion 說明取捨;
  不得用檔案數、行數、Project 數、Coverage、Commit 數或 Token 單獨判定品質。
- 需求仍在演進、每一步可驗證且可安全回退時,優先採用持續設計。
- Public API、Database Schema、資料移轉、資安、權限、金流、
  跨服務契約或不可逆副作用,必須先完成與風險相稱的事前設計。
- 可丟棄 Prototype、成熟固定模板或具有獨立完整驗證器的機械化任務,
  可以一次生成,但必須標示產物能否直接成為長期維護的 Production Code。
- 完成後回報 Prompt、起點、Diff、Commit/Tag、Gate、可回退範圍、
  外部副作用、Token/Tool Call 與尚未處理的風險。

這份示範 Policy 沒有把所有任務都固定拆成小步驟。User 與 Agent 仍要依需求穩定度、不可逆風險與驗證能力,選擇持續演進、必要事前設計或一次完成,再用 Four Cs 驗收結果。

本系列接受介面版本,因為契約、測試接縫與設計來歷都已成立

本系列接下來使用持續設計路線的 Worker 版本:

git switch --detach day-18-continuous-design

Commit 是:

be987152555b651532e4ba68ba1029aa777fb40e

回到標題,一次完成三項需求的版本並沒有失控,它用較少檔案讓兩個入口直接共用具體 Processor;逐步設計的版本也沒有自動比較小,它的累積 Diff 反而更大。兩邊真正不同的,是 Worker 出現時,是否已經有足夠理由建立 Use Case 契約。

我接受持續設計版本,因為現在的 Code 能直接看出 Controller 與 Worker 共用同一份逾期 Use Case。IOverdueWorkItemProcessor 已被兩個 Production 入口使用,也真的形成 Worker 隔離測試的接縫;Commit 歷程則補充說明,介面是在第二個入口出現後才建立。修改檔案數、測試總數與較低 Output Token 都只描述成本,不是接受條件。

Big-bang 的具體 Processor 依賴仍是合理選擇。如果專案只有單一實作,而且團隊本來就以完整整合測試驗證 Worker,增加介面只會多一次跳轉,我會保留具體依賴。

持續設計幫這個 API 承接了第二個入口,卻沒有自動解決兩個入口同時執行的問題。兩個版本都留下同一個缺口:Smoke Test 對兩筆已標成逾期的 Work Item 再執行一次時,processedCount=0,流程卻仍對兩筆資料各嘗試一次通知。共用 Use Case 解決規則重複,還沒有解決副作用重複。

明天我會用兩個獨立執行流程同時處理同一筆逾期項目,實際重現競態,再拆開 Concurrency Control、Transaction、Outbox 與 Idempotency 各自能守住的失敗窗口。

參考資料


上一篇
Day 17|AI 把 API 拆成多個 Project,怎麼判斷哪些元件邊界值得留下?
下一篇
Day 19|兩個執行流程同時處理同一筆 Work Item,為什麼會重複通知?從競態條件到 Outbox 與冪等設計
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言