iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Software Development

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

Day 15|第三條規則由不同負責人維護,SRP/OCP 要求 AI 把責任拆到哪裡?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天只有兩條通知升級規則,而且都由同一位服務等級政策負責人維護。我因此把判斷集中在 RequiresEscalationNotification,沒有再建立 Interface 或規則集合。對當時的需求來說,一個具名方法已經夠用。

今天,第三條規則真的來了:

尚未指派的工作項目一到期,就使用 Escalation Notification,不等待 24 或 72 小時。

它沿用既有的 Assignee,API 欄位與 Database Schema 都不用改。真正改變昨天答案的,不是規則數量從二變成三,而是出現了第二個變更來源:派工管理負責人。

九份候選最後都守住功能。直接擴充的版本型別最少,共同 Rule Interface 能讓 Processor 不必因新規則修改;我最後留下的,卻是兩個具體 Policy。原因是目前已知的只有兩個 Actor,而且所有規則仍一起編譯、一起部署。兩個具名 Policy 已足夠隔離修改,不需要先建立一套共同規則平台。

SRP(Single Responsibility Principle,單一職責原則)所說的 Actor,指的是會提出同一類變更要求的人,或代表同一項狹義業務功能的群體。本篇的服務等級與派工管理負責人都是實驗設定,用來模擬兩種不同政策來源,不代表任何真實公司的組織分工。

《無瑕的程式碼 第二版》的〈SOLID 原則〉把五項原則整理成設計層的快速參照,用來思考函式與資料結構該怎麼安排成 Class,以及 Class 之間該如何連接。本篇先聚焦 SRP 與 OCP:第二個 Actor 出現後,原本的具名方法還夠不夠用?昨天沒有採用的抽象,今天又該增加到哪裡才停?

先完整認識 SOLID:五項原則分別檢查責任、擴充、替換、介面與依賴

五項原則看的不是同一件事。SRP 找變更來源,OCP 決定哪一種變化值得保護;建立抽象後,再用 LSP、ISP 與 DIP 檢查替換行為、使用端需要的介面,以及原始碼依賴方向。是否新增 Interface 或 Class,仍要回到這些問題判斷。

原則 判斷的設計問題 Work Item API 對照 AI Coding 常見誤判
S — SRP/Single Responsibility Principle 單一職責原則 哪一類 Actor 或變更理由會修改這段程式碼? 服務等級政策與派工政策是否應有各自的修改位置? 只看行數或 Method 數量就拆 Class。
O — OCP/Open-Closed Principle 開放封閉原則 哪一種預期變化值得建立擴充邊界?哪段穩定流程要受到保護? 增加 Escalation Policy 時,是否需要反覆修改通知與 Save 流程? 每個 if 都抽成 Interface 或 Strategy。
L — LSP/Liskov Substitution Principle 里氏替換原則 替代實作能否守住呼叫端原本依賴的行為契約? 換掉 Notification Gateway 後,失敗、Exception 與 Cancellation 語意是否相同? 能編譯、能透過 DI 切換,就當成可以替換。
I — ISP/Interface Segregation Principle 介面隔離原則 使用端是否只依賴自己實際需要的能力? Processor 是否被迫依賴通知管理、設定或查詢等用不到的方法? 把 Interface 拆得越小越好,卻不看實際使用端(Consumer)。
D — DIP/Dependency Inversion Principle 依賴反轉原則 原始碼依賴是否朝向穩定的高階政策? Processor 依賴通知行為契約,還是直接依賴特定供應商 SDK? 使用 DI Container,就宣稱已經完成依賴反轉。

S — SRP 單一職責原則:用變更來源判斷責任

SRP 先問:這個模組改變時,誰會提出要求?因為相同理由而改變的程式碼適合放在一起;變更理由不同時,設計要避免其中一方的修改牽動另一方。

Actor 數量不會直接換算成 Class 數量。隔離可以發生在方法、類別或元件;一個 Actor 也可能需要多個 Class 協作。今天真正要驗收的,是兩位負責人下一次改政策時,能不能各自找到清楚、可獨立測試的修改位置。

