iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

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

Day 20|兩份程式碼都通過測試,加入重試規則後哪一種結構比較容易修改?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天,我們處理了 HTTP 與背景 Worker 同時選中同一筆 Work Item、進而重複送出通知的問題。資料更新、通知傳送、失敗重試與取消路徑,也都有對應測試。

從行為價值來看,昨天的需求已經完成。不過,測試全綠只說明系統現在能運作,還沒有回答下一次需求進來時,哪些責任會被迫一起修改。

所以今天我先建立兩份行為相同、結構不同的程式碼:一份保留整合式 Dispatcher,另一份把不依賴 EF Core 與 Gateway 的決策移到具體 Policy。第一階段完成時,兩個方向都有合理停止點,我也刻意沒有公開下一題。

接著,我才交付事前保密的重試矩陣:Escalation 第一次失敗等 5 分鐘,之後等 30 分鐘;Overdue 第一次失敗等 15 分鐘,之後等 60 分鐘。

六次壓力實驗都通過相同行為 Gate。整合式候選的第二次 Diff 比較短;Policy 分離式的三次執行,則都把重試矩陣放進既有的 NotificationDispatchPolicy。本系列最後採用 Policy Run 03,因為「通知種類 × 嘗試次數」已經形成真實變化軸,而且過時的 ReleaseLease 也被改成更準確的 ScheduleRetry

這不是宣布 Policy 永遠比較好。如果重試矩陣沒有出現,規則少、只有一個 Consumer,整合式 Dispatcher 的零修改結果反而更精簡。今天要驗收的,是哪一份結構能讓已經發生的變化沿著清楚邊界落下,而不是哪一份看起來比較有架構。

〈軟體的兩種價值〉談的是現在能不能用,以及明天能不能改

Clean Code 進入架構層次後,檢查範圍會從名稱、函式與類別,延伸到 Use Case、背景 Worker、資料庫與外部服務之間的責任。

〈軟體的兩種價值〉把這件事分成兩個面向:

  • 行為價值(Behavior Value):系統現在能不能完成使用者需要的功能。
  • 結構價值(Structure Value):需求改變後,系統能不能在合理成本下繼續修改、擴充與維護。

行為價值很容易看見:API 回傳正確、資料成功儲存、通知有送出、測試通過。結構價值則通常要等下一項需求進來,才能從實際修改路徑判斷:

  • 新規則需要修改幾種責任?
  • 是否要跨過不相關的技術依賴?
  • 同一項知識要同步幾份?
  • 原本穩定的 I/O 流程,是否被迫跟純決策一起改動?

如果用生活中的例子來說,行為價值像是確認今天按下開關,電燈會亮;結構價值則是下次想多裝一個開關時,是否得拆掉整面牆。

檔案數與抽象數量都只能當線索。多種責任可能被塞在同一個檔案裡;Interface、Strategy 與 Framework 也可能只是替尚未出現的變化預付成本。設計層次真正要看的,是已經出現的變化能不能沿著清楚責任發生。

先建立兩份不同結構,再用同一項保密需求比較修改成本

整合式 Dispatcher 與 Policy 分離式 Dispatcher,是我為了觀察結構價值而設計的兩種對照,不是〈軟體的兩種價值〉列出的固定解法。實驗分成兩個階段:

  1. 建立相同行為、不同結構:一份保留整合式 Dispatcher,另一份把純決策分離成 Policy。
  2. 加入事前保密的新需求:兩份結構都加入通知失敗後的等待規則,再比較修改位置與結構成本。

第一階段不公開重試需求。Agent 若事先知道下一題,很可能直接替那項規則準備結構;這樣只能觀察預先設計的效果,無法知道原本結構面對未知變更時會怎麼改。

我選擇重試時間作為壓力,是因為兩份候選都必須修改相同的 Outbox 欄位、時間資格與 Atomic Claim;真正拉開差異的地方,會集中在重試矩陣屬於 Dispatcher 的 I/O 流程,還是已形成獨立變化方向的純決策。

flowchart LR
    B[昨天確認的並行行為基準] --> I[整合式 Dispatcher]
    B --> P[Policy 分離式 Dispatcher]
    I --> R1[加入相同重試規則]
    P --> R2[加入相同重試規則]
    R1 --> C[比較修改位置與結構成本]
    R2 --> C

固定模型、起點與驗證方式,只改變第一階段留下的結構

實驗起點是昨天接受的並行版本:

  • Commit:388f0af06987056516a7fbc9b461c107127b067c
  • Annotated Tag:day-19-concurrency-race

