iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

前言:型別提示介面,換實作應該很安全吧?

「建構子依賴的是一個介面,不是具體類別,換掉底層實作應該不會影響到呼叫端吧?」

昨天談到,依賴注入容器本身是一把雙面刃:它讓「依賴介面而不是具體實作」這件事變得容易做到,卻也在容器設定裡藏了一層 AI 讀程式碼時看不到的邏輯。今天用一個具體案例,把這個雙面性落地——一次「換底層實作」的改動,明明契約沒變、測試也過,卻悄悄繞過了一段本來該生效的邏輯。

今日目標

  • 看一個「型別提示介面、換底層實作」卻意外繞過裝飾器邏輯的具體案例
  • 理解為什麼「介面契約沒變」不等於「行為沒變」
  • 認識容器設定本身也是一種需要被查證的「隱藏依賴」
  • 建立「換實作前先看容器怎麼組裝這個依賴」的具體習慣

案例:一個被裝飾器包住的介面

假設系統裡有一個 NotificationSender 介面,負責發送通知。容器設定裡實際綁定的方式是這樣:

❌ AI 只看到呼叫端的型別提示:
class OrderService
{
    public function __construct(
        private NotificationSender $sender
    ) {}
}
// AI 判斷:型別提示是介面,換底層實作應該很安全

但容器實際組裝 NotificationSender 的方式,並沒有直接綁定到某個發送通知的具體類別,而是綁定到一個裝飾器

✅ 容器設定裡實際發生的事:
$container->bind(NotificationSender::class, function ($c) {
    $rawSender = new SmtpNotificationSender($c->get('mailer'));
    return new RateLimitedNotificationSender($rawSender, $c->get('rateLimiter'));
});
// 呼叫端拿到的不是 SmtpNotificationSender 本身,
// 而是包了一層「限流邏輯」的裝飾器

RateLimitedNotificationSender 這個裝飾器的職責,是在真正發送通知之前先檢查頻率限制——避免同一個使用者在短時間內被灌爆一堆通知信。這段邏輯不在介面契約裡,介面只定義了「發送通知」這個方法簽章,完全沒有規定「發送前要不要先做限流檢查」。

換掉底層實作,繞過了裝飾器

有一次的改動需求是:把發送通知的方式從 SMTP 換成一個第三方通知服務。AI 接到這個任務,查看程式碼後判斷:OrderService 依賴的是 NotificationSender 介面,只要新的實作也符合這個介面契約,換掉底層應該是安全的重構。

於是 AI 寫了一個新的 ThirdPartyNotificationSender 類別,符合介面契約,接著直接修改了容器裡綁定 SmtpNotificationSender 的那一行,把它換成新類別:

❌ 只換掉裝飾器包住的那一層:
$container->bind(NotificationSender::class, function ($c) {
    $rawSender = new ThirdPartyNotificationSender($c->get('apiClient'));
    return new RateLimitedNotificationSender($rawSender, $c->get('rateLimiter'));
});
// 看起來只是換了實作,裝飾器邏輯應該還在……

這段改動看起來沒問題,測試也全部通過——因為單元測試裡呼叫 OrderService 時,容器仍然回傳一個包了限流邏輯的物件。問題出在另一個地方:專案裡還有一段背景排程程式碼,繞過主容器,直接手動 new ThirdPartyNotificationSender(...) 來發送批次通知,因為當初這段程式碼是為了效能考量、避免走完整的容器解析流程而手寫的。AI 在查證「這個類別還有哪些地方被直接建構」時,沒有意識到需要搜尋的不只是介面的呼叫端,還包括所有繞過容器、直接 new 具體類別的地方——而這正是限流邏輯真正被繞過去的入口。

為什麼「介面契約沒變」不保證「行為沒變」

介面契約規定的是「這個方法接受什麼、回傳什麼」,不規定「呼叫這個方法之前後,還有沒有其他邏輯會發生」。 裝飾器模式的本質,就是在不改變介面契約的前提下,附加額外的行為——這正是它好用的地方,卻也是它容易被 AI 忽略的地方:AI 讀程式碼時习慣性地把「依賴介面」等同於「這個依賴的完整行為都寫在介面文件裡」,但實際上裝飾器加的行為,只存在於容器設定那一行,不存在於介面本身。

用一組對照來看這件事:

❌ 只驗證介面契約沒變:
「新類別實作了 NotificationSender 介面,
 方法簽章一致,測試通過,可以安全替換。」
→ 沒有查證容器實際組裝這個依賴時,
  中間還包了哪些裝飾器、加了哪些額外邏輯

✅ 先查容器怎麼組裝這個依賴:
「先看容器設定裡 NotificationSender 是怎麼 bind 的,
 發現外層包了一個限流裝飾器;
 換底層實作時要維持這個裝飾器結構,
 並且搜尋一遍有沒有繞過容器、直接 new 具體類別的地方。」
→ 把容器組裝邏輯當成需要查證的一部分,
  不是只看介面契約

AI 對「介面契約沒變、應該安全」給出的信心,只涵蓋了它讀過的那份介面定義,沒有涵蓋容器設定裡另外附加的行為,也沒有涵蓋繞過容器的例外情況。 這正是這個系列反覆出現的模式——自信的範圍,跟實際查證的範圍不一致。

這個陷阱教會我們的事

DI 容器讓「依賴介面而不是具體實作」這個架構原則變得容易落地,這是它的價值;但容器設定本身也是一段程式碼,裡面可能藏著裝飾器、可能藏著條件式綁定、可能藏著跟環境有關的組裝邏輯——這些邏輯沒有寫在介面裡,也不會出現在型別提示上,只有真正去讀容器設定的那個人才看得到。 對 AI 來說,「查看容器怎麼組裝一個依賴」跟「查看介面定義」是兩個必須分開執行的查證步驟,缺一步都可能讓改動看起來安全、實際上留下缺口。

今日思考題

回想你手上系統裡用到 DI 容器的地方:如果現在要換掉某個依賴的底層實作,你知道容器設定裡有沒有裝飾器、條件式綁定包在外面嗎?如果不確定,你會怎麼查?

今日重點回顧

  • 一個看似安全的「換底層實作」改動,因為容器設定裡有裝飾器包住原本的依賴,換完之後意外繞過了裝飾器邏輯
  • 真正的缺口出在繞過容器、直接 new 具體類別的另一段程式碼,換底層實作時沒有搜尋到這種例外情況
  • 介面契約規定方法簽章,不規定「呼叫前後還有沒有其他邏輯」,裝飾器加的行為只存在於容器設定裡
  • 換依賴實作前,要把「查容器怎麼組裝這個依賴」跟「查介面定義」當成兩個分開的查證步驟

明日預告

明天要換一個角度:測試邊界跟架構邊界的關係——一段程式碼能不能被獨立測試,其實反映了它的架構乾不乾淨,這正好可以拿來當一種「架構品質」的檢驗方法。


上一篇
Day 14:依賴注入容器——架構約束力還是另一層隱藏耦合?
系列文
AI 寫 Code 之後,我們還需要 Software Architecture 嗎?15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言