2024-11-10 029_建構子的演進
2024-11-10 029_建構子的演進
依賴注入 (Dependency Injection, DI) 是一種用來實現鬆耦合設計的設計模式,目的是改善系統的彈性、可維護性和測試性。DI 的出現源於面向對象設計中的依賴倒置原則(Dependency Inversion Principle, DIP)以及控制反轉(Inversion of Control, IoC)的理念。這個過程經歷了幾個階段的演進。
DI 的演進過程
硬編碼依賴 (Tightly Coupled Code)
最早期,物件之間的依賴是直接硬編碼的。例如,一個PaymentProcessor
可能直接依賴於CreditCardPayment
類別:1
2
3
4
5
6
7
8
9public class PaymentProcessor
{
private CreditCardPayment _paymentMethod = new CreditCardPayment();
public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}這種硬編碼方式帶來很大的限制。每次要更改支付方式,都需要修改
PaymentProcessor
類別的程式碼,導致系統的擴展性和維護性變差。使用接口進行抽象 (Programming to an Interface)
為了實現鬆耦合,我們可以引入接口。接口定義了支付方法的抽象,並允許不同的支付方式去實現該接口,但仍然有個缺點:我們還是必須在類別內部實例化具體的支付方式。1
2
3
4
5
6
7
8
9public class PaymentProcessor
{
private IPaymentMethod _paymentMethod = new CreditCardPayment();
public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}依賴注入的引入 (Dependency Injection)
DI 將具體實例的創建移到外部,讓PaymentProcessor
類別不再直接實例化支付方式,而是由外部在初始化時提供具體的依賴,從而達到完全的鬆耦合設計。現在,只需改變外部傳入的依賴即可變更支付方式,而無需修改PaymentProcessor
類別。1
2
3
4
5
6
7
8
9
10
11
12
13
14public class PaymentProcessor
{
private readonly IPaymentMethod _paymentMethod;
public PaymentProcessor(IPaymentMethod paymentMethod)
{
_paymentMethod = paymentMethod;
}
public void CompletePurchase(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}使用 DI,程式碼變得更具彈性,因為可以在建構
PaymentProcessor
時提供不同的支付方式,例如CreditCardPayment
或PaypalPayment
。DI 容器和自動化依賴管理
在大型應用程式中,手動管理依賴注入可能變得複雜,因此 DI 容器應運而生。DI 容器可以自動解析和管理依賴關係,從而簡化了注入過程。框架如 ASP.NET Core 提供了內建的 DI 容器,允許透過配置的方式在整個應用中管理依賴。
DI 的優勢
- 減少耦合性:物件之間的依賴關係變得靈活,不再直接依賴具體實現,而是依賴於抽象接口。
- 提高可測試性:通過 DI,可以輕易替換依賴的具體實現,例如在單元測試中注入模擬 (mock) 物件。
- 增強可擴展性:增加新功能或替換某個依賴時,不需要更改使用這些依賴的類別。
- **控制反轉 (IoC)**:透過 DI,對象的依賴由容器管理,不再由物件自己控制,這是 IoC 原則的具體應用。
總結
DI 的引入是對系統設計演進過程中的一次重大改進,使系統的各個組件可以更加靈活地替換、測試和擴展。在應用 DI 模式時,建構一個高效的依賴管理系統(如 DI 容器)可以大幅簡化代碼,減少耦合,並提升整體代碼品質。
當然可以,我們來看一個具體的範例,從最初的硬編碼依賴,到使用接口,再到依賴注入的改進,每一步都會展示程式碼如何演進,以及為什麼每個演進步驟更有利於系統的設計。
1. 硬編碼依賴 (Tightly Coupled Code)
在最早期的設計中,我們會直接在類別中實例化依賴對象,比如說在 PaymentProcessor
中直接依賴 CreditCardPayment
類別:
1 | public class CreditCardPayment |
問題
- 如果未來需要支持其他支付方式(例如 PayPal 或銀行轉帳),我們需要改動
PaymentProcessor
類別,新增或替換具體的支付類別。 - 系統的可擴展性差:每次需求變更都需要直接修改程式碼。
- 測試難度增加:無法模擬
CreditCardPayment
來進行單元測試。
2. 引入接口 (Programming to an Interface)
我們可以將支付方式抽象成一個接口 IPaymentMethod
,讓不同支付方式各自實現這個接口。這樣,PaymentProcessor
類別可以依賴於 IPaymentMethod
而非具體的 CreditCardPayment
。
1 | public interface IPaymentMethod |
優點和問題
- 可擴展性提升:增加支付方式時,只需新增實現
IPaymentMethod
的類別,PaymentProcessor
不需要改動。 - 耦合性降低:
PaymentProcessor
不再依賴於具體的支付方式。 - 問題仍然存在:
PaymentProcessor
依然自己決定具體使用哪一個支付方式(仍然硬編碼了new CreditCardPayment()
),導致擴展性受限。如果需求更變,仍需修改PaymentProcessor
類別的程式碼。
3. 依賴注入 (Dependency Injection)
這一步,我們通過依賴注入來進一步改善設計,將 PaymentProcessor
的支付方式作為外部傳入的依賴,從而達到更高的靈活性。
1 | public interface IPaymentMethod |
現在,使用 PaymentProcessor
時,可以選擇傳入不同的支付方式:
1 | IPaymentMethod paymentMethod = new CreditCardPayment(); |
優點
- 完全鬆耦合:
PaymentProcessor
不再負責創建具體支付方式。它的依賴由外部提供,使得系統更具彈性。 - 便於測試:可以在測試中傳入一個模擬
IPaymentMethod
,來測試PaymentProcessor
的功能,而不需要實際的支付方式。 - 擴展性更高:增加或更改支付方式時,無需修改
PaymentProcessor
的內部實現。
DI 總結
依賴注入將類別的依賴關係從內部創建轉移到外部提供,使系統更易擴展和維護。在大型應用中,依賴注入可以使用 DI 容器自動管理依賴,進一步簡化設計流程。
當然,讓我們逐行分析 硬編碼依賴 (Tightly Coupled Code) 的語法和設計。這裡有兩個主要的類別:
CreditCardPayment
:代表一種支付方式。PaymentProcessor
:處理支付的主要類別。
在此設計中,PaymentProcessor
直接依賴於 CreditCardPayment
,因此在程式中是“硬編碼”的依賴關係。
1 | public class CreditCardPayment |
逐行解釋
**
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 | public class PaymentProcessor |
逐行解釋
**
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);
**:- 調用
_paymentMethod
的ProcessPayment
方法來完成支付。 - 這裡
_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 | public class Person |
呼叫建構子的過程
- 使用
new
關鍵字:new
關鍵字會創建Person
類別的新物件,並在該物件上呼叫Person
類別的建構子。 - 傳遞參數:
new Person("Alice", 30)
會將"Alice"
和30
傳入建構子,並將這些值分別賦予Name
和Age
屬性。 - 返回物件:建構子執行完後,
person
變數會指向新創建並初始化的Person
物件。
沒有參數的建構子
如果類別內沒有自訂建構子,C# 會自動提供一個無參數建構子:
1 | public class Person |
在這個例子中,Person
類別沒有建構子,所以當呼叫 new Person()
時,C# 提供了一個隱含的無參數建構子,並將 Name
的初始值設為 "Unknown"
。
接近了!在這段範例中,隱含建構子的寫法其實是這樣的:
1 | public class Person |
這個隱含建構子在實際情況中不需要我們手動寫出。只要類別內部沒有定義任何建構子,C# 編譯器就會自動生成這個無參數建構子。
在這種情況下,如果你希望 CreditCardPayment
類別的建構子可以接受參數(如 cardNo
、CSV
、expYYMM
),那麼建構子的定義需要匹配這些參數,並在初始化時將它們賦值給類別中的私有欄位。你可以這樣設置建構子:
1 | public class CreditCardPayment |
這樣,當你使用 new CreditCardPayment(cardNo, CSV, expYYMM)
時,就會調用這個帶參數的建構子,並將 cardNo
、CSV
和 expYYMM
的值分別賦予對應的欄位 _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 | public class PaymentProcessor |
這樣,你只需要在創建 PaymentProcessor
的時候決定使用哪一種支付方式,而不需要修改 PaymentProcessor
的內部代碼。這種方法更符合開放-封閉原則(Open/Closed Principle),即對擴展開放,對修改封閉。
依賴注入(Dependency Injection,DI)的使用確實是設計模式演進過程的一部分,目的是解決早期設計中硬編碼依賴所帶來的問題。以下是一些要點來總結這個演變過程:
演進過程總結
硬編碼依賴 (Tightly Coupled Code):
- 在這個階段,類別之間有強烈的耦合,每個類別直接創建其依賴對象,這使得代碼難以測試和維護。當需要改變或擴展功能時,必須直接修改代碼。
引入接口 (Programming to an Interface):
- 當引入接口時,類別開始依賴於抽象而不是具體實現。這樣做可以使代碼更靈活,但仍然需要在類別內部進行修改以更改依賴。這樣的設計雖然比硬編碼依賴有所改善,但在需要頻繁修改的場景中仍然不夠靈活。
依賴注入 (Dependency Injection):
- 這個階段的主要目的是減少類別之間的耦合。通過將依賴作為參數傳遞給類別,開發者可以輕鬆地在不改變類別內部代碼的情況下更改其行為。這樣的設計符合開放-封閉原則(OCP),即對擴展開放,對修改封閉。這意味著你可以新增新的支付方式(或其他功能)而不需要改變已經存在的類別。
總結
DI 不僅提高了代碼的靈活性和可維護性,還使單元測試變得更加容易,因為你可以使用模擬對象來測試你的類別,而不需要依賴於具體的實現。
這樣的設計模式使得系統架構更加清晰、可擴展且易於維護,並且能夠更好地應對未來的需求變更。