O — OCP 開放封閉原則:先選定要保護的預期變化

Strategic Closure 可理解成「策略性封閉」:只針對已知、反覆出現或風險較高的變化建立穩定邊界。新增 if 或修改既有檔案不會自動違反 OCP;當同類需求每次都要同步修改穩定流程,才形成值得處理的抽象壓力。

任何系統都無法對所有未來同時封閉。今天要保護的是通知升級政策持續增加時,逾期查詢、通知、失敗統計與 Save 順序不必跟著重寫;DI Registration、Policy 本身與 Tests 仍然可以是明確修改點。

L — LSP 里氏替換原則:替換要守住行為契約

LSP 檢查替代實作能否維持呼叫端依賴的結果、失敗方式與副作用。Notification Gateway 即使實作相同 Interface,若把 false 改成 Exception、忽略 Cancellation,或自行改變重試次數,呼叫端仍不能安心替換。這會是明天第二個 Provider 的主要問題。

I — ISP 介面隔離原則:介面由 Consumer 的需要決定

ISP 要求使用端只依賴自己會用到的能力。OverdueWorkItemProcessor 若只需要送通知,就不該同時依賴範本管理與通知歷史;反過來,同一個 Use Case 本來就需要兩個緊密相關的方法時,硬拆成兩個 Interface 也不會更乾淨。

D — DIP 依賴反轉原則:高階政策擁有自己需要的契約

DIP 檢查原始碼依賴方向。高階流程先定義自己需要的通知能力,低階 Adapter 再去實作;DI Container 只負責建立與組合物件。Processor 若仍直接引用供應商 SDK,就算改成 Constructor Injection,依賴方向也沒有反轉。

今天先用第二個 Actor 驗證 SRP 與 OCP。明天換上第二個通知 Provider,再實際檢查 LSP 的替換行為、ISP 的 Consumer 契約與 DIP 的依賴方向。

本篇用兩個問題實驗 SRP/OCP:誰會改規則,哪一種變化會持續增加

只寫「請遵守 SRP 與 OCP」,沒有交代 Actor、預期變化與停止條件,Agent 仍得自己猜責任邊界。它可能建立一整套擴充架構,也可能把新條件留在原方法;光看 SOLID 名稱,兩種結果都無法驗收。

我不會直接接受 Agent 說自己「符合 SOLID」。這次改看兩項可查結果:服務等級與派工政策能否各自修改、各自審查;同類政策繼續增加時,哪些穩定流程可以維持不動。

本次受控情境如下:

Actor/變更來源 負責的政策 會讀取的資料
服務等級政策負責人 High 逾期 24 小時升級;所有 Priority 逾期 72 小時升級 PriorityDueAtUtc、目前時間
派工管理負責人 未指派工作項目一到期就升級 AssigneeDueAtUtc、目前時間

程式碼裡的 if 與欄位只能顯示資料形狀,無法推回組織內的決策權責。SRP 所需的 Actor 證據,必須來自需求、Repository 文件或團隊分工。

OCP 這次保護的變化方向是「持續加入 Escalation Policy」;希望保持穩定的部分包括逾期資料查詢、通知路由、失敗統計與 Save 順序。它不追求整個系統零修改:若最後採用 Rule Set,新增 Policy 時仍可能要修改 DI Registration、Tests 或設定。

SRP 先辨認 Actor 與變更理由

圖:SRP 先辨認兩個政策由誰、因何改變;欄位與 if 本身無法提供 Actor 證據。

今天用五個問題比較三份設計:

  1. 兩個 Actor 的規則放在哪裡?
  2. 同一個 Actor 再改規則時,需要碰哪些地方?
  3. 第三個 Actor 出現時,需要碰哪些地方?
  4. 哪一段穩定流程受到保護?
  5. 為了保護這段流程,現在增加多少型別、依賴與組合成本?

先固定既有兩條規則,以及新政策不能改壞的行為

九次實驗都從昨天接受的具名規則開始:

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;
}

