# 依賴注入 - DI(Dependency injection)
###### tags: `C#`
## 1. IoC — Inversion of Control 控制反轉
>IoC是一個設計思想,把對於某個物件的控制權移轉給第三方容器
### 1.1 範例描述
你是否有個工具人男友 或 常發好人卡?
有的話先恭喜你自帶IoC容器~~
<font size="5">需求:</font>
某個夜深人靜的夜晚,想吃雞排又爽爽喝珍奶時
- 概念圖

>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的方式來決定誰去實作(也就是實際的方法)

>如果統一叫寶貝就不會有這個問題了
<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 第一次嘗試 - 傳統寫法
功能需求 :
==構建一個可以讓使用者輸入產品名稱,並搜尋產品清單的應用程式。==
我們先從建立分層架構開始。使用分層架構有很多個好處,但我們不會在這介紹太多,
因為我們關注的是依賴注入。

>之後我們將圖中的名詞先縮寫檢視
> 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)