想直接查看起點,可以執行:

git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
cd AI-CleanCode-API-Demo
git switch --detach day-19-concurrency-race

完整 Prompt、原始輸出、每份候選與主流程驗證保存在公開實驗資料

實驗條件 固定方式
Agent Codex,gpt-5.6-sol,Reasoning Effort:high
起點 相同的 day-19-concurrency-race
工作階段 Ephemeral Session,每次建立全新工作階段,不沿用前一次對話
工作目錄 各自使用獨立 Git Worktree
Repository 規則 相同的 Repository Instruction
驗證 相同的 Build、Tests、Format、Diff 與 API 基本流程檢查
第一階段唯一規劃變因 保留整合式 Dispatcher,或分離純決策 Policy

第一階段兩個方向各執行一次;第二階段再從兩份結構各跑三次。每組三次用來觀察修改方向是否穩定,仍只是小樣本,不能當成統計結論。

這組結果屬於可重跑的工程觀察。模型、起點、需求與驗證方式固定,Agent 的探索順序與工具使用仍有差異,因此不能把結果解讀成嚴格的單一變因實驗。

第一階段先建立行為相同、結構不同的兩份程式碼

起點的 NotificationOutboxDispatcher 同時負責:

  • 用 EF Core 掃描可處理訊息,並透過 Atomic Claim 取得處理權。
  • 計算 Lease 到期時間。
  • 依通知種類選擇 Gateway。
  • 根據 Gateway 結果更新完成或失敗狀態。
  • 在 lost ACK、取消與重跑時維持相同的 Idempotency Key。

這些責任可以有兩種合理解讀:

結構方向 怎麼看目前責任 當下優點 先支付的成本 什麼情況會成立
整合式 Dispatcher 掃描、Claim、傳送與狀態更新共同維護一條完整流程 跳轉少、結構直接、沒有新增概念 純決策與 I/O 留在同一類別 只有一個 Consumer,規則少且一起改變
Policy 分離式 Dispatcher Lease、路由與完成狀態是可以脫離 EF Core 與 Gateway 的純決策 規則可獨立閱讀與測試 多一個類別、依賴與跳轉 決策有獨立變化方向,或會被多處重用

第一階段的共同條件相同,只有結構方向不同:

共同條件:
- 保留 HTTP Contract、SQLite Schema、並行、lost ACK、取消與冪等行為。
- 不新增尚未出現的 Provider、Strategy 或部署層。
- 完成後執行相同驗證,並說明停止理由。

整合式方向:
- 先判斷現有流程是否真的需要拆分。
- 沒有獨立變化證據時,允許保持零修改。

Policy 分離式方向:
- 將不需要 EF Core、Gateway 或時間來源即可回答的決策,移到具體 Policy。
- 沒有第二個實作需求時,不建立 Interface。

完整 Prompt 留在公開實驗資料,正文只保留會影響結果的條件,避免操作細節蓋過核心問題。

第一階段尚未公開重試規則,整合式與 Policy 分離都有合理停止點

整合式候選讀完程式碼與測試後,選擇不修改正式程式碼。它看到 Lease、通知路由、完成狀態與 lost ACK 目前都跟著同一條 Dispatch 流程改變;Repository 也沒有第二個 Consumer 或獨立變化規則。此時繼續拆分,只會增加依賴與閱讀跳轉。

Policy 分離式候選則新增具體的 NotificationDispatchPolicy,但沒有為了形式再建立 Interface:

public sealed class NotificationDispatchPolicy
{
    private static readonly TimeSpan LeaseDuration =
        TimeSpan.FromMinutes(1);

    public DateTimeOffset CalculateLeaseExpiration(
        DateTimeOffset claimUtc) =>
        claimUtc.Add(LeaseDuration);

    public NotificationDeliveryRoute SelectDeliveryRoute(
        string notificationKind) =>
        notificationKind == "Escalation"
            ? NotificationDeliveryRoute.Escalation
            : NotificationDeliveryRoute.Overdue;

    public NotificationDispatchCompletion DecideCompletion(
        bool acknowledged) =>
        acknowledged
            ? NotificationDispatchCompletion.MarkSent
            : NotificationDispatchCompletion.ReleaseLease;
}

Dispatcher 依賴具體 Policy;Policy 不依賴 EF Core、Gateway 或 TimeProvider

第一階段得到兩個都說得通的結果。整合式候選留下零 Production Diff,表示「停止重構」在當下成立;Policy 分離式則隔離純決策,而且沒有再堆出 Interface 或新架構層。真正的結構價值,要等新規則實際進場才看得見。

