iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

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

Day 16|兩個通知 Provider 都能編譯,為什麼還不能證明可以互相替換?

  • 分享至 

  • xImage
  •  

安安~我是ChiYu~

昨天把服務等級與派工政策分開後,今天我在共同通知介面後方,再放入一個 Queued Provider。Provider 就是負責實際送出通知的替代實作。

Demo 與 Queued 都能實作同一個 Interface,也都能由 DI 正常切換。可是只要其中一個把已知失敗的 false 改成 Exception、吞掉 Cancellation,或在內部自行重試,OverdueWorkItemProcessor 收到的行為就已經不同。

九份候選最後都能編譯,也能由 DI 解析。真正拉開差異的,不是 Interface 有幾個,而是三件事:兩個 Provider 是否接受同一份行為契約、正式 Consumer 是否被迫依賴用不到的能力,以及通知契約究竟由高階流程還是低階 Adapter 擁有。

實驗中,強制拆成兩個小介面的版本讓 Processor 多了一項依賴,卻沒有隔離任何新 Consumer;Contract-First 的三次執行則都先建立共用契約,也都判斷目前不需要方法級拆分。本系列最後採用其中一份 Consumer Port 版本,是為了繼續觀察 DIP 的契約所有權;若需求只是新增第二個 Provider,我反而會選擇更小的修改。

一句「請幫我套用 SOLID」,很快就能得到 Interface、Constructor Injection 與 DI Registration。型別接得起來只是起點,今天要繼續檢查 LSP、ISP 與 DIP 真正要求的行為、使用端與依賴方向。

LSP、ISP、DIP 分別檢查行為契約、使用端依賴與原始碼方向

只有一個實作時,這三項原則仍然存在,只是許多錯誤暫時不會發作。第二個 Provider 加入後,替換前後的差異才有具體對照。

原則 本篇實際驗收的問題 容易出現的誤判
LSP/Liskov Substitution Principle 里氏替換原則 Demo 換成 Queued 後,成功、失敗、例外與取消語意是否一致? 兩個 Class 實作同一個 Interface,就直接宣布可以替換。
ISP/Interface Segregation Principle 介面隔離原則 Consumer 是否被迫依賴自己不會使用的能力? 一個方法拆一個 Interface,介面越小就越符合 ISP。
DIP/Dependency Inversion Principle 依賴反轉原則 通知契約是否由高階 Use Case 擁有,低階 Adapter 是否反過來實作它? 使用 Constructor Injection 或 DI Container,就直接宣布依賴方向正確。

共同行為測試回答 LSP,Production Consumer 的使用方式回答 ISP,原始碼參考方向則回答 DIP。三者處理的是不同問題,不能用「都有 Interface」一次帶過。

LSP 檢查兩個 Provider 能否維持相同的成功、失敗與取消語意

OverdueWorkItemProcessor 至少依賴三項通知行為:

  1. 一般標題回傳 true
  2. 標題含 [notify-fail] 時回傳 false,Processor 累加失敗摘要,但仍保存 Overdue 狀態。
  3. 已取消的 CancellationToken 必須傳播取消,不能改成 false 或成功。

Queued 若把已知失敗改成 Exception,它可能符合自己的內部設計,卻違反 Processor 已經依賴的契約。LSP 不要求兩個 Provider 寫法相同,而是要求替換後,呼叫端依賴的成功、失敗、取消與副作用語意不變。

Design by Contract 通常翻成契約式設計。前置條件描述呼叫端必須提供什麼;後置條件與不變量則說明完成後必須守住哪些結果與狀態。Subtype 不能突然要求更多輸入,也不能少做 Client 已經依賴的保證。

ISP 要從實際 Consumer 判斷介面是否需要拆分

本篇的正式 Consumer 是 OverdueWorkItemProcessor,同一段流程會依升級政策選擇一般通知或升級通知:

var notificationSucceeded = requiresEscalationNotification
    ? await notificationSender.SendEscalationAsync(workItem, cancellationToken)
    : await notificationSender.SendOverdueAsync(workItem, cancellationToken);

目前只有這一個 Consumer,而且它確實同時使用兩種通知。兩個方法也共用 Provider、生命週期、Cancellation 與失敗語意,所以留在同一個 Interface 並沒有增加使用端負擔。

