2024-11-07 026_具體地說明「擴展困難」的情況

2024-11-06 025_在 ASP.NET Core 中實現 DDD(領域驅動設計)

當然,讓我更具體地說明「擴展困難」的情況,並解釋接口與抽象類在此方面的差異。

情境假設

假設我們的系統中已經有不同的員工類別(例如 PermanentEmployeeContractEmployee),每個類別都繼承自抽象類 EmployeeBase,並已經實現了基礎方法 GetSalary()。接著,公司希望在年末發放年終獎金,並需要計算每個員工的獎金。

情境一:如果只使用抽象類

假設你在 EmployeeBase 中添加了 CalculateBonus() 這個方法:

1
2
3
4
5
6
7
8
9
10
public abstract class EmployeeBase
{
public string Name { get; set; }
public int EmployeeID { get; set; }

public abstract decimal GetSalary();

// 新增方法:計算年終獎金
public abstract decimal CalculateBonus();
}

這樣一來,每個繼承 EmployeeBase 的子類別都必須實現 CalculateBonus() 方法,不然無法編譯。然而,可能有些員工類別不需要年終獎金,或獎金計算方式非常簡單,這樣一來,對於無需此功能的子類別也得實作,可能會造成額外的代碼負擔。

可能的兼容性問題

由於所有繼承 EmployeeBase 的類別都必須重新實現這個新方法(即使他們可能不需要),所以這樣的設計會帶來以下幾個問題:

  1. 無法靈活應用:假設有些類型的員工根本不應該參與年終獎金計算(如臨時員工),但它們依然必須提供這個方法,甚至只能返回 0 或擺放空的實現。這樣的設計不夠靈活。

  2. 影響現有系統運行:如果你的應用程序中已經包含許多繼承 EmployeeBase 的現有類別,那麼現在修改基類會破壞所有子類的兼容性,使得它們必須都去實現 CalculateBonus() 方法,即使對部分類別而言它是無效的操作。這樣會增加系統維護成本。

情境二:使用接口來增加年終獎金方法

如果你使用的是接口,你可以創建一個新接口 IBonusCalculable,其中包含 CalculateBonus() 方法,然後只讓需要計算年終獎金的類別去實現它。例如:

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
public interface IBonusCalculable
{
decimal CalculateBonus();
}

public class PermanentEmployee : EmployeeBase, IBonusCalculable
{
public override decimal GetSalary()
{
// 實作薪資計算
}

public decimal CalculateBonus()
{
// 實作年終獎金計算
}
}

// 臨時員工類別無需實現 IBonusCalculable
public class ContractEmployee : EmployeeBase
{
public override decimal GetSalary()
{
// 實作薪資計算
}
}

這樣設計的優勢

  1. 更具靈活性:只需要年終獎金計算功能的類別才會去實現 IBonusCalculable,這樣臨時員工(或任何不需要年終獎金的類型)就不會被強迫去實現這個方法。

  2. 保持現有代碼的兼容性:現有的 EmployeeBase 及其子類別不需要被更改。如果你需要在其他類型的員工中加入年終獎金計算,只需讓它們實現 IBonusCalculable 接口,而不需要修改基類或其他類別的代碼。

  3. 接口的可組合性:在不同需求下,可以新增多個接口,比如 IBonusCalculableIOvertimeCalculable,讓類別按需實現所需的接口,而不必被迫承擔所有功能。

總結

使用接口來增加新功能(如 CalculateBonus())讓你可以選擇哪些類別需要實現,而不強制所有子類別進行修改,這樣的設計更有彈性,也不會影響系統中不需要此功能的類別。而僅使用抽象類的設計,會迫使所有繼承的類別都去實現這些功能,增加了不必要的代碼負擔和維護成本。