加入通知失敗後的等待規則,實際測量下一次修改成本

兩份結構固定後,我才交付相同的新需求:

通知種類 第一次失敗後 第二次及後續失敗後
Escalation 等待 5 分鐘 等待 30 分鐘
Overdue 等待 15 分鐘 等待 60 分鐘

Retry Policy 通常要回答哪些失敗可以重試、何時重試、最多幾次,以及最後仍失敗時如何處理。今天只聚焦排程時間。Backoff 則是在下一次嘗試前先等待,避免外部服務尚未恢復時被立即連續呼叫。

這次實作的是持久化業務排程:下一次可處理時間會寫進 SQLite,Host 重啟或換另一個 Dispatcher 接手後仍能延續。

Microsoft 的 Retry Pattern 主要處理單次遠端呼叫遇到的短暫性錯誤,還要一起考慮錯誤類型、嘗試次數、等待間隔與冪等性。兩者處理的生命週期不同。

這項需求還有幾個不能省略的邊界:

  • AttemptCount 在 Atomic Claim 時增加,所以第一次失敗時的值是 1。
  • 新訊息建立後立即可處理。
  • 邊界前 1 毫秒不得掃描、Claim 或呼叫 Gateway。
  • currentUtc == NextAttemptAtUtc 時可以開始處理。
  • 一般失敗要清除 Lease,並保存下一次可處理時間。
  • OperationCanceledException 必須向外傳遞並保留 Lease,不能被當成一般失敗。
  • 雙 Dispatcher、lost ACK、Idempotency Key 與 At-least-once 保證都不能漂移。

TimeProvider 是 .NET 的時間來源抽象。正式程式用它取得目前時間,測試則替換成 FakeTimeProvider,直接把時鐘往前推,不必真的等待 5、15 或 60 分鐘,也能驗證邊界前不可執行、到達邊界即可執行。

先確認新測試會因舊行為失敗,才能知道它真的抓得到需求缺口

第二階段的關鍵 Prompt 可以濃縮成:

- 從指定的第一階段結構開始,不得重新選擇起點。
- 先新增時間邊界測試,並確認舊程式會失敗。
- 再加入最小正式程式碼,使相同測試轉綠。
- 保留既有 API、SQLite、並行、lost ACK、取消與冪等行為。
- 不得以 Task.Delay 代替持久化的 NextAttemptAtUtc。
- 完成後回報規則落點、修改範圍、驗證結果與停止理由。

兩種起始結構各跑三次。舊程式在尚未到達 NextAttemptAtUtc 時,仍然掃描訊息、取得處理權並呼叫 Gateway,因此六次都得到相同 RED:

Expected gateway attempts: 0
Actual gateway attempts:   1

這個失敗直接對應需求缺口:還沒到重試時間,舊 Dispatcher 就已呼叫通知服務。保留 RED 也能確認新測試真的攔得住舊行為;若 Agent 同時改完 Tests 與 Production,最後只剩綠燈,就無法證明測試曾經看見缺口。

兩種候選都必須同步修改 Outbox 欄位、掃描條件與 Atomic Claim

不論規則放在哪裡,兩種結構都需要替 Outbox 保存下一次可處理時間:

/// <summary>
/// 取得下一次允許 Dispatcher 處理的 UTC 時間。
/// </summary>
public DateTimeOffset NextAttemptAtUtc { get; private set; }

新訊息建立時,NextAttemptAtUtc 等於 CreatedAtUtc,因此可以立即處理。

掃描條件要排除尚未到時間的訊息:

message.Status == "Pending" &&
message.NextAttemptAtUtc <= scanUtc &&
(message.LeaseToken == null ||
    message.LeaseExpiresAtUtc <= scanUtc)

真正取得處理權的 Atomic Claim,也要再次檢查:

message.Id == messageId &&
message.Status == "Pending" &&
message.NextAttemptAtUtc <= claimUtc &&
(message.LeaseToken == null ||
    message.LeaseExpiresAtUtc <= claimUtc)

掃描只是列出當下看來符合資格的候選;Atomic Claim 才在資料庫更新那一刻重新確認資格並取得處理權。兩個動作之間,狀態、Lease 或下一次處理時間都可能被另一個 Dispatcher 改變,所以相同時間條件必須在 Claim 時再檢查。