等另一個 Consumer 只需要其中一項能力,或兩種通知開始使用不同權限、Provider、生命週期與部署方式,再拆才有隔離效果。ISP 看的是誰被迫依賴什麼,不是方法數量是否夠少。

DIP 檢查高階流程是否擁有自己需要的通知契約

執行時,Processor 會向外呼叫通知 Adapter;編譯時的原始碼依賴,則應指向高階流程需要的抽象。Processor 只需要知道「傳送 Work Item 通知」的能力,不應知道 Queue SDK、Provider Key、序列化格式或重試設定。

Runtime 呼叫方向:
OverdueWorkItemProcessor → Demo/Queued Adapter

原始碼依賴方向:
OverdueWorkItemProcessor → IWorkItemNotificationSender
Demo/Queued Adapter    → IWorkItemNotificationSender

Program.cs 可以知道具體型別,因為它是 Composition Root,專門負責選擇並組合物件。DI Container 只完成物件建立與接線;如果高階 Processor 仍直接引用某家通知 SDK,改成 Constructor Injection 也沒有改變依賴方向。

這次所有型別仍編譯進同一個 .NET Assembly。Namespace 能表達邏輯所有權,Compiler 卻不會因此禁止反向引用。明天進入元件原則後,再用 Project Reference 檢查這條方向能不能成為真正的編譯限制。

Consumer、Contract 與兩個 Provider 之間的 LSP、ISP、DIP 契約檢查

圖:可以編譯只是入場券;Provider 替換還要檢查可觀察行為、Consumer 需要的介面大小與原始碼依賴方向。

先用本機 Queued Provider 排除網路、SDK 與重試行為的干擾

這次的 QueuedNotificationGateway 不會連線到真實 Queue,也不加入 SDK、背景服務、Outbox 或自動重試。真實服務會同時帶入網路、Credential、序列化、送達保證與 Idempotency,我就無法判斷候選差異究竟來自 SOLID 邊界,還是外部服務剛好失敗。

這個可預測的本機模擬 Provider 只製造一種替換壓力:

  • 未設定 Notification:Provider 時使用 Demo。
  • 設成 DemoQueued 時,DI 解析成不同具體 Provider。
  • 兩個 Provider 都支援一般通知與升級通知。
  • 兩個 Provider 都遵守成功、[notify-fail] 與 Cancellation 契約。
  • 替換 Provider 不能改動 HTTP Contract、Database Schema、Save 順序、失敗摘要與重跑行為。

這個本機模擬只回答今天的問題:Demo 與 Queued 放在同一個 Interface 後方時,已列出的行為與原始碼依賴是否相容。真實 Queue 的送達、重試與跨程序失敗不在本次結論內。

實驗固定從昨天接受的 SRP/OCP 版本開始

本次基準固定為:

  • Baseline Commit:ccbc136daa59cfcd430d44ebd4cb62b57b4ab72d
  • Annotated Tag:day-16-solid-lsp-isp-dip-baseline
  • Model:Codex GPT-5.6-SOL-HIGH
  • Reasoning effort: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-16-solid-lsp-isp-dip-baseline

不需要 Clone 專案才能閱讀本文。完整 Prompt、Diff 與實驗紀錄都保留在公開 Repository;Tag 主要提供想自行重播的讀者使用。

起點原本只有一個 INotificationGateway,同時提供一般與升級通知:

public interface INotificationGateway
{
    Task<bool> SendOverdueAsync(
        WorkItem workItem,
        CancellationToken cancellationToken);

    Task<bool> SendEscalationAsync(
        WorkItem workItem,
        CancellationToken cancellationToken);
}

這次不預先要求 Agent 保留或拆分介面。我會先看 Consumer 實際用了什麼、兩個 Provider 要守住哪些行為,再檢查契約由哪一層擁有。

Contract Test 讓每個 Provider 接受相同的行為檢查

如果 Demo 與 Queued 各寫一份 Unit Test,兩份測試很容易逐漸分岔。Demo 驗了取消,Queued 可能忘記;第三個 Provider 又可能只驗成功。

Contract Test 是把所有替代實作共同遵守的 Assertions 集中起來,再讓每個 Provider 套用同一份測試。我把 Provider、通知操作與結果交叉列成 Contract Matrix。兩個 Provider、兩種通知操作,再乘上成功、失敗與取消三條路徑,共有 2 × 2 × 3 = 12 個案例。

