安安~我是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 出現後,原本的具名方法還夠不夠用?昨天沒有採用的抽象,今天又該增加到哪裡才停?
五項原則看的不是同一件事。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,就宣稱已經完成依賴反轉。 |
SRP 先問:這個模組改變時,誰會提出要求?因為相同理由而改變的程式碼適合放在一起;變更理由不同時,設計要避免其中一方的修改牽動另一方。
Actor 數量不會直接換算成 Class 數量。隔離可以發生在方法、類別或元件;一個 Actor 也可能需要多個 Class 協作。今天真正要驗收的,是兩位負責人下一次改政策時,能不能各自找到清楚、可獨立測試的修改位置。
Strategic Closure 可理解成「策略性封閉」:只針對已知、反覆出現或風險較高的變化建立穩定邊界。新增 if 或修改既有檔案不會自動違反 OCP;當同類需求每次都要同步修改穩定流程,才形成值得處理的抽象壓力。
任何系統都無法對所有未來同時封閉。今天要保護的是通知升級政策持續增加時,逾期查詢、通知、失敗統計與 Save 順序不必跟著重寫;DI Registration、Policy 本身與 Tests 仍然可以是明確修改點。
LSP 檢查替代實作能否維持呼叫端依賴的結果、失敗方式與副作用。Notification Gateway 即使實作相同 Interface,若把 false 改成 Exception、忽略 Cancellation,或自行改變重試次數,呼叫端仍不能安心替換。這會是明天第二個 Provider 的主要問題。
ISP 要求使用端只依賴自己會用到的能力。OverdueWorkItemProcessor 若只需要送通知,就不該同時依賴範本管理與通知歷史;反過來,同一個 Use Case 本來就需要兩個緊密相關的方法時,硬拆成兩個 Interface 也不會更乾淨。
DIP 檢查原始碼依賴方向。高階流程先定義自己需要的通知能力,低階 Adapter 再去實作;DI Container 只負責建立與組合物件。Processor 若仍直接引用供應商 SDK,就算改成 Constructor Injection,依賴方向也沒有反轉。
今天先用第二個 Actor 驗證 SRP 與 OCP。明天換上第二個通知 Provider,再實際檢查 LSP 的替換行為、ISP 的 Consumer 契約與 DIP 的依賴方向。
只寫「請遵守 SRP 與 OCP」,沒有交代 Actor、預期變化與停止條件,Agent 仍得自己猜責任邊界。它可能建立一整套擴充架構,也可能把新條件留在原方法;光看 SOLID 名稱,兩種結果都無法驗收。
我不會直接接受 Agent 說自己「符合 SOLID」。這次改看兩項可查結果:服務等級與派工政策能否各自修改、各自審查;同類政策繼續增加時,哪些穩定流程可以維持不動。
本次受控情境如下:
| Actor/變更來源 | 負責的政策 | 會讀取的資料 |
|---|---|---|
| 服務等級政策負責人 | High 逾期 24 小時升級;所有 Priority 逾期 72 小時升級 | Priority、DueAtUtc、目前時間 |
| 派工管理負責人 | 未指派工作項目一到期就升級 | Assignee、DueAtUtc、目前時間 |
程式碼裡的 if 與欄位只能顯示資料形狀,無法推回組織內的決策權責。SRP 所需的 Actor 證據,必須來自需求、Repository 文件或團隊分工。
OCP 這次保護的變化方向是「持續加入 Escalation Policy」;希望保持穩定的部分包括逾期資料查詢、通知路由、失敗統計與 Save 順序。它不追求整個系統零修改:若最後採用 Rule Set,新增 Policy 時仍可能要修改 DI Registration、Tests 或設定。