對昨天的需求來說,這段程式碼剛好夠用。今天的方法會多讀一個 Assignee,但增加參數本身沒有提供 SRP 的分離證據。設計判斷之所以改變,是因為這條規則由第二個 Actor 維護。

本次固定以下行為:

  • Assigneenull、空字串或純空白,都算未指派。
  • 未指派、Normal、剛好到期,使用 Escalation Notification。
  • 已指派、Normal、剛好到期,仍使用一般 Overdue Notification。
  • 已指派 High 24 小時與已指派 Normal 72 小時規則維持。
  • 已是 Overdue 的未指派項目重跑時,仍再次送出 Escalation,ProcessedCount 不重複增加。
  • Escalation Gateway 回傳 false 時,失敗摘要與最終 Overdue 狀態維持。
  • Exception、Cancellation、通知先於 Save、HTTP Contract 與 SQLite Schema 不變。

若想重現實驗,可以從公開的 AI-CleanCode-API-Demo 切到相同起點:

  • Commit:fb22f445f383c7977dfac71f35cc028b0ca53e00
  • Baseline Annotated Tag:day-15-solid-srp-ocp-baseline
  • Model:Codex GPT-5.6-SOL-HIGH
  • Reasoning effort:high
git clone https://github.com/eric861129/AI-CleanCode-API-Demo.git
Set-Location AI-CleanCode-API-Demo
git switch --detach day-15-solid-srp-ocp-baseline

三種受控輸入分別測直接擴充、Actor 分離與共同 Rule Interface

這次刻意把抽象程度拉開:一組直接修改既有方法,一組依 Actor 建立具體 Policy,另一組建立共同 Rule Interface。三組分別觀察最少結構、責任分離與 OCP 邊界的成本。

實驗方向 刻意施加的限制 想回答的問題
Conditional Extension 直接擴充既有方法,不新增 Production 型別 最少結構能否保留兩個 Actor 的責任線索?
Actor-separated Policies 依 Actor 建立兩個具體 Policy,不建立共同 Interface 已出現不同變更來源時,責任分離是否已經值得?
Interface-based Rule Set 建立 Rule Interface、DI 集合與明確 Registration 提早保護 Processor 要付出多少抽象與組合成本?

每種方向各執行三個彼此獨立、不沿用前次對話與修改結果的 Codex Session,共九次。三組都從相同 Commit 開始,使用相同模型、Reasoning effort、測試入口與修改範圍。比較焦點是 User 給出的設計方向;不過三份 Prompt 也同時改變允許的抽象、禁止事項與停止條件,因此這是三種策略的重複觀察,不能當成單一提示詞造成的嚴格因果。

三組共同條件包括:

既有 High 24 小時與所有 Priority 72 小時規則,
由服務等級政策負責人維護。

新規則「未指派工作項目一到期就升級」,
由派工管理負責人維護。

Assignee 為 null、空字串或純空白時,都視為未指派。
保留重跑、失敗摘要、Exception、Cancellation、通知先於 Save、
HTTP Contract 與 Database Schema。

先取得能說明需求缺口的 Red,再完成最小實作。
最後執行完整 Tests、dotnet format 與 git diff --check。

三份 Prompt 的差異如下。

Conditional Extension:直接擴充既有方法

直接擴充既有 RequiresEscalationNotification。
不新增 Production Class、Interface、Policy、Strategy、Factory、Resolver、
Rule Collection、Reflection、Plugin、外部設定或新的部署單位。
在同一個方法內,讓服務等級與派工管理的條件仍可辨識。

這一組保留最少結構。若兩個 Actor 的條件放進同一個方法後仍能清楚辨認,也許還不必拆 Class;若下一次修改無法避開另一位 Actor 的規則,SRP 壓力就會出現。

Actor-separated Policies:依兩個 Actor 建立具體 Policy

依兩個 Actor/變更理由建立兩個小型、具名且可獨立測試的 Production Policy。
OverdueWorkItemProcessor 只組合兩份 Policy 的結果。
使用具體型別依賴,不建立共同 Rule Interface、IEnumerable 規則集合、
Factory、Resolver、Reflection 掃描、Plugin 載入或外部設定。

