「我們全面導入依賴注入容器了,所有類別都透過建構子拿依賴,架構應該乾淨很多了吧?」
這句話只對了一半。依賴注入容器確實是這個系列前幾天講的架構原則(介面切分、依賴反轉)最常見的實現工具——它強迫你把依賴宣告在建構子上,逼你依賴介面而不是具體實作。但今天要講一個容易被忽略的另一面:DI 容器在強制一種耦合消失的同時,悄悄引入了另一種耦合——只是這種耦合不在程式碼裡,在容器設定裡,程式碼本身完全看不出來。
先講清楚 DI 容器的架構價值。沒有 DI 容器時,一個類別要用到另一個依賴,最直接的寫法就是在方法內部直接 new 出來,或者呼叫一個全域單例——這兩種寫法都讓依賴關係藏在方法內部,讀介面/建構子完全看不出來,測試時也沒辦法替換掉這個依賴。
DI 容器強迫依賴宣告攤在建構子的參數列表上,這件事本身確實是架構約束——它讓「這個類別依賴什麼」變成一個可以從簽章一眼看出的事實,也讓依賴反轉(型別提示介面而不是具體類別)有了落地的地方。
問題出在下一步:建構子參數寫的是 PaymentGatewayInterface,這件事只回答了「這個類別依賴一個付款閘道的抽象契約」,完全沒有回答「執行時,容器實際會塞進哪一個具體實作」。這個答案通常寫在容器的綁定設定裡——可能是一份獨立的設定檔,可能是啟動時執行的一段綁定程式碼,而這份設定不會出現在使用這個依賴的類別附近,甚至可能散落在專案的另一個角落。
用一組對照來看這個落差:
❌ 只看建構子,看不出實際依賴的實作:
class CheckoutService
{
public function __construct(
private PaymentGatewayInterface $gateway
) {}
}
→ 讀到這裡,只知道「依賴某個付款閘道」,
不知道正式環境實際綁定的是哪一個具體實作,
也不知道這個實作有沒有被其他綁定條件(環境變數、feature flag)覆寫過
✅ 容器綁定設定跟依賴使用處放在一起查證,而不是分開假設:
// container-bindings.php
$container->bind(PaymentGatewayInterface::class, function () {
return match (env('PAYMENT_PROVIDER')) {
'stripe' => new StripeGateway(),
'sandbox' => new SandboxGateway(),
default => throw new RuntimeException('unknown provider'),
};
});
→ 實際綁定的實作,取決於一個環境變數,
這個資訊只存在綁定設定裡,讀 CheckoutService 本身完全看不到
這正是 DI 容器架構兩面性的核心:它讓「依賴什麼抽象」變透明,卻讓「依賴哪個具體實作」變得更不透明——因為這個答案從「就寫在呼叫的那一行」搬到了「要另外去查容器設定才知道」。
一個人類工程師在專案裡待久了,通常會累積出「這個介面在這個環境下綁的是哪個實作」的直覺記憶。但 AI 每次接手一個任務時,這種記憶並不存在——它讀到 CheckoutService 的建構子,看到型別提示是介面,如果它止步於「型別提示介面,代表架構是乾淨的」這個結論,就漏掉了「這個介面實際指向哪裡」這個必須另外查證的事實。
這跟這個系列的主題句是同一件事的另一種樣貌:AI 對「這段程式碼依賴什麼」給出的「已確認」結論,如果只查了型別提示、沒有查容器的實際綁定設定,那個結論的查證範圍就沒有涵蓋到真正決定執行期行為的地方。 一個常見的具體後果是:AI 要修改或測試一段依賴付款閘道的邏輯時,以為自己面對的是介面契約,實際上正式環境綁定的實作有著介面沒有寫明的額外行為(例如重試邏輯、超時設定),AI 沒有查證綁定設定,就對這段邏輯的行為做出了錯誤假設。
這裡要澄清一個容易被誤解的方向:這篇的重點不是「DI 容器有問題,不該用」——依賴反轉、可測試性這些好處是真實的,放棄 DI 容器只會讓耦合問題變得更直接、更難處理。重點是誠實面對 DI 容器把耦合「搬家」了,而不是「消除」了——耦合從程式碼裡的具體依賴,變成容器設定裡的綁定關係,這個耦合依然存在,只是換了一個更難被隨手讀到的地方存在。
具體的因應做法,是把容器的綁定設定當成跟程式碼同等重要的查證對象——AI(或任何協作者)在對一段有依賴注入的程式碼做判斷之前,同樣要去查一次「這個介面在當前情境下實際綁定的是什麼」,不能只憑型別提示是介面,就假設依賴關係已經完全透明。
回想你手上的專案:如果有人問你「這個介面在正式環境實際綁定的是哪個實作」,你能不看容器設定檔就答出來嗎?如果不能,那 AI 在讀這段程式碼時,跟你面對的是同一個資訊落差。
明天用一個具體案例,把今天講的兩面性落地:一次因為沒有查證容器綁定設定,讓架構邊界在實務上變得模糊的真實陷阱。