通知種類 一般標題 [notify-fail] 已取消 Token
一般逾期通知 回傳 true 回傳 false 傳播 OperationCanceledException
升級通知 回傳 true 回傳 false 傳播 OperationCanceledException

接受版本使用一個抽象測試類別,兩個 Provider 各自提供建立實作的方式:

public abstract class NotificationGatewayContractTests
{
    protected abstract IWorkItemNotificationSender CreateSender();

    [Fact]
    public async Task SendOverdueAsync_RegularTitle_ReturnsSuccess()
    {
        var sender = CreateSender();

        var succeeded = await sender.SendOverdueAsync(
            CreateWorkItem("一般逾期工作"),
            TestContext.Current.CancellationToken);

        Assert.True(succeeded);
    }

    [Fact]
    public async Task SendEscalationAsync_TitleContainsFailureMarker_ReturnsFailure()
    {
        var sender = CreateSender();

        var succeeded = await sender.SendEscalationAsync(
            CreateWorkItem("需要升級通知 [notify-fail]"),
            TestContext.Current.CancellationToken);

        Assert.False(succeeded);
    }
}

這十二個案例支持的範圍很明確:兩個 Provider 在目前列出的成功、失敗與取消情境下,對 Client 提供相同語意。未知 Exception、Retry、Idempotency 與真實 Queue 的送達保證還沒有被產品契約定義。未來加入 Timeout 或部分成功時,Contract Matrix 也要跟著擴充。

用 CLEAN 的 E 寫清楚 Provider 契約,再用 N 守住替換前後的行為

E — Explicit Intent and Boundaries 意圖明確:先交代契約、Consumer 與停止條件

「新增 Queue Provider,請遵守 SOLID」沒有說明失敗語意,也沒有交代一般與升級通知是否屬於不同 Consumer。Agent 只能依常見 Pattern 自行補完。

E — Explicit Intent and Boundaries 意圖明確 要我先寫出成功、失敗、取消、禁止新增的能力與 Composition Root 邊界。Interface 名稱與檔案位置可以交給 Agent;成功、失敗、取消、副作用順序與不得擅自增加的 Retry、Outbox 等行為,必須由 User 先說清楚。

N — Non-Surprising Behavior 符合預期:替換 Provider 不能偷偷改變系統答案

Provider 換掉後,既有 API Tests 全綠仍可能漏掉失敗語意、Cancellation 或副作用順序漂移。

N — Non-Surprising Behavior 符合預期 要求每份候選接受同一份 Client Contract,主流程再用獨立 Oracle 檢查 Demo/Queued 的成功、失敗、取消、Provider 型別與 Processor 依賴。Adapter 內部可以不同,對外可觀察行為必須穩定。

三種 Prompt 比較 User 提供不同設計約束時,Agent 會怎麼拆通知邊界

我讓三種方向各跑三個全新 Codex Session,共九次正式實驗。三組都使用相同 Baseline、模型、功能需求與驗收入口,也不沿用前次對話。差異集中在 User 事前固定多少設計條件;這些 Prompt 同時改變允許的抽象、禁止事項與停止條件,因此屬於三種策略的重複觀察,不是單一詞句造成結果的嚴格因果實驗。

三組 Prompt 共用下列條件:

- 新增可預測的 Queued Provider,未設定時仍使用 Demo。
- 兩個 Provider 都要支援一般通知、升級通知、已知失敗與取消傳播。
- Provider 替換不得修改 OverdueWorkItemProcessor 的業務流程。
- 保留 HTTP Contract、Database Schema、通知與 Save 順序及重跑行為。
- 不新增真實網路、套件、Retry、Outbox、背景服務或產品功能。
- 完成後執行完整 Tests、dotnet format 與 git diff --check。

三組差異如下:

Prompt 約束 User 先固定什麼 Agent 還能決定什麼 想回答的問題
不指定新架構 需求、既有行為與修改範圍 是否拆介面、是否建立 Contract Test、契約放在哪裡 Repository 現況會把 Agent 帶往哪一種最小修改?
先指定兩個小介面,後文簡稱 Interface-First 一般通知與升級通知要拆成不同介面 名稱、檔案、DI 與 Tests 跳過 Consumer 分析後,方法級拆分會增加多少成本?
先固定契約與 Consumer,後文簡稱 Contract-First 先建立共用 Contract Test,再檢查正式使用端 保留 Gateway、建立由高階 Use Case 擁有的 Consumer Port,或有證據時拆介面 規定判斷流程,能否讓 Agent 保留設計自主性?

