# 依賴注入 - DI(Dependency injection) ###### tags: `C#` ## 1. IoC — Inversion of Control 控制反轉 >IoC是一個設計思想,把對於某個物件的控制權移轉給第三方容器 ### 1.1 範例描述 你是否有個工具人男友 或 常發好人卡? 有的話先恭喜你自帶IoC容器~~ <font size="5">需求:</font> 某個夜深人靜的夜晚,想吃雞排又爽爽喝珍奶時 - 概念圖 ![](https://i.imgur.com/AfXIOgc.png) >A : 開車去商圈 >B : 到雞排店吃雞排 >C : 到飲料店喝珍珠奶茶 >IoC Container : 好(人)朋友 <font size="5">- 邊緣人情境 >> Original </font> 你需要自行開車(A)前往雞排店(B)+飲料店(C )去購買才吃的到 ※上述可知,我直接耦合B和C <br> <font size="5">- 招喚工具人 >> 使用 IoC </font> 只需準備一張好人卡,寫上需求:肚子餓了好想喝珍奶又配雞排,發給工具人 之後你只要等待熱騰騰的餐點**主動**幫您送上,不需要管好人如何幫你<font size="5" >**實現**</font> ※上述可知,我(被動接收的物件)把依賴的對象從店家(被依賴的物件) 變成依賴好(人)朋友(DI Container) <br> ### 1.2 技術描述 沒有引入IoC容器之前,A物件依賴于B物件,那麼A物件在初始化或者運行到某一點的時候,自己必須主動去new B物件或者使用已經創建的B物件,無論是創建還是使用B物件,控制權都在自己手上。 引入IoC容器之后,這種情形就改變了, 物件A與物件B之間失去了直接聯系,所以 A 的代碼只需要定義一個 private 的B物件,不需要直接 new 來獲得這個物件,而是通過相關的容器,控制程式將B物件在外部new出來並注入到A類裡。 **控制反轉**是把原本A對B控制權移交給第三方容器 降低A對B物件的耦合性,讓雙方都倚賴第三方容器。 <br> <br> <br> <br> <br> <br> <br> <br> <br> <hr> ## 2. 依賴注入(Dependency injection) DI 是將一個程式的相依關係改由呼叫它的外部程式來決定的方法。 主要會使用到抽象介面,讓組件與組件之間依賴於**抽象介面**, 當組件要與其它實際的物件發生依賴關係時,僅透過**抽象介面**來注入依賴的實際物件。 <br> --- ### 2.1 使用介面 (interface) 去設計程式 請問你都如何稱呼男/女友? 叫對方的「名字」,還是「寶貝」? 如果叫的是「==寶貝==」,那恭喜你 你是<font size="5" color="red"> **DI大師**👍</font> 因為你使用了<font size="5" color="red">介面</font>(「寶貝」稱呼)而非實作(不同的名字), 在不同的地方換了男/女友(抽換不同的實作)也都不用改稱謂 。 而<font size="5" color="red">設定</font>現在這個寶貝是哪位的控制行為,可先理解為DI 介面在 IoC/DI 框架裡面非常重要,因為透過interface把功能抽離,我們的框架才能夠使用IoC的方式來決定誰去實作(也就是實際的方法) ![](https://i.imgur.com/imX8sm7.png) >如果統一叫寶貝就不會有這個問題了 <br> <br> <br> <br> --- ### 2.2 什麼是 依賴 (Dependency) 在開始範例程式之前,得先知道什麼是『 依賴 』: 依賴,白話就是『需要』,但 為何需要呢? >—— 用來達到 目的/功能 X 需要 Y —— > 用來達到 目的 Z 例如: * 小明需要車車 ——> 脫魯 * 老王需要食物 ——> 填飽肚子 * ~~地方的阿姨需要 ——> (誤)~~ <br> 而在物件導向程式(OOP)中,程式是透過許多類別(Class)的實例(instance),也就是物件(object),彼此的交互組合來實現各種功能。物件導向程式的 **依賴** 關係 是指「一個物件需要另一個物件才能作用」的意思。 --- :::info (以下為補充資訊:有興趣再繼續看吧) ::: 淺談一下 - **SOLID 依賴反轉原則 Dependency Inversion Principle (DIP)** 由上述的例子可以看出: ><font color="b">我們真正所需要的、依賴的,其實不是實作的**類別與物件**,而是他所擁有的**功能**。 其實這就是 依賴反轉原則 DIP (Dependency Inversion Principle):</font> 1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象。 1. 抽象不應該依賴於具體實作方式。 1. 具體實作方式則應該依賴抽象。 名詞解釋 * 高階與低階,是相對關係,其實也就是 呼叫者 (Caller) 與 被呼叫者 (Callee), 此例中: - 高階: 人 - 低階: 車車、食物 * 抽象,是指 介面 (interface) 或是 抽象類別 (Abstract Class), 也就是不知道實作方式。 * 具體實作方式,就是指有實作介面或是繼承抽象的 非 抽象類別。如範例中: - 車車為抽象,實作的車子可能為: Toyota、Benz - 食物為抽象,實作的食物可能為: 義大利麵、漢堡 <br><br><br> --- ### 2.3 DI 跟 IoC 的關係是……? 控制反轉 ( IoC ) 是一種 **思想** 依賴注入 ( DI ) 是一種 **設計模式** DI是實作IoC的一種方式,但是IoC還有其他實作方式,例如 : * DI(例如我需要車子類別不直接用實作,而是注入 ICar 介面) * 工廠模式(拉個 CarFactory 類別把控制產生執行個體如 Benz, BMW 類別的權力移轉給工廠,再由工廠拿車)……等。 結論:IoC 的範疇包含 DI,但不僅限於 DI。 <br> <br> <br> <br> <br> <br> <br> <br> --- ## 3. DI 範例 >範例使用 .net core 6、VS2022 以下我們將使用 C# (.net 6 WebAPI) 示範並重構一個非常簡單的程式碼,來解釋DI和IoC容器。 ### 3.1 第一次嘗試 - 傳統寫法 功能需求 : ==構建一個可以讓使用者輸入產品名稱,並搜尋產品清單的應用程式。== 我們先從建立分層架構開始。使用分層架構有很多個好處,但我們不會在這介紹太多, 因為我們關注的是依賴注入。 ![](https://i.imgur.com/CuR116Y.png) >之後我們將圖中的名詞先縮寫檢視 > Business Layer 業務邏輯層 -> BL > Data Access Layer 資料邏輯層 -> DAL 首先,我們將從建立一個Product類開始: ```csharp= public class ProductModel { /// <summary>產品編號</summary> public int Id { get; set; } /// <summary>產品名稱</summary> public string Name { get; set; } /// <summary>說明</summary> public string Description { get; set; } /// <summary>銷量</summary> public int Sales { get; set; } } ``` 然後,我們將建立資料訪問層(DAL): ```csharp= public class ProductDAL { private readonly List<ProductModel> _products; public ProductDAL() { _products = new List<ProductModel> { new ProductModel { Id = 1, Name= "iPhone 12", Description = "iPhone 12 mobile phone" , Sales = 100}, new ProductModel { Id = 2, Name= "iPhone 13", Description = "iPhone 13 mobile phone", Sales = 200}, new ProductModel { Id = 3, Name= "iPhone 13 Pro", Description = "iPhone 13 Pro mobile phone", Sales = 300}, }; } public IEnumerable<ProductModel> GetProducts(string name) { var result = _products.Where(p => string.IsNullOrEmpty(name) || p.Name.Contains(name)) .ToList(); return result; } } ``` 接著,我們將建立業務層(BL): ```csharp= public class ProductBL { private readonly ProductDAL _productDAL; public ProductBL() { //建構式就 new 出實作的DAL服務 _productDAL = new ProductDAL(); } public IEnumerable<ProductModel> GetProducts(string name) { return _productDAL.GetProducts(name); } } ``` 最後,我們建立Controller(UI Layer): ```csharp= /// <summary>第一次嘗試</summary> [ApiController] [Route("[controller]")] public class FirstController : Controller { private readonly ProductBL _productBL; public FirstController() { //建構式就 new 出實作的BL服務 _productBL = new ProductBL(); } [HttpGet] public IEnumerable<ProductModel> Get(string name) { var products = _productBL.GetProducts(name); return products; } } ``` 我們已經完成第一次嘗試的程式碼,但有幾個問題: - 可以觀察到這幾個在建構函式裡面,都使用 new下一個要使用的實作服務,物件間耦合性強。 - 像是BL它依賴於DAL的實現,要是某天因為不同需求想要更換新的實作,就必須要連帶調整有使用到實作的地方。 <br><br><br> --- ### 3.2 第二次嘗試 - 使用 介面(Interface) + DI 複習一下抽象概念是什麼呢? 抽象是**功能**的定義。 像是我們的例子中,業務層(BL)依賴於資料訪問層(DAL)來取得資料。 而==取得產品資料==這個**功能**就可以使用抽象來重構 在C#中,我們主要使用介面(Interface)實現抽象。 讓我們來建立資料層(DAL)抽象: ```csharp= public interface IProductDAL { IEnumerable<ProductModel> GetProducts(string? name); } ``` 我們還需要更新資料訪問層的實作(Implement),讓他繼承 Interface: ```csharp= public class ProductDAL : IProductDAL ``` 更新業務層(BL),使其依賴於資料訪問層(DAL)的抽象(interface),而不是依賴於資料訪問層的實現(Implement): ```csharp= public class ProductBL { private readonly IProductDAL _productDAL; public ProductBL(IProductDAL productDAL) { //建構式只需指定介面,而介面會由DI決定實作的Service _productDAL = productDAL; } public IEnumerable<ProductModel> GetProducts(string? name) { return _productDAL.GetProducts(name); } } ``` 建立業務層(BL)的抽象: ```csharp= public interface IProductBL { IEnumerable<ProductModel> GetProducts(string? name); } ``` 我們也需要更新業務層(BL)的實作: ```csharp= public class ProductBL : IProductBL ``` 更新UI: ```csharp= /// <summary> /// 第二次嘗試-使用 Interface + DI /// </summary> [ApiController] [Route("[controller]")] public class DIController : Controller { private readonly IProductBL _productBL; public DIController(IProductBL productBL) { //建構式只需指定介面,而介面由DI決定實作的Service this._productBL = productBL; } [HttpGet] public IEnumerable<ProductModel> Get(string? name) { var products = _productBL.GetProducts(name); return products; } } ``` 最後的重頭戲DI設定!! .net 6 官方原生的DI主要設定在 ==Program.cs== 檔 ```csharp= //DI ,為Inserface注入實作 builder.Services.AddSingleton<IProductBL, ProductBL>(); // Product BL builder.Services.AddSingleton<IProductDAL, ProductDAL>(); // Product DAL ``` 現在,如果我們看一下程式碼,我們不是在使用到功能時才建立實作,而是在基礎設施的中建立它。 像這樣把建立BL、DAL的控制與基礎設施結合在一起,也可以稱為控制反轉(IoC)。 我們可以更容易讓不同的團隊在不同的層上工作,只要我們先互相定義好溝通介面之後,我們可以讓一個團隊處理資料訪問層,一個團隊處理業務層,一個團隊處理UI。 能否感受到維護性的提升和可擴充套件性的好處呢? 舉個例子吧,假設今天 DAL層的資料來源,要套用 SQL Server 的真實Table,我們只需建立新的DAL去實作,並調整DI指定新的實作就完工拉! ## 4. DI 生命週期 註冊在 DI 容器的 Service 有分三種生命週期: * Transient 每次注入時,都重新 new 一個新的實例。 * Scoped 每個 Request 都重新 new 一個新的實例,同一個 Request 不管經過多少個 Pipeline 都是用同一個實例。 * Singleton 被實例化後就不會消失,程式啟動後運行期間只會有一個實例。 --- [範例程式GitHub](https://github.com/sd76705/DI_Demo.git)