ExecuteUpdateAsync 會把 Claim 條件轉成資料庫更新的 WHERE,再以受影響列數判斷目前 Dispatcher 是否取得處理權。只在掃描時檢查,單一流程可能通過,並行時仍可能繞過時間限制。

整合式候選把重試矩陣留在 Dispatcher

三次整合式實驗都採用相同方向:新增私有函式,讓 Dispatcher 自己計算等待時間。

private static TimeSpan GetRetryDelay(
    string notificationKind,
    int attemptCount) =>
    (notificationKind, attemptCount) switch
    {
        ("Escalation", 1) => TimeSpan.FromMinutes(5),
        ("Escalation", _) => TimeSpan.FromMinutes(30),
        (_, 1) => TimeSpan.FromMinutes(15),
        _ => TimeSpan.FromMinutes(60)
    };

一般失敗時,Dispatcher 計算下一次時間,再存回 SQLite:

var nextAttemptAtUtc = timeProvider.GetUtcNow().Add(
    GetRetryDelay(
        message.NotificationKind,
        message.AttemptCount));

await ScheduleRetryAsync(
    messageId,
    leaseToken,
    nextAttemptAtUtc,
    cancellationToken);

在單一 Dispatcher、規則固定的小型服務裡,這個版本很合理。四格規則只有一份,方法名稱也能表達意圖,閱讀時不必跳到另一個類別。

整合式 Dispatcher 承接新重試規則

圖:整合式 Dispatcher 修改短、跳轉少,但重試決策與 EF/Gateway 協調留在同一類別。

Policy 分離式候選讓純決策承接重試規則

三次 Policy 分離式實驗都把重試矩陣加入第一階段已存在的 NotificationDispatchPolicy,沒有再新增 Retry Service、Strategy Interface 或新架構層。

Policy Run 03 最後留下這段實作:

public DateTimeOffset CalculateNextAttemptAtUtc(
    string notificationKind,
    int attemptCount,
    DateTimeOffset failureUtc)
{
    var isFirstAttempt = attemptCount == 1;
    var retryDelay = SelectDeliveryRoute(notificationKind) switch
    {
        NotificationDeliveryRoute.Escalation
            when isFirstAttempt =>
                EscalationFirstRetryDelay,
        NotificationDeliveryRoute.Escalation =>
            EscalationSubsequentRetryDelay,
        NotificationDeliveryRoute.Overdue
            when isFirstAttempt =>
                OverdueFirstRetryDelay,
        _ => OverdueSubsequentRetryDelay
    };

    return failureUtc.Add(retryDelay);
}

重試矩陣直接重用 SelectDeliveryRoute。通知種類只在 NotificationDispatchPolicy 解析一次,Dispatcher 不必再維護另一份 NotificationKind 判斷。

Dispatcher 只負責取得失敗時間、詢問 Policy,再把結果存回 SQLite:

var nextAttemptAtUtc =
    dispatchPolicy.CalculateNextAttemptAtUtc(
        message.NotificationKind,
        message.AttemptCount,
        timeProvider.GetUtcNow());

await ScheduleRetryAsync(
    messageId,
    leaseToken,
    nextAttemptAtUtc,
    cancellationToken);

兩種依賴方向可以簡化成:

flowchart LR
    subgraph Integrated["整合式候選"]
        ID[NotificationOutboxDispatcher]
        IR[私有重試矩陣]
        ID --> IR
        ID --> IDB[(EF Core / SQLite)]
        ID --> IG[Notification Gateway]
        ID --> IT[TimeProvider]
    end

    subgraph Separated["Policy 分離式候選"]
        SD[NotificationOutboxDispatcher]
        SP[NotificationDispatchPolicy]
        SD --> SP
        SD --> SDB[(EF Core / SQLite)]
        SD --> SG[Notification Gateway]
        SD --> ST[TimeProvider]
        SP --> SR[Route / Lease / Retry / Completion]
    end

Policy 不知道 EF Core、Gateway 與 TimeProvider。它只接收已取得的事實,回傳決策結果。

Policy 分離式 Dispatcher 承接新重試規則

圖:Policy 分離式多一個概念,換來通知路由、Lease、Retry 與 Completion 的單一決策落點。

兩種結構都維持行為,差別在重試規則與 I/O 是否混在一起

Agent 完成後,主流程逐一重跑 Release Build、完整 Tests、Format、Diff Check、套件弱點掃描與 API Smoke,確認六份候選都維持既有行為與新的時間邊界。