圖:SRP 先辨認兩個政策由誰、因何改變;欄位與 if 本身無法提供 Actor 證據。
今天用五個問題比較三份設計:
九次實驗都從昨天接受的具名規則開始:
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 維護。
本次固定以下行為:
Assignee 是 null、空字串或純空白,都算未指派。ProcessedCount 不重複增加。false 時,失敗摘要與最終 Overdue 狀態維持。若想重現實驗,可以從公開的 AI-CleanCode-API-Demo 切到相同起點:
fb22f445f383c7977dfac71f35cc028b0ca53e00
day-15-solid-srp-ocp-baseline
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 建立具體 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 的差異如下。
直接擴充既有 RequiresEscalationNotification。
不新增 Production Class、Interface、Policy、Strategy、Factory、Resolver、
Rule Collection、Reflection、Plugin、外部設定或新的部署單位。
在同一個方法內,讓服務等級與派工管理的條件仍可辨識。
這一組保留最少結構。若兩個 Actor 的條件放進同一個方法後仍能清楚辨認,也許還不必拆 Class;若下一次修改無法避開另一位 Actor 的規則,SRP 壓力就會出現。
依兩個 Actor/變更理由建立兩個小型、具名且可獨立測試的 Production Policy。
OverdueWorkItemProcessor 只組合兩份 Policy 的結果。
使用具體型別依賴,不建立共同 Rule Interface、IEnumerable 規則集合、
Factory、Resolver、Reflection 掃描、Plugin 載入或外部設定。
這一組把兩個 Actor 明確交給 Agent,並禁止建立共同 Interface。我想確認的是:不同變更來源已經出現時,兩個具名 Policy 是否就能把責任拆清楚,不必立刻升級成通用規則平台。
建立 Escalation Rule Interface,由個別規則實作判斷。
用 ASP.NET Core DI 註冊規則,並以集合或 Policy 組合結果。
Processor 不保存個別門檻、Priority 或未指派判斷細節。
新增規則以新增實作與明確 Registration 為主要路徑。
不使用 Reflection/Assembly 掃描、動態 Plugin 載入、外部設定或新的部署單位。
這一組把規則移出 Processor,觀察共同 Interface 與 DI 集合能不能讓後續 Policy 以新增實作的方式加入。所有 Rule 仍和 API 一起編譯、一起部署;動態 Plugin 需要處理的載入、版本與安全問題不在本次範圍內。
Conditional Extension、Actor-separated Policies 與 Interface-based Rule Set 都是本系列定義的實驗方向,不是書中的正式分類。正文把共同條件抽出來方便比較;三份逐字原始 Prompt 保留在 Day 15 公開實驗資料。
同一筆資料若同時命中服務等級與派工政策,測試通過時就不知道是哪條規則生效。因此我從公開 HTTP 入口 POST /api/work-items/process-overdue 加入三項 Host Oracle,分別檢查新政策、重跑與通知失敗:
ProcessedCount 仍為 0。false 時,要留下失敗摘要並保存 Overdue。這三項測試放回 Baseline 後全部轉紅,而且都停在未指派項目沒有送出 Escalation;這才確認 Red 來自尚未實作的新需求。
這裡我也踩到另一個容易被綠燈掩蓋的問題。原本測 24/72 小時邊界的資料,Assignee 預設也是 null。新規則加入後,如果不校正測試資料,High 24 小時案例即使把時間條件寫壞,也可能因為「未指派」而照樣走 Escalation。
測試是綠的,原本的服務等級政策卻沒有被驗到。
所以九份候選都先把既有服務等級案例設為已指派,讓它們只驗收 24/72 小時門檻;未指派政策使用另一組資料。這樣才能辨認到底是哪一項政策讓測試通過。
WorkItem.AssignTo() 會先執行 Trim(),純空白最後會變成空字串。部分候選測試因此使用 Test seam,也就是只為測試開放的受控入口,直接保存原始空白 Assignee。這能確認 Processor 從資料庫讀回純空白後仍會判定為未指派,而且不會改變正式 API 的輸入流程。
三種方向各跑三次,九份候選都通過相同的三項 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 與規則落點。
它把新條件直接加進具名方法:
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,兩邊下一次改政策時仍會進入同一個方法,兩類修改責任依然混在一起。
這組把既有兩條服務等級規則放進 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 仍然會修改。它不是終極架構,只是目前壓力下剛好足夠的一步。
這組建立共同介面,再由 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 註冊。

圖:規則少且同一 Actor 時可以直接擴充;Actor 分離後建立 Policy;同類規則持續增加時才考慮共同介面。
每個方向都有三個獨立工作階段。下表是三次平均:
| 設計方向 | 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」,也不能取代責任與修改路徑的判斷。
本系列接下來沿用:
ccbc136daa59cfcd430d44ebd4cb62b57b4ab72d
day-15-solid-srp-ocp
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。
如果 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 和方法名稱。ServiceLevelEscalationPolicy 與 AssignmentManagementEscalationPolicy 則提供清楚的搜尋入口,下一次修改可以先進入對應政策。這類命名通常能減少重新理解規則的工作,但本次實驗不能把它換算成固定的 Token 節省比例。
老樣子~這些判斷會跨任務重複使用,我先把它們整理成一份可以放進 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。
C — Context-Aware Code 情境感知 要求 User 補上程式碼看不到的資訊。三個條件只能顯示資料與判斷,無法告訴 Agent 應該建立一個 Policy、兩個 Policy,還是三個 Rule;Actor、部署方式與已知變化必須來自需求或 Repository。常見 Pattern 可以列入候選,不能取代這些專案證據。
L — Localized Change 局部變更 在這裡追蹤某位 Actor 修改政策時,實際會牽動哪些位置。Conditional Extension 的 Diff 最小,但兩個 Actor 下次仍會進入同一個方法。Actor-separated 增加檔案與測試,換來各自的政策位置。Interface-based 保住 Processor,新增規則時仍要修改 Composition Root。檔案數只描述修改面積,責任是否局部還要看誰會因什麼理由修改它。
回到開頭,第三條規則改變昨天答案的原因,不是數量,而是第二個 Actor 已經出現。這次我留下兩個具體 Policy,讓服務等級與派工規則各自擁有名稱、測試與修改位置。
這一步已經符合目前的 SRP 壓力,也保護了同一 Actor 繼續調整規則時的修改範圍。共同 Interface 會等到新 Actor 持續加入、需要執行期組合,或同一套規則出現不同生命週期時再建立;Plugin 則需要更進一步的獨立建置、載入、版本隔離或部署證據。
明天換上第二個通知 Provider,繼續用 LSP、ISP 與 DIP 檢查今天建立的抽象能不能守住契約。