這一組把兩個 Actor 明確交給 Agent,並禁止建立共同 Interface。我想確認的是:不同變更來源已經出現時,兩個具名 Policy 是否就能把責任拆清楚,不必立刻升級成通用規則平台。

Interface-based Rule Set:建立共同 Rule Interface 與 DI 集合

建立 Escalation Rule Interface,由個別規則實作判斷。
用 ASP.NET Core DI 註冊規則,並以集合或 Policy 組合結果。
Processor 不保存個別門檻、Priority 或未指派判斷細節。
新增規則以新增實作與明確 Registration 為主要路徑。
不使用 Reflection/Assembly 掃描、動態 Plugin 載入、外部設定或新的部署單位。

這一組把規則移出 Processor,觀察共同 Interface 與 DI 集合能不能讓後續 Policy 以新增實作的方式加入。所有 Rule 仍和 API 一起編譯、一起部署;動態 Plugin 需要處理的載入、版本與安全問題不在本次範圍內。

Conditional ExtensionActor-separated PoliciesInterface-based Rule Set 都是本系列定義的實驗方向,不是書中的正式分類。正文把共同條件抽出來方便比較;三份逐字原始 Prompt 保留在 Day 15 公開實驗資料

新規則讓舊測試誤判:High 24 小時案例被「未指派」條件意外救綠

同一筆資料若同時命中服務等級與派工政策,測試通過時就不知道是哪條規則生效。因此我從公開 HTTP 入口 POST /api/work-items/process-overdue 加入三項 Host Oracle,分別檢查新政策、重跑與通知失敗:

  1. 未指派 Normal 剛好到期要走 Escalation;已指派 Normal 剛好到期仍走一般通知;既有 24/72 小時規則保留。
  2. 已是 Overdue 的未指派項目重跑時要再次 Escalation,ProcessedCount 仍為 0。
  3. 未指派項目的 Escalation 回傳 false 時,要留下失敗摘要並保存 Overdue。

這三項測試放回 Baseline 後全部轉紅,而且都停在未指派項目沒有送出 Escalation;這才確認 Red 來自尚未實作的新需求。

這裡我也踩到另一個容易被綠燈掩蓋的問題。原本測 24/72 小時邊界的資料,Assignee 預設也是 null。新規則加入後,如果不校正測試資料,High 24 小時案例即使把時間條件寫壞,也可能因為「未指派」而照樣走 Escalation。

測試是綠的,原本的服務等級政策卻沒有被驗到。

所以九份候選都先把既有服務等級案例設為已指派,讓它們只驗收 24/72 小時門檻;未指派政策使用另一組資料。這樣才能辨認到底是哪一項政策讓測試通過。

WorkItem.AssignTo() 會先執行 Trim(),純空白最後會變成空字串。部分候選測試因此使用 Test seam,也就是只為測試開放的受控入口,直接保存原始空白 Assignee。這能確認 Processor 從資料庫讀回純空白後仍會判定為未指派,而且不會改變正式 API 的輸入流程。

三種設計都守住行為,差別在兩個 Actor 下一次要修改哪裡

三種方向各跑三次,九份候選都通過相同的三項 Host Oracle。主流程也重新完成套件還原、Release Build、完整測試、格式與 Diff 檢查,以及 HTTP Smoke,確認接下來比較的是設計差異,不是某份候選根本跑不起來。

設計方向 兩個 Actor 的規則落點 同 Actor 再加規則 新 Actor 加入 目前成本
Conditional Extension 同一個具名方法 修改同一方法 繼續擴張同一方法 最少型別與跳轉
Actor-separated Policies 各自具體 Policy 修改自己的 Policy Processor 與 DI 仍要調整 兩個 Class、Registration 與對應測試
Interface-based Rule Set 共同 Interface 下的多個 Rule 新增實作與 Registration Processor 維持不動 Interface、集合、DI 與 Rule 粒度判斷

