2024-11-10 029_建構子的演進

2024-11-10 029_建構子的演進

依賴注入 (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. 硬編碼依賴 (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 容器自動管理依賴,進一步簡化設計流程。

當然,讓我們逐行分析 硬編碼依賴 (Tightly Coupled Code) 的語法和設計。這裡有兩個主要的類別:

  1. CreditCardPayment:代表一種支付方式。
  2. PaymentProcessor:處理支付的主要類別。

在此設計中,PaymentProcessor 直接依賴於 CreditCardPayment,因此在程式中是“硬編碼”的依賴關係。

1
2
3
4
5
6
7
public class CreditCardPayment
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

逐行解釋

  • **public class CreditCardPayment**:

    • 這行宣告了一個名為 CreditCardPayment 的公共類別,這是一個支付方式的具體實現。
    • 關鍵字 public 表示這個類別可以從其他類別或命名空間訪問。
    • 類別的名稱 CreditCardPayment 顯示它的用途:處理信用卡支付。
  • **public void ProcessPayment(decimal amount)**:

    • 這行宣告了一個名為 ProcessPayment 的公共方法,用於處理支付。
    • void 表示此方法沒有返回值。
    • 方法參數 (decimal amount) 用於接收支付的金額,decimal 類型適合表示精確的小數值,通常用於金融計算。
  • **Console.WriteLine($"Processing credit card payment of {amount}");**:

    • 這行代碼會打印出支付的訊息,用來模擬支付處理的過程。
    • $ 表示字符串插值,可以直接在字符串中插入變數或表達式的值。
    • 這裡的 {amount} 會被替換為 amount 的值,構建出 "Processing credit card payment of 100.0" 類似的輸出。

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);
}
}

逐行解釋

  • **public class PaymentProcessor**:

    • 宣告一個名為 PaymentProcessor 的公共類別,表示支付處理器。
    • PaymentProcessor 類別的責任是完成支付流程。
  • **private CreditCardPayment _paymentMethod = new CreditCardPayment();**:

    • 宣告了一個私有變數 _paymentMethod,其類型為 CreditCardPayment
    • private 關鍵字表示 _paymentMethod 只能在 PaymentProcessor 類別內部使用。
    • _paymentMethod 被初始化為一個新的 CreditCardPayment 實例,這裡的 new CreditCardPayment() 表示直接在程式內部創建該依賴。
    • 因此,PaymentProcessor 只能使用 CreditCardPayment 作為支付方式,形成“硬編碼依賴”。
  • **public void CompletePurchase(decimal amount)**:

    • 宣告一個名為 CompletePurchase 的公共方法,用於完成購買。
    • 方法的參數 (decimal amount) 代表交易的金額。
  • **_paymentMethod.ProcessPayment(amount);**:

    • 調用 _paymentMethodProcessPayment 方法來完成支付。
    • 這裡 _paymentMethod 依賴於具體的 CreditCardPayment 類別,這是“硬編碼依賴”的一部分。

總結

在這種設計中,PaymentProcessor 無法輕易地支持其他支付方式,因為它依賴於 CreditCardPayment 的具體實現。

在這裡 new CreditCardPayment() 後的括號 () 是用來呼叫 CreditCardPayment 類別的**建構子 (constructor)**。

為什麼需要 () 括號

  • 呼叫建構子:當你寫 new CreditCardPayment()() 表示呼叫這個類別的建構子。建構子是一個特殊的方法,會在物件被創建時自動執行,用於初始化物件的狀態。

  • 初始化過程:若 CreditCardPayment 類別中有自訂的建構子,或即使沒有自訂建構子,C# 也會有一個隱含的無參數建構子,即空參數的 CreditCardPayment()。這樣的呼叫表示創建類別的實例並且執行基礎的初始化。

new CreditCardPayment() 中,() 不僅是語法上的要求,也表達了對物件初始化的需求。如果類別中有其他參數化的建構子,例如 CreditCardPayment(int accountId),則必須提供相應的參數才能創建該類型的實例。

呼叫建構子 (Calling a Constructor) 是指在創建物件時,自動執行該類別的建構子方法以初始化物件的狀態。建構子是一種特殊的方法,用來設定物件初始值或進行必要的初始化工作,以便物件能夠正常運行。

什麼是建構子?

建構子 (Constructor) 是一個和類別同名的方法,且沒有回傳值。在 C# 中,當你使用 new 關鍵字創建一個物件時,這個建構子就會被呼叫。

建構子的用途

  • 初始化物件屬性:可以用來設定物件的初始屬性值。
  • 依賴注入:可以用來接收參數,將外部依賴注入到物件中。
  • 設定預設值或執行初始邏輯:例如開啟檔案、建立資料庫連線等等。

例子:建構子的基本使用

以下是帶有建構子的簡單範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
public string Name;
public int Age;

// 這是一個建構子
public Person(string name, int age)
{
Name = name; // 將參數 name 設定給屬性 Name
Age = age; // 將參數 age 設定給屬性 Age
}
}

// 呼叫建構子來創建 Person 物件
Person person = new Person("Alice", 30); // 呼叫建構子並傳入 "Alice" 和 30

呼叫建構子的過程

  1. 使用 new 關鍵字new 關鍵字會創建 Person 類別的新物件,並在該物件上呼叫 Person 類別的建構子。
  2. 傳遞參數new Person("Alice", 30) 會將 "Alice"30 傳入建構子,並將這些值分別賦予 NameAge 屬性。
  3. 返回物件:建構子執行完後,person 變數會指向新創建並初始化的 Person 物件。