完整原始 Prompt 都保留在公開專案:

不指定新架構,觀察 Agent 只根據 Repository 現況如何替換 Provider

第一組不使用 LSP、ISP、DIP 或 Contract-First 等設計關鍵字。這是控制組,用來觀察既有介面、測試與 Repository 結構本身會把 Agent 帶往哪裡。

Agent 可以沿用 INotificationGateway、拆出新介面、建立共用 Contract Test,或替兩個 Provider 各寫測試。後面兩組加入結構與決策約束後,才有基準可以比較那些指令改善了什麼,又增加了什麼。

先指定兩個小介面,測量方法級拆分是否隔離任何已知 Consumer

第二組直接要求一般通知與升級通知拆成兩個介面,OverdueWorkItemProcessor 也要改成依賴拆分後的抽象。

Agent 不再判斷目前是否需要拆分,只能處理介面名稱、檔案位置、DI 註冊與 Tests。這組刻意模擬 User 把「介面越小越好」當成 ISP 實作指令,接著量測建構式參數、DI 映射與組裝成本有沒有換到實際隔離效果。

先固定契約與 Consumer,讓 Agent 自己決定介面是否需要拆

第三組要求 Demo 與 Queued 先跑同一套 Contract Test,再依 OverdueWorkItemProcessor 實際使用的能力決定邊界。

User 固定的是判斷順序與禁止事項。Agent 仍能保留既有 Gateway、建立 Consumer Port,或在發現不同 Consumer 後拆介面;每個選擇都要對得回可執行契約與正式使用端。

三種 Prompt 各跑三次後,差異出現在介面負擔與契約位置

九份候選都由主流程重新完成 Build、既有行為測試、格式、Diff 與 HTTP Smoke Test,另外接受相同的 Host Oracle。確認 Demo/Queued 的成功、失敗、取消與 Processor 抽象依賴都成立後,我才比較三組的介面負擔與契約位置:

Prompt 約束 LSP 實據 ISP 判斷 DIP 方向 User 看到的成本
不指定新架構 三次都補了 Provider Tests,契約組織方式不同 保留既有 INotificationGateway Processor 維持原依賴 修改最少,共用契約的可重複性由各次 Agent 自行判斷
先指定兩個小介面 兩個 Provider 都能履行一般與升級通知 Processor 被改成同時依賴兩個小介面 仍由 Composition Root 選擇完整 Provider 多出建構式參數與 DI 映射,沒有隔離新的 Consumer
先固定契約與 Consumer 三次都有 Demo/Queued 共用 Contract Test 三次都判斷目前不需要方法級拆分 兩次建立 Consumer Port,一次保留既有 Gateway 行為與判斷流程較一致,類別配置仍有差異

檔案數與新增/刪除行數等原始統計保留在完整實驗紀錄。正文聚焦在這些修改換到了什麼。

沒有指定新架構時,三次都沿用現有 Gateway

自由替換的三次結果都保留 INotificationGateway,也沒有修改 OverdueWorkItemProcessor。Agent 看見既有介面已能同時表達一般與升級通知,因此只加入 Queued Adapter、Provider Tests 與 Composition Root 選擇。

當既有介面穩定、只有一個 Consumer,而且行為契約已經被測試清楚時,沿用原介面是成本最低的選擇。未來若 Consumer 或失敗語意開始分化,這份最小修改才需要重新評估。

強制拆成兩個介面後,Processor 多了依賴,卻沒有隔離新的 Consumer

Interface-First 三次都產生類似結構:

public interface IOverdueNotificationGateway
{
    Task<bool> SendOverdueAsync(
        WorkItem workItem,
        CancellationToken cancellationToken);
}

public interface IEscalationNotificationGateway
{
    Task<bool> SendEscalationAsync(
        WorkItem workItem,
        CancellationToken cancellationToken);
}

public interface INotificationGateway
    : IOverdueNotificationGateway, IEscalationNotificationGateway;

Composition Root 把同一個 Provider 映射成兩個小介面,Processor 的通知依賴也從一項變成兩項。這份程式碼能編譯、測試也通過,但目前唯一的 Production Consumer 同時需要兩種通知。介面拆分增加了建構式參數與 DI 映射,沒有隔離任何已知差異。

等到一般通知與升級通知真的出現不同 Consumer、權限、生命週期或 Provider,再採用這個方向。

