---
# System prepended metadata

title: 依賴注入 - DI(Dependency injection)
tags: [C#]

---

#  依賴注入 - 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)