沒有參數的建構子

如果類別內沒有自訂建構子,C# 會自動提供一個無參數建構子

1
2
3
4
5
6
7
8
public class Person
{
public string Name = "Unknown";
}

// 無參數建構子會自動被呼叫
Person person = new Person(); // 使用內建的無參數建構子
Console.WriteLine(person.Name); // Output: Unknown

在這個例子中,Person 類別沒有建構子,所以當呼叫 new Person() 時,C# 提供了一個隱含的無參數建構子,並將 Name 的初始值設為 "Unknown"

接近了!在這段範例中,隱含建構子的寫法其實是這樣的:

1
2
3
4
5
6
7
8
9
10
11
public class Person
{
// 隱含建構子(無參數)
public Person()
{
// 不需要特別的初始化邏輯
}

// 屬性 Name 的初始值設為 "Unknown"
public string Name = "Unknown";
}

這個隱含建構子在實際情況中不需要我們手動寫出。只要類別內部沒有定義任何建構子,C# 編譯器就會自動生成這個無參數建構子。

在這種情況下,如果你希望 CreditCardPayment 類別的建構子可以接受參數(如 cardNoCSVexpYYMM),那麼建構子的定義需要匹配這些參數,並在初始化時將它們賦值給類別中的私有欄位。你可以這樣設置建構子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CreditCardPayment
{
private string _cardNo;
private string _CSV;
private string _expYYMM;

// 帶有參數的建構子,用來初始化欄位
public CreditCardPayment(string cardNo, string CSV, string expYYMM)
{
_cardNo = cardNo;
_CSV = CSV;
_expYYMM = expYYMM;
}
}

這樣,當你使用 new CreditCardPayment(cardNo, CSV, expYYMM) 時,就會調用這個帶參數的建構子,並將 cardNoCSVexpYYMM 的值分別賦予對應的欄位 _cardNo_CSV_expYYMM

所以像 2. 引入接口 (Programming to an Interface) 的方式,
那如果要新增支付方式, 就還是要修改
public class PaymentProcessor {
裡面的這段程式:
private IPaymentMethod _paymentMethod = new CreditCardPayment(); // 這裡仍然是硬編碼
就是把 new CreditCardPayment() , 改成我要的支付方式,
比如是 轉帳 , 就是 new BankTransferPayment() , 對嗎?

是的,你的理解是正確的!在這種情況下,PaymentProcessor 類別內部的 _paymentMethod 仍然是硬編碼的依賴。這意味著如果你想新增支付方式,就需要在 PaymentProcessor 類別中手動修改這段程式碼。

例如,如果你希望將支付方式從信用卡改為銀行轉帳,你需要將以下這行:

1
private IPaymentMethod _paymentMethod = new CreditCardPayment(); // 這裡仍然是硬編碼

改成:

1
private IPaymentMethod _paymentMethod = new BankTransferPayment(); // 改為銀行轉帳支付

這樣的缺點

這種方式的缺點是你必須每次都手動更改 PaymentProcessor 類別的代碼,這可能導致代碼不易維護和擴展,尤其是在有多個地方使用 PaymentProcessor 的情況下。

如何改善?

為了避免這個問題,你可以使用依賴注入(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
public class PaymentProcessor
{
private IPaymentMethod _paymentMethod;

// 透過建構子注入支付方法
public PaymentProcessor(IPaymentMethod paymentMethod)
{
_paymentMethod = paymentMethod;
}

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

// 使用示例
public class Program
{
public static void Main(string[] args)
{
// 在這裡選擇使用哪一種支付方式
IPaymentMethod paymentMethod = new BankTransferPayment();
PaymentProcessor processor = new PaymentProcessor(paymentMethod);
processor.CompletePurchase(100.00m);
}
}

這樣,你只需要在創建 PaymentProcessor 的時候決定使用哪一種支付方式,而不需要修改 PaymentProcessor 的內部代碼。這種方法更符合開放-封閉原則(Open/Closed Principle),即對擴展開放,對修改封閉。

依賴注入(Dependency Injection,DI)的使用確實是設計模式演進過程的一部分,目的是解決早期設計中硬編碼依賴所帶來的問題。以下是一些要點來總結這個演變過程:

演進過程總結

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

    • 在這個階段,類別之間有強烈的耦合,每個類別直接創建其依賴對象,這使得代碼難以測試和維護。當需要改變或擴展功能時,必須直接修改代碼。
  2. 引入接口 (Programming to an Interface):

    • 當引入接口時,類別開始依賴於抽象而不是具體實現。這樣做可以使代碼更靈活,但仍然需要在類別內部進行修改以更改依賴。這樣的設計雖然比硬編碼依賴有所改善,但在需要頻繁修改的場景中仍然不夠靈活。
  3. 依賴注入 (Dependency Injection):

    • 這個階段的主要目的是減少類別之間的耦合。通過將依賴作為參數傳遞給類別,開發者可以輕鬆地在不改變類別內部代碼的情況下更改其行為。這樣的設計符合開放-封閉原則(OCP),即對擴展開放,對修改封閉。這意味著你可以新增新的支付方式(或其他功能)而不需要改變已經存在的類別。

總結

DI 不僅提高了代碼的靈活性和可維護性,還使單元測試變得更加容易,因為你可以使用模擬對象來測試你的類別,而不需要依賴於具體的實現。

這樣的設計模式使得系統架構更加清晰、可擴展且易於維護,並且能夠更好地應對未來的需求變更。