精確 Diff 仍保留在公開資料:Conditional 每次修改 2 個檔案,Actor-separated 修改 6~7 個,Interface-based 也修改 6~7 個。這些數字只描述修改面積,設計判斷仍要回到 Actor 與規則落點。

Conditional Extension:最少結構仍把兩個 Actor 綁在同一個方法

它把新條件直接加進具名方法:

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

    return isUnassignedWorkItem ||
           reachedPriorityIndependentEscalationThreshold ||
           reachedHighPriorityEscalationThreshold;
}

最少結構同樣能完成目前需求。呼叫端只多傳一個 Assignee,沒有新增 Production 類別、DI 或 Interface。

如果三條規則由同一位政策負責人維護,或派工規則只是短期例外,我會接受這份程式碼。本次受控情境已指定兩個不同 Actor,兩邊下一次改政策時仍會進入同一個方法,兩類修改責任依然混在一起。

Actor-separated Policies:兩個已知變更理由各自擁有修改位置

這組把既有兩條服務等級規則放進 ServiceLevelEscalationPolicy,新規則則放進 AssignmentManagementEscalationPolicy

public sealed class ServiceLevelEscalationPolicy
{
    public 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;
    }
}
public sealed class AssignmentManagementEscalationPolicy
{
    public bool RequiresEscalationNotification(
        string? assignee,
        DateTimeOffset dueAtUtc,
        DateTimeOffset currentUtc) =>
        string.IsNullOrWhiteSpace(assignee) && dueAtUtc <= currentUtc;
}

Processor 只組合兩份結果:

var requiresEscalationNotification =
    serviceLevelEscalationPolicy.RequiresEscalationNotification(
        workItem.Priority,
        workItem.DueAtUtc,
        currentUtc) ||
    assignmentManagementEscalationPolicy.RequiresEscalationNotification(
        workItem.Assignee,
        workItem.DueAtUtc,
        currentUtc);

這個版本把兩個已知變更理由寫進名稱與測試。服務等級負責人修改 24/72 小時門檻時,只需要進入自己的 Policy;派工管理負責人也有獨立落點。

代價是兩個新 Class、兩筆 DI Registration、Processor 多兩個具體相依物,以及對應的 Policy Tests。這份設計先取得 SRP 的責任邊界;第三個 Actor 出現時,Processor 仍然會修改。它不是終極架構,只是目前壓力下剛好足夠的一步。

Interface-based Rule Set:保護 Processor,卻把責任粒度留給 Agent 猜

這組建立共同介面,再由 Processor 組合 DI 注入的規則:

public interface IEscalationRule
{
    bool RequiresEscalation(WorkItem workItem, DateTimeOffset currentUtc);
}
private bool RequiresEscalationNotification(
    WorkItem workItem,
    DateTimeOffset currentUtc) =>
    escalationRules.Any(rule => rule.RequiresEscalation(workItem, currentUtc));

新增規則時,Processor 不必修改,但 Program.cs 仍要明確註冊:

builder.Services.AddScoped<IEscalationRule, ServiceLevelEscalationRule>();
builder.Services.AddScoped<IEscalationRule, UnassignedWorkItemEscalationRule>();

同一份 Interface Prompt 跑三次,兩次得到「一條條件一個 Rule」,另一次得到「一個 Actor 一個 Rule」。三份都通過測試,責任粒度卻分成兩種。共同 Interface 固定了呼叫形狀,沒有替 User 補上「一個 Actor 一個 Rule」或「一條條件一個 Rule」的業務決策。

缺少 Actor Context 時,Agent 很容易把每一個 if 都拆成 Class,最後得到技術上可擴充、業務責任卻更模糊的結構。OCP 讓修改集中,沒有讓修改消失;新 Rule 仍要進入 Composition Root 註冊。

SRP 與 OCP 的三種結構採用條件

圖:規則少且同一 Actor 時可以直接擴充;Actor 分離後建立 Policy;同類規則持續增加時才考慮共同介面。

檔案更多不代表 Agent 一定讀得更多:Token 只能比較同品質候選

每個方向都有三個獨立工作階段。下表是三次平均:

設計方向 Fresh Input Token Output Token Tool Calls 平均時間
Conditional Extension 111,136 17,383 42.3 617.0 秒
Actor-separated Policies 103,281 18,492 33.7 636.3 秒
Interface-based Rule Set 120,605 19,958 53.0 606.1 秒

Actor-separated 修改較多檔案,平均 Fresh Input Token 與 Tool Calls 卻低於 Conditional,因此檔案數不能直接預測 Agent 的閱讀成本。Interface-based 同時出現最高的 Fresh Input、Output 與 Tool Calls,也新增較多抽象與組裝位置;兩者在這次實驗中同時發生,但九個 Session 不足以證明額外結構是唯一原因。

Token 只用來比較已通過相同行為 Gate 的候選工作成本。它不能換算成「拆一個 Class 固定省多少 Token」,也不能取代責任與修改路徑的判斷。

本次接受兩個具體 Policy,另外兩種方向保留明確採用條件

本系列接下來沿用:

  • Commit:ccbc136daa59cfcd430d44ebd4cb62b57b4ab72d
  • Annotated Tag:day-15-solid-srp-ocp
  • 正式候選 Tag:day-15-formal-actor-separated-policies-run-01

讀者可以直接切換:

git switch --detach day-15-solid-srp-ocp

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

我依目前已知的兩個變更來源選擇 Actor-separated Policies。其他方向仍有明確的採用條件:

情境 採用方向 原因
規則由同一 Actor 維護、數量少且穩定 Conditional Extension 一個具名方法即可完整閱讀,結構與跳轉最少
已知有不同 Actor,需要獨立修改與測試 Actor-separated Policies 變更理由能從名稱、依賴與測試直接辨認
Actor 或執行期組合持續增加 Interface-based Rule Set 新增 Rule 時可保護 Processor,組合集中到 Composition Root
需要獨立建置、發現、載入、版本隔離或部署 Plugin Architecture 這些需求才足以支持 Contract Assembly 與動態載入成本

目前完成的是 SRP 的責任分離,以及 OCP 的局部 Strategic Closure:同一個 Actor 增加規則時,可以只修改自己的 Policy。第三個 Actor 出現後,Processor、建構式或 Composition Root 仍可能需要調整;等這種擴充真的重複發生,再評估共同 Rule Interface。

AI Coding 要先提供 Actor 與停止條件,「請遵守 SOLID」無法決定責任邊界

如果 User 只寫:

新增未指派工作項目要立刻升級的規則,請遵守 SRP 與 OCP。

Agent 還缺少幾個會改變架構的事實:誰會維護新規則?既有兩條規則由誰負責?未來是否要執行期切換?Plugin 能否獨立部署?哪些流程已經穩定,不希望再修改?

任務沒有交代 Actor、部署方式與替換需求時,Agent 只能依常見 Pattern 推測。我因此把已知情境、限制與停止條件一起寫進單次 Prompt:

既有 High 24 小時與所有 Priority 72 小時規則,由服務等級政策負責人維護。
新規則「未指派工作項目一到期就升級」由派工管理負責人維護。

請先用公開 HTTP 行為測試隔離兩位負責人的規則,再依變更理由整理責任。
目前所有規則一起編譯、一起部署,沒有執行期切換、外部模組發現或獨立部署需求。
若兩個具名具體 Policy 已足以隔離修改,停止,不要再新增共同 Rule Interface、
Factory、Resolver 或 Plugin Loader。

完成後回報:每個 Actor 的修改位置、Processor 仍會因何種變化而修改、
未來升級成 Rule Set 所需要的具體壓力,以及刻意沒有建立的抽象。

User 只要補上程式碼看不到的 Actor、部署方式與停止條件,不必替 Agent 指定每個 Class 和方法名稱。ServiceLevelEscalationPolicyAssignmentManagementEscalationPolicy 則提供清楚的搜尋入口,下一次修改可以先進入對應政策。這類命名通常能減少重新理解規則的工作,但本次實驗不能把它換算成固定的 Token 節省比例。