先固定契約後,三次都得到相同的 Consumer 判斷,類別配置仍不相同

Contract-First 三次都先建立 Demo/Queued 共用測試,再檢查 Processor 的實際使用方式。三次都判斷一般與升級通知由同一個 Consumer 使用,目前不需要拆成兩個方法級 Interface。

實作位置仍然出現差異:

  • Run 01 建立 IOverdueNotificationPort,放在 Ports 目錄。
  • Run 02 保留既有 INotificationGateway,只補強共用 Contract Test。
  • Run 03 建立 IWorkItemNotificationSender,與高階 Use Case 放在 WorkItems.Api;兩個 Adapter 留在 WorkItems.Api.Notifications

我看重 Contract-First 的地方,不是三次檔案配置完全一致,而是它們都先建立共用契約,也都先做出相同的 Consumer 判斷。User 最後仍要比較契約所有權與新增結構是否值得。

我依需求選擇 Provider 替換、共用契約、Consumer Port 或介面拆分

這次我不把 Run 03 寫成固定模板。需求只增加 Provider、需要共用契約、準備建立獨立元件,或真的出現不同 Consumer 時,我會採用不同設計:

目前情境 我會優先考慮的設計 理由
只新增第二個 Provider,既有介面穩定 自由替換或 Contract-First Run 02 修改較小,Processor 不需要搬動;補上共用契約即可。
多個 Provider 必須共享成功、失敗與取消語意 Contract Test 新增 Provider 時會自動接受相同 Assertions。
高階 Use Case 即將形成獨立元件 Contract-First Run 03 的 Consumer Port 契約由高階流程擁有,低階 Adapter 反過來實作。
不同 Consumer、權限、生命週期或部署方式已經出現 方法級或角色級 Interface Split 拆分能隔離已知差異,不只增加介面數量。

本系列接下來沿用 Run 03:

  • Commit:5f596a62a9db82a0acaade6509579b449ebfcf61
  • 正式候選 Tag:day-16-formal-contract-first-consumer-port-run-03
  • 系列接續 Tag:day-16-solid-lsp-isp-dip
git switch --detach day-16-solid-lsp-isp-dip

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

今天接受 Run 03,是因為下一篇要繼續觀察 DIP 的契約所有權。它把 IWorkItemNotificationSender 放在高階 WorkItems.Api,Demo/Queued Adapter 反過來實作,具體選擇留在 Program.cs。所有型別目前仍在同一個 Assembly,所以這只是原始碼層級的所有權線索。

如果專案已有可靠共同契約,單純新增 Provider 時,自由替換組的修改最少;若只需要補 Contract Test,不需要移動 Port,Run 02 會比 Run 03 精簡。等元件真的需要獨立建置或發布,再支付拆 Project 的成本。

Token 沒有固定贏家,Contract-First 讓 Agent 先驗證契約再決定介面

三種方向各有三個 Session。Fresh Input Token 是扣除快取後重新讀入的內容量,Output Token 是模型輸出量,Tool Calls 則是 Agent 呼叫工具或終端指令的次數。

Prompt 約束 Fresh Input Token Output Token Tool Calls 平均時間
不指定新架構 140,311 25,301 52.3 830.5 秒
先指定兩個小介面 135,160 22,698 61.3 797.9 秒
先固定契約與 Consumer 137,434 22,696 50.7 748.3 秒

三次執行看不出任何策略具有固定 Token 優勢。Interface-First 的 Fresh Input 最低,Tool Calls 卻最高;Contract-First 的平均時間最短,Output Token 則與 Interface-First 接近。

我更在意工作順序。Contract-First 先固定 Client 行為,再決定 Interface 與 Adapter,審查時比較容易追問每個抽象為什麼存在。這是本次 Repository 的觀察,不直接外推到其他專案。

測試把 null 當成未設定,會讓 Agent 在錯誤 Oracle 上得到綠燈

其中一個 Session 測試「未設定 Provider 時使用 Demo」時,寫下:

settings["Notification:Provider"] = null;

把設定值寫成 null,代表 Configuration 裡仍然存在 Notification:Provider 這個 Key;完全沒有加入 Key,才符合這次的「未設定」。主流程修正測試後,答案才對回需求。

這次誤判也落在 N — Non-Surprising Behavior 符合預期:Oracle 本身若寫錯,Agent 只會很認真地取得一個沒有意義的綠燈。