比較面向 整合式 Dispatcher Policy 分離式 Dispatcher
既有行為測試 全部通過 全部通過
新的時間邊界測試 全部通過 全部通過
重試規則位置 Dispatcher 私有函式 純 Policy
I/O 與規則 留在同一類別 分開
第二次修改量 較短 較大
閱讀方式 跳轉少,完整流程集中 可單獨閱讀規則,但多一個概念
未來新增規則 繼續修改 Dispatcher 優先修改 Policy,Dispatcher 保持 I/O 編排

第二次 Diff 的確是整合式較短,但行數沒有說明規則落點。整合式把 EF 查詢、Gateway 協調與重試矩陣留在同一個 Dispatcher;Policy 分離式則把「通知種類 × 嘗試次數」集中在不依賴外部技術的決策物件。

Policy 分離式先支付一個類別、一項依賴與一次閱讀跳轉;重試矩陣出現後,新規則直接落進既有 Policy,這筆成本才開始產生用途。

完整測試數、每次程式碼差異與主流程驗證都保留在公開證據,正文不拿測試總數或行數替結構排名。

三次 Token 樣本沒有顯示哪種結構一定更省

Policy 分離組的平均 Input/Output Token 較低,執行時間與工具呼叫次數卻比較高。每組只有三次,而且 Agent 的探索、測試輸出與重跑路徑不同,不能因此宣稱「拆出 Policy 可以固定節省多少 Token」。

這次能直接確認的是:三個 Policy 分離式 Run 都把重試矩陣放進既有 NotificationDispatchPolicy,修改落點相當一致。這不等於 Agent 一定理解得更快,也無法讓錯誤抽象突然變成好設計。

本系列沿用第三次 Policy 結果:重試矩陣有唯一落點,過時的 ReleaseLease 也完成改名

本系列接續 policy-separated-pressure-run-03

  • Commit:2ae2de5
  • Annotated Tag:day-20-two-values

可以直接切換:

git fetch --tags
git switch --detach day-20-two-values

我接受 Run 03 的理由,都能從目前 Repository 找到證據:

  1. 重試已形成真實變化軸:四個等待時間由「通知種類 × 嘗試次數」共同決定。錯誤類型、Provider 或 SLA 日後若也加入矩陣,才是下一次重新評估邊界的條件。
  2. 第一階段的 Policy 被下一項需求實際使用:Lease、路由、完成決策與重試時間都在描述通知傳送生命週期,不是替未知需求留下的空殼。
  3. 名稱跟著行為更新:一般失敗不再只是釋放 Lease,還會排定下次時間,因此 ReleaseLease 改成更符合意圖的 ScheduleRetry

代價也很明確:累積 Diff 較大,閱讀流程時多一次類別跳轉。這次我願意支付,因為重試矩陣已經重用既有路由判斷,而且只有一個規則來源。

換一種專案情境,整合式 Dispatcher 可能才是較好的選擇

這項決策只適用於目前的 Work Item API、這組重試矩陣與既有通知流程。

專案情境 比較適合的方向 判斷理由
單一 Dispatcher、固定少量規則,近期沒有獨立變化 整合式私有函式 跳轉少、修改小,規則仍有唯一來源
通知種類、嘗試次數、SLA 或錯誤類型開始形成矩陣 具體 Policy 純決策能獨立閱讀與測試,I/O 編排保持穩定
不同租戶或 Provider 需要替換不同重試行為 設定物件或 Strategy 已出現明確替換需求,此時新增抽象才有用途
只處理一次遠端呼叫的短暫性錯誤 平台或 SDK 內建 Retry 避免自行重做成熟機制,但仍要驗證逾時與冪等性
Provider 不具冪等性,lost ACK 可能造成重複副作用 先處理冪等與投遞保證 Backoff 只能延後重複發生的時間,不能消除重複效果

如果重試需求從未出現,而且 Lease、路由與完成狀態仍然一起改變,第一階段保持零修改的整合式候選會更合適。此時先新增 Policy,主要只會多出一項依賴與閱讀跳轉。

把結構選擇條件整理成可放進 AGENTS.md 的 Repository Instruction

老樣子~我把這次選擇條件整理成一份可放進 AGENTS.md 的範例。這裡的 Policy 指寫給 Agent 的 Repository Instruction,和前面的 NotificationDispatchPolicy 程式類別是兩件事。

這份規則是實驗完成後才整理的,沒有回頭放進本次 Prompt,也尚未寫入 API Demo;否則會污染兩種結構的比較。

## Notification Dispatch Structure Policy