把三種採用條件寫進 Repository Policy,讓 Agent 知道何時直接改、何時拆 Policy

老樣子~這些判斷會跨任務重複使用,我先把它們整理成一份可以放進 AGENTS.md 的 Policy 範例:

## SOLID Change Boundary Policy

- 套用 SRP 前,先列出需求的 Actor、政策來源與變更理由;不得只用行數、
  Method 數量或「只做一件事」決定拆分。
- 同一 Actor、同一政策來源且會一起變更的少量規則,允許留在一個具名方法。
- 不同 Actor 的規則應有可辨識、可獨立測試的修改位置;先考慮具體 Policy,
  不因 Actor 數量直接建立共同 Interface。
- 套用 OCP 前,先命名要保護的預期變化,以及刻意保留的修改點;不得宣稱
  系統能對所有未來需求封閉。
- 相同擴充方向反覆出現,或需要執行期組合、第二個 Consumer、不同生命週期時,
  再建立共同 Interface、Strategy 或 Rule Collection。
- DI Registration 是修改點,不得把 Processor 零修改描述成整個系統零修改。
- 沒有獨立建置、發現、載入、版本隔離或部署需求時,不建立動態 Plugin 機制。
- 先用可觀察行為測試隔離各項政策,避免新規則讓舊測試因另一條件而假陽性。
- 保留 HTTP Contract、Database Schema、通知順序、失敗語意、Exception、
  Cancellation 與重跑行為,除非任務需求明確允許修改。
- 完成後回報新增概念、Actor 對應、Composition Root、同 Actor 與不同 Actor
  的下一次修改位置、驗證結果與停止理由。

Repository Policy 能讓 Agent 每次先找變更證據,再決定要不要抽象。實際的 Actor、政策來源與部署情境,仍要由 User 或 Repository 提供。

這份 SOLID Change Boundary Policy 目前只放在文章中示範,尚未寫入 API Demo 的 AGENTS.md。未來抽成可重複觸發、能帶著 Agent 執行檢查流程的 Skill 會更適合;系列最後也會把前面累積的 Clean Code Policy 集中整理成公開 Skill。

CLEAN 原則

C — Context-Aware Code 情境感知:Actor 必須由需求與 Repository 提供

C — Context-Aware Code 情境感知 要求 User 補上程式碼看不到的資訊。三個條件只能顯示資料與判斷,無法告訴 Agent 應該建立一個 Policy、兩個 Policy,還是三個 Rule;Actor、部署方式與已知變化必須來自需求或 Repository。常見 Pattern 可以列入候選,不能取代這些專案證據。

L — Localized Change 局部變更:追蹤變更理由能否停在自己的 Policy

L — Localized Change 局部變更 在這裡追蹤某位 Actor 修改政策時,實際會牽動哪些位置。Conditional Extension 的 Diff 最小,但兩個 Actor 下次仍會進入同一個方法。Actor-separated 增加檔案與測試,換來各自的政策位置。Interface-based 保住 Processor,新增規則時仍要修改 Composition Root。檔案數只描述修改面積,責任是否局部還要看誰會因什麼理由修改它。

今天先拆開兩個政策負責人;共同 Interface 等到第三種擴充壓力出現

回到開頭,第三條規則改變昨天答案的原因,不是數量,而是第二個 Actor 已經出現。這次我留下兩個具體 Policy,讓服務等級與派工規則各自擁有名稱、測試與修改位置。

這一步已經符合目前的 SRP 壓力,也保護了同一 Actor 繼續調整規則時的修改範圍。共同 Interface 會等到新 Actor 持續加入、需要執行期組合,或同一套規則出現不同生命週期時再建立;Plugin 則需要更進一步的獨立建置、載入、版本隔離或部署證據。

明天換上第二個通知 Provider,繼續用 LSP、ISP 與 DIP 檢查今天建立的抽象能不能守住契約。

參考資料


上一篇
Day 14|測試全綠後,怎麼判斷設計已經足夠簡單?
下一篇
Day 16|兩個通知 Provider 都能編譯,為什麼還不能證明可以互相替換?
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言