把 Provider 的共同契約與設計升級條件寫進 Repository Policy

大量 AI Coding 時,我不想逐次指定要建立哪個 Interface、放在哪個資料夾。因此,我先把共同契約與選擇條件整理成一段可放進 AGENTS.md 的範例:

## Provider Contract Policy

- 替換 Provider 前,先列出 Production Consumer 依賴的成功、已知失敗、
  Exception、Cancellation、Retry、Idempotency 與副作用順序;
  Repository 沒有定義的項目,不得由 Agent 自行補答案。
- 兩個以上 Provider 共用同一抽象時,建立可重複套用的 Contract Test;
  每個 Provider 必須執行相同 Assertions。全綠只代表已列案例成立。
- Repository 已有穩定抽象,而且高階 Consumer 沒有引用 Vendor 細節時,
  保留既有 Interface,優先補齊 Contract Test。
- 高階 Use Case 需要擁有契約,或即將形成獨立元件時,才建立 Consumer Port;
  具體 Provider、設定綁定與選擇留在 Adapter/Composition Root。
- 套用 ISP 前,列出每個 Interface Member 的 Production Consumer;
  沒有不同 Consumer、權限、生命週期或部署影響時,不因方法數量拆分介面。
- Provider 替換不得修改 HTTP Contract、Database Schema、錯誤語意、
  Cancellation、通知與 Save 順序及重跑行為。
- 未授權時,不新增 Retry、Outbox、背景服務、狀態查詢、撤回、
  管理 API、真實網路呼叫或新套件。
- 完成後回報 Consumer、共用契約、失敗矩陣、Interface Members、
  原始碼依賴方向、Composition Root、Tests、Diff 與停止理由。

這份 Policy 目前尚未寫入 API Demo 的 AGENTS.md,先留在文章中方便示範。規則成熟後,抽成能重複引導 Agent 執行檢查流程的 Skill 會更適合;系列最後也會把累積的 Clean Code Policy 整理成公開 Skill。

讓 AI Agent 在修改前回答 LSP、ISP 與 DIP 的三個問題

昨天已經用 SRP 與 OCP 檢查 Actor、變更理由與預期擴充。今天再補上後三問:

## SOLID Decision Check

- L/LSP:Client 依賴哪些成功、失敗、Exception、Cancellation 與副作用語意?
  所有替代實作要如何接受相同驗收?
- I/ISP:每一個 Interface Member 分別由哪些 Production Consumer 使用?
  是否有 Consumer 被迫依賴自己用不到的能力?
- D/DIP:高階政策需要什麼契約?誰擁有這份契約?
  Adapter、SDK、Options 與 Composition Root 的原始碼依賴各自指向哪裡?

找不到契約、Consumer 或依賴方向的 Repository 證據時,先回報缺口;
現有抽象已足以處理目前壓力時,停止新增介面並說明理由。

這份 Check 要 Agent 先列出行為契約、Consumer 與依賴方向,再由測試、Diff 和 Repository 情境決定哪些抽象值得留下。類別與檔案配置仍保留給 Agent 判斷。

編譯相容只是起點,替換還要通過行為、Consumer 與依賴方向檢查

回到標題,兩個 Provider 都能編譯,只能證明型別相容。真正的替換還要通過三道檢查:對 Client 提供相同的成功、失敗與取消語意;Interface 沒有把 Consumer 用不到的能力一起塞進來;高階流程依賴自己需要的契約,而不是直接依賴 Vendor 細節。

本篇為了觀察 DIP 而接受 Run 03,但這不是所有 Provider 替換都要建立 Consumer Port。只有新增 Provider 時,我會選更小的修改;只有不同 Consumer、權限、生命週期或部署方式真的出現時,才拆介面。

目前這條依賴方向仍靠 Namespace 與團隊規則維持,Compiler 還不會攔住反向引用。明天進入元件原則,我會把同一份程式碼拆到 Project 層級,再檢查哪些內容真的需要一起改、一起重用與一起發布。

參考資料


上一篇
Day 15|第三條規則由不同負責人維護,SRP/OCP 要求 AI 把責任拆到哪裡?
下一篇
Day 17|AI 把 API 拆成多個 Project,怎麼判斷哪些元件邊界值得留下?
系列文
AI 時代的 Clean Code:30 天讓 AI 產出的程式碼可讀、可驗證、可維護23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言