- 單一 Dispatcher、規則固定且共同改變時,優先保留具名私有函式,不為展示分層新增 Policy。
- 當通知種類、嘗試次數、錯誤類型或 SLA 形成獨立規則矩陣時,將純決策移到具體 Policy。
- 只有出現兩個以上可替換實作、租戶差異或 Provider 差異時,才考慮 Strategy 與 Interface。
- 單次遠端呼叫的短暫性錯誤,先評估 SDK 或平台提供的 Retry;不得與 Outbox 的持久化排程重複重試。
- Provider 不具冪等性時,先說明重複副作用與投遞保證,Backoff 不得被宣稱為 Exactly Once。
- 掃描資格與 Atomic Claim 必須使用相同的時間、狀態與 Lease 邊界。
- 重構不得改變 HTTP Contract、取消傳遞、lost ACK、Idempotency Key 與既有外部副作用。
- 完成後回報真實變化軸、規則落點、新增依賴、保留的替代方案與停止理由。

等這套判斷流程在更多專案與情境下穩定後,抽成 Skill 會比長期堆在 AGENTS.md 更合適。

CLEAN 原則

C — Context-Aware Code 情境感知:抽象必須有專案裡的真實證據

第一階段只有單一 Consumer,也沒有獨立重試規則,整合式候選停在零修改是合理結果。Policy 分離式候選則把不依賴 EF Core、Gateway 與時間來源的決策集中起來。

第二階段出現「通知種類 × 嘗試次數」矩陣後,Policy 分離才取得足夠的 Repository 證據。C — Context-Aware Code 情境感知 要我用這些真實使用情境判斷抽象是否值得留下。

N — Non-Surprising Behavior 符合預期:內部結構改變,外部答案不能跟著漂移

第二階段新增 NextAttemptAtUtc,也改變失敗後何時可以再次處理。如果只顧著整理結構,很容易順便破壞取消、Lease、lost ACK 或冪等語意。

因此兩種結構都必須保留:

  • HTTP Route、JSON 與 Status Code。
  • 雙 Dispatcher 只能有一個取得有效 Lease。
  • lost ACK 必須沿用相同 Idempotency Key。
  • 一般失敗要排定下次處理時間;取消則保留 Lease 並向外傳遞。
  • 第一次執行會完成應處理的通知,重跑時不得再次產生相同副作用。

重試時間與內部責任可以調整;HTTP Contract、取消傳遞、Lease、lost ACK、冪等鍵與副作用次數則必須維持原本語意。

這次結果不能直接套用到舊資料庫、非冪等服務與所有專案

  • Demo 使用全新 SQLite,沒有驗證既有資料庫的欄位升級、回填與回復流程。正式系統仍要另外設計 Migration、預設時間與 Rollback。
  • Provider 不具冪等性時,系統仍只有 At-least-once 保證;Backoff 只能延後可能重複的外部效果,無法把 lost ACK 造成的兩次副作用變成一次。
  • 第二階段六個工作階段只涵蓋這個 Repository、這組 Prompt 與固定重試矩陣,不能外推到支付、權限、批次處理或多租戶系統。

測試確認兩份候選都能運作,保密的新需求才看出規則落點

回到標題,兩份程式碼都通過測試,只能證明它們目前具有相同行為價值。加入事前保密的重試規則後,才看見結構價值的差異:整合式候選用較短修改把矩陣留在 Dispatcher;Policy 分離式則讓三次 Agent 都找到同一個純決策落點,穩定的 EF 與 Gateway 編排不必承擔規則內容。

我最後接受 Policy Run 03,因為重試矩陣已經是真實需求,也重用了既有路由判斷。這份選擇不是由 Token、行數或設計模式名稱決定;如果矩陣沒有出現,整合式版本仍會是更精簡的答案。

〈軟體的兩種價值〉讓我把「現在能運作」和「下一次仍容易修改」分開驗收。CLEAN 則把它轉成 User 的操作:提供 Repository 真實情境、固定不能漂移的行為,再要求 Agent 回報新規則落點、穩定責任是否被迫修改,以及為什麼應該在這個抽象層次停下來。

明天會接著談〈獨立性〉。Policy 與 Dispatcher 分開,只回答原始碼中的一小部分邊界;Use Case、執行方式、團隊開發與部署能不能各自改變,還要分開檢查。

參考資料


上一篇
Day 19|兩個執行流程同時處理同一筆 Work Item,為什麼會重複通知?從競態條件到 Outbox 與冪等設計
下一篇
Day 21|拆成多個 Project,業務流程、執行方式、團隊與部署就真的獨立了嗎?
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言