2026-06-20 C# DI 的語法構造是什麼?

2026-06-20 C# DI 的語法構造是什麼?

我:
DI 的語法構造是什麼?

GPT:
在 C# 中,依賴注入(Dependency Injection, DI)是一種設計模式,通常透過建構子注入、屬性注入或方法注入等方式來實現。依賴注入的主要目的是讓程式更具彈性和可測試性,因為具體實現可以在外部動態替換。

1. 建構子注入 (Constructor Injection)

建構子注入是最常見的依賴注入方式。主要是在類的建構子中傳入依賴的物件,並將其儲存在類的私有欄位中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1. 定義接口
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}

// 2. 實現接口的具體類別
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

// 3. 依賴注入的類別,接收 IPaymentMethod 的實現作為依賴
public class PaymentProcessor
{
private readonly IPaymentMethod _paymentMethod;

public PaymentProcessor(IPaymentMethod paymentMethod)
{
_paymentMethod = paymentMethod;
}

public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}

// 使用時
IPaymentMethod paymentMethod = new CreditCardPayment();
PaymentProcessor processor = new PaymentProcessor(paymentMethod);
processor.CompletePurchase(100);

2. 屬性注入 (Property Injection)

屬性注入是通過公共屬性將依賴注入進去。在某些情況下,如果依賴不是在建構子時就必須使用,這種方式可能會更靈活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PaymentProcessor
{
public IPaymentMethod PaymentMethod { get; set; }

public void CompletePurchase(decimal amount)
{
PaymentMethod?.ProcessPayment(amount); // 需要確認非空
}
}

// 使用時
PaymentProcessor processor = new PaymentProcessor
{
PaymentMethod = new CreditCardPayment() // 設定依賴
};
processor.CompletePurchase(100);

3. 方法注入 (Method Injection)

在方法注入中,依賴作為參數傳入到方法中,這通常適用於臨時需要的依賴,不一定要成為類的屬性或欄位。

1
2
3
4
5
6
7
8
9
10
11
public class PaymentProcessor
{
public void CompletePurchase(decimal amount, IPaymentMethod paymentMethod)
{
paymentMethod.ProcessPayment(amount);
}
}

// 使用時
PaymentProcessor processor = new PaymentProcessor();
processor.CompletePurchase(100, new CreditCardPayment());

4. 使用內建的依賴注入容器

在 ASP.NET Core 等框架中,內建了 DI 容器,可以更方便地管理和注入依賴。在 Startup.cs 中配置服務:

1
2
3
4
5
6
public void ConfigureServices(IServiceCollection services)
{
// 註冊 IPaymentMethod 的具體實現
services.AddScoped<IPaymentMethod, CreditCardPayment>();
services.AddScoped<PaymentProcessor>();
}

在使用時,透過依賴注入容器自動解析並注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SomeController : ControllerBase
{
private readonly PaymentProcessor _processor;

public SomeController(PaymentProcessor processor)
{
_processor = processor;
}

public IActionResult Purchase(decimal amount)
{
_processor.CompletePurchase(amount);
return Ok();
}
}

總結

  • 建構子注入:適合必須提供的依賴。
  • 屬性注入:適合可選的依賴。
  • 方法注入:適合臨時需要的依賴。
  • 使用DI 容器能夠自動解析依賴,簡化管理。

通過這些模式和方法,依賴注入讓程式碼更加模組化,增強了可讀性、測試性和擴展性。

我:
為什麼要用DI 注入? 它有演進過程嗎?

GPT:
依賴注入 (Dependency Injection, DI) 是一種用來實現鬆耦合設計的設計模式,目的是改善系統的彈性、可維護性和測試性。DI 的出現源於面向對象設計中的依賴倒置原則(Dependency Inversion Principle, DIP)以及控制反轉(Inversion of Control, IoC)的理念。這個過程經歷了幾個階段的演進。

DI 的演進過程

  1. 硬編碼依賴 (Tightly Coupled Code)
    最早期,物件之間的依賴是直接硬編碼的。例如,一個 PaymentProcessor 可能直接依賴於 CreditCardPayment 類別:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class PaymentProcessor
    {
    private CreditCardPayment _paymentMethod = new CreditCardPayment();

    public void CompletePurchase(decimal amount)
    {
    _paymentMethod.ProcessPayment(amount);
    }
    }

    這種硬編碼方式帶來很大的限制。每次要更改支付方式,都需要修改 PaymentProcessor 類別的程式碼,導致系統的擴展性和維護性變差。

  2. 使用接口進行抽象 (Programming to an Interface)
    為了實現鬆耦合,我們可以引入接口。接口定義了支付方法的抽象,並允許不同的支付方式去實現該接口,但仍然有個缺點:我們還是必須在類別內部實例化具體的支付方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class PaymentProcessor
    {
    private IPaymentMethod _paymentMethod = new CreditCardPayment();

    public void CompletePurchase(decimal amount)
    {
    _paymentMethod.ProcessPayment(amount);
    }
    }
  3. 依賴注入的引入 (Dependency Injection)
    DI 將具體實例的創建移到外部,讓 PaymentProcessor 類別不再直接實例化支付方式,而是由外部在初始化時提供具體的依賴,從而達到完全的鬆耦合設計。現在,只需改變外部傳入的依賴即可變更支付方式,而無需修改 PaymentProcessor 類別。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class PaymentProcessor
    {
    private readonly IPaymentMethod _paymentMethod;

    public PaymentProcessor(IPaymentMethod paymentMethod)
    {
    _paymentMethod = paymentMethod;
    }

    public void CompletePurchase(decimal amount)
    {
    _paymentMethod.ProcessPayment(amount);
    }
    }

    使用 DI,程式碼變得更具彈性,因為可以在建構 PaymentProcessor 時提供不同的支付方式,例如 CreditCardPaymentPaypalPayment

  4. DI 容器和自動化依賴管理
    在大型應用程式中,手動管理依賴注入可能變得複雜,因此 DI 容器應運而生。DI 容器可以自動解析和管理依賴關係,從而簡化了注入過程。框架如 ASP.NET Core 提供了內建的 DI 容器,允許透過配置的方式在整個應用中管理依賴。

