在現代軟體開發中,IoC 容器、控制反轉、依賴注入等術語早已司空見慣。Spring.NET、Castle Windsor、Unity 等成熟框架更是將這些概念落地為實用工具。但這項技術究竟是如何演進而來?適用場景為何?我們又該如何正確掌握?本文將從設計模式原理出發,層層剖析依賴注入的來龍去脈。
假設我們開發一款「貓狗大戰」遊戲,初期設計了抽象基類 Dog,定義跑、跳、叫等共同行為,僅將外貌 Display() 設為抽象方法。各品種狗狗(貴賓犬、臘腸犬等)繼承基類並實現 Display()。看似完美,但當需求引入「攻擊」行為時,問題浮現:若在基類新增 Attack(),連電子寵物狗 AIBO、絨毛玩具狗都會獲得攻擊能力,顯然違背邏輯。
解決之道在於「介面隔離原則」——提取 IAttack 介面,僅讓具備攻擊能力的狗狗實作。然而需求持續變動:不同狗狗攻擊方式迥異(咬、撲、獅子吼),且升級後會出現新招式。若每個子類各自實作,程式碼將大量重複,且運行時難以動態切換行為。
應用「針對介面編程,不針對實作編程」原則,將攻擊行為提取為獨立類別(BiteAttack、SnapAttack 等)實作 IAttack。狗狗類別持有 IAttack 參考,透過建構子或 SetAttackBehavior() 動態注入具體策略。如此,行為不再綁死於繼承體系,新增攻擊方式無需修改既有類別,完美符合「開放封閉原則」。
雖然引入介面,但程式碼中仍充斥 new ConcreteClass()。例如遊戲場景切換時,需根據條件實例化不同貓咪(山貓、波斯貓、雲貓),導致高層邏輯緊密依賴低層實作,違反「依賴倒置原則」(DIP):高層模組不應依賴低層模組,二者皆應依賴抽象。
引入 IFactory 介面定義 CreateCat()、CreateDog(),具體場景工廠(MountainSceneFactory、VeldtSceneFactory)實作介面。場景類別 VeldtScene 透過建構子注入工廠介面,客戶端僅需決定使用哪個工廠。這樣,實例化邏輯從業務代碼中剝離,達成「配置與使用分離」。
隨著系統擴展,手動管理依賴圖愈發繁瑣。IoC 容器應運而生,Martin Fowler 將其命名為「依賴注入」(DI)。核心思想:由容器負責解析介面對應的具體實作,並將實例注入調用者欄位。開發者僅需聲明依賴關係,容器自動完成物件圖建構。
微軟經典專案 PetShop 4.0 透過 DI 實現 SQL Server 與 Oracle 的無縫切換。IDAL 定義資料存取介面,SQLServerDAL、OracleDAL 分別實作。DALFactory 讀取 Web.config 中的 WebDAL 設定,利用反射動態建立實例。BLL 層僅依賴 ICategory 介面,切換資料庫只需修改設定檔,零程式碼變更。但作者也提醒:若部署環境固定不變,引入 DI 反而增加複雜度與反射效能損耗(約 20%)。
TheBeerHouse 採用自訂設定區段,各模組繼承 DataAccess 基類並實作 Provider,透過單例模式與 Activator.CreateInstance 依設定動態建立實例。SiteProvider 靜態類別提供統一存取入口。相較 PetShop 的簡潔,TBH 更靈活但結構較重。
三者各有優劣,選型取決於團隊習慣與專案需求。
依賴注入的核心適用場景只有一個——「變化」。當預判某處會因部署環境、業務規則、技術棧變更而頻繁調整實作時,DI 能以極低成本應對。反之,若系統無實質變化點,強行引入 DI 屬於過度設計,將引入不必要的複雜度與效能開銷。架構師的智慧,在於精準識別「變化點」並適度應用模式。
從介面導向設計、依賴倒置原則、工廠模式,到依賴注入容器的自動化裝配,軟體架構的演進始終圍繞著「解耦」與「應對變化」。掌握這條脈絡,不僅能靈活運用 DI 框架,更能在架構決策中游刃有餘。