DI 的優勢

  • 減少耦合性:物件之間的依賴關係變得靈活,不再直接依賴具體實現,而是依賴於抽象接口。
  • 提高可測試性:通過 DI,可以輕易替換依賴的具體實現,例如在單元測試中注入模擬 (mock) 物件。
  • 增強可擴展性:增加新功能或替換某個依賴時,不需要更改使用這些依賴的類別。
  • **控制反轉 (IoC)**:透過 DI,對象的依賴由容器管理,不再由物件自己控制,這是 IoC 原則的具體應用。

總結

DI 的引入是對系統設計演進過程中的一次重大改進,使系統的各個組件可以更加靈活地替換、測試和擴展。在應用 DI 模式時,建構一個高效的依賴管理系統(如 DI 容器)可以大幅簡化代碼,減少耦合,並提升整體代碼品質。

我:
妳說的 1. 2. 3. 演進過程, 有點難懂, 可以用具體的程式碼說明, 每一個演進遇到的困難嗎?

GPT:
當然可以,我們來看一個具體的範例,從最初的硬編碼依賴,到使用接口,再到依賴注入的改進,每一步都會展示程式碼如何演進,以及為什麼每個演進步驟更有利於系統的設計。

1. 硬編碼依賴 (Tightly Coupled Code)

在最早期的設計中,我們會直接在類別中實例化依賴對象,比如說在 PaymentProcessor 中直接依賴 CreditCardPayment 類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CreditCardPayment
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

public class PaymentProcessor
{
private CreditCardPayment _paymentMethod = new CreditCardPayment();

public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}

問題

  • 如果未來需要支持其他支付方式(例如 PayPal 或銀行轉帳),我們需要改動 PaymentProcessor 類別,新增或替換具體的支付類別。
  • 系統的可擴展性差:每次需求變更都需要直接修改程式碼。
  • 測試難度增加:無法模擬 CreditCardPayment 來進行單元測試。

2. 引入接口 (Programming to an Interface)

我們可以將支付方式抽象成一個接口 IPaymentMethod,讓不同支付方式各自實現這個接口。這樣,PaymentProcessor 類別可以依賴於 IPaymentMethod 而非具體的 CreditCardPayment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}

public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

public class PaymentProcessor
{
private IPaymentMethod _paymentMethod = new CreditCardPayment(); // 依然是硬編碼的依賴

public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}

優點和問題

  • 可擴展性提升:增加支付方式時,只需新增實現 IPaymentMethod 的類別,PaymentProcessor 不需要改動。
  • 耦合性降低PaymentProcessor 不再依賴於具體的支付方式。
  • 問題仍然存在PaymentProcessor 依然自己決定具體使用哪一個支付方式(仍然硬編碼了 new CreditCardPayment()),導致擴展性受限。如果需求更變,仍需修改 PaymentProcessor 類別的程式碼。

3. 依賴注入 (Dependency Injection)

這一步,我們通過依賴注入來進一步改善設計,將 PaymentProcessor 的支付方式作為外部傳入的依賴,從而達到更高的靈活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
}

public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

public class PaypalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount}");
}
}

public class PaymentProcessor
{
private IPaymentMethod _paymentMethod;

// 建構子注入:在建構時由外部提供具體的支付方式
public PaymentProcessor(IPaymentMethod paymentMethod)
{
_paymentMethod = paymentMethod;
}

public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}

現在,使用 PaymentProcessor 時,可以選擇傳入不同的支付方式:

1
2
3
4
5
6
7
IPaymentMethod paymentMethod = new CreditCardPayment();
PaymentProcessor processor = new PaymentProcessor(paymentMethod);
processor.CompletePurchase(100);

paymentMethod = new PaypalPayment();
processor = new PaymentProcessor(paymentMethod);
processor.CompletePurchase(50);

優點

  • 完全鬆耦合PaymentProcessor 不再負責創建具體支付方式。它的依賴由外部提供,使得系統更具彈性。
  • 便於測試:可以在測試中傳入一個模擬 IPaymentMethod,來測試 PaymentProcessor 的功能,而不需要實際的支付方式。
  • 擴展性更高:增加或更改支付方式時,無需修改 PaymentProcessor 的內部實現。

DI 總結

依賴注入將類別的依賴關係從內部創建轉移到外部提供,使系統更易擴展和維護。在大型應用中,依賴注入可以使用 DI 容器自動管理依賴,進一步簡化設計流程。