2026-06-17 C# 接口與抽象類要怎麼練習這種思考呢?

2026-06-17 C# 接口與抽象類要怎麼練習這種思考呢?

我:
關於 C# 接口與抽象類 妳舉的例子什麼 animal 及 dog bird 我可以理解,
但是實務上如何應用我就無法分析, 要怎麼練習這種思考呢?

GPT:
理解如何將接口和抽象類應用到實際情境中確實可以加深你對它們的掌握。我們可以用更接近商業邏輯的範例來幫助你了解。比方說,你可以想像一個「支付系統」的場景,包含不同的支付方式,例如信用卡、PayPal 等。每種支付方式有相似的操作,但細節不同。以下是一個練習思考的方向:

1. 設計接口來描述共同操作

假設每種支付方式都會有「付款」和「退款」的操作。你可以定義一個 IPaymentMethod 接口,包含 ProcessPaymentProcessRefund 方法,這些是每個支付方式的基本操作。

1
2
3
4
5
public interface IPaymentMethod
{
void ProcessPayment(decimal amount);
void ProcessRefund(decimal amount);
}

2. 建立具體的支付類別

針對不同的支付方式(例如,信用卡和 PayPal),你可以實現 IPaymentMethod 接口並定義它們自己的付款和退款邏輯:

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
public class CreditCardPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
// 信用卡付款的具體邏輯
}

public void ProcessRefund(decimal amount)
{
Console.WriteLine($"Processing credit card refund of {amount}");
// 信用卡退款的具體邏輯
}
}

public class PayPalPayment : IPaymentMethod
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount}");
// PayPal 付款的具體邏輯
}

public void ProcessRefund(decimal amount)
{
Console.WriteLine($"Processing PayPal refund of {amount}");
// PayPal 退款的具體邏輯
}
}

3. 應用場景

假設你有一個網路商店系統,當用戶選擇支付方式時,你的系統就會根據選擇的支付方式來執行相應的付款或退款方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PaymentProcessor
{
private IPaymentMethod _paymentMethod;

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

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

public void RefundPurchase(decimal amount)
{
_paymentMethod.ProcessRefund(amount);
}
}

思考練習

  1. 思考有哪些共同特性:這可以幫助你決定哪些應該放在接口中,哪些放在具體類別中。
  2. 考慮不同實現細節:透過這些練習,你可以模擬支付方式之間的不同點,如不同的費用計算方式,讓你更理解如何在接口中設計共同特性並在具體類別中執行細節。

這樣的練習可以幫助你逐步習慣在設計時如何運用接口和抽象類來實現系統的彈性和可擴展性。

我:
但妳沒有說到抽象類 , 要不妳設計一個習題讓我思考 接口與抽象類

GPT:
沒錯,將 GetSalary() 放在 IEmployee 接口中,主要是因為「每個員工類別都必須實現這個方法」。接口的作用就是為不同類型的員工(例如,正式員工、約聘員工)定義一組共同的操作契約,確保所有員工類別都會實現 GetSalary() 這個方法來計算薪資。

這樣設計接口的原因主要有幾個:

  1. 統一行為:接口 IEmployee 保證所有員工類別都有 GetSalary() 的實現,這樣在處理員工薪資時,不論員工類型,系統都可以調用 GetSalary() 而不必知道具體類別。

  2. 靈活擴展:透過接口,你可以輕鬆擴展新員工類型(例如,實習生)。新類別只要實現 IEmployee,就能確保新類別具備 GetSalary() 方法的實現,且與系統其他部分兼容。

  3. 解耦性:接口將「應用程序需要什麼方法」和「具體如何實現這些方法」分離。這樣的解耦設計讓不同類型的員工可以各自實現自己的薪資計算邏輯,而不會影響整體系統。

所以,將 GetSalary() 放在接口中是為了定義一個「必須存在的」行為,以便所有員工類型都符合此統一標準。而抽象類(如 EmployeeBase)則可以用來進一步共享一些具體的基礎功能或屬性,但仍可將 GetSalary() 的實現交由具體子類別來完成。

我:
所以說如果, 我設計沒有接口, 但只有抽象會發生什麼事呢?

GPT:
如果只使用抽象類而不使用接口,仍然能夠達到類似的效果,但會在靈活性和擴展性上有所限制。讓我們看看會發生什麼情況:

1. 少了靈活的契約

使用接口可以明確定義一組必須實現的操作,而無需限制具體的繼承架構。也就是說,接口不限制類別的繼承關係,因此類別可以實現多個接口,但只能繼承一個抽象類。如果只有抽象類,其他類別必須繼承這個抽象類來實現相同的操作,這在多繼承受限的情況下會讓設計更不靈活。

2. 擴展困難

如果將來你需要為不同員工類型引入更多操作(例如計算年終獎金),使用接口可以直接新增一個新的方法至接口,並讓所有實現該接口的類別繼續實現這個方法;但如果只用抽象類,添加新操作會強制每個子類重新實作並可能會帶來兼容性問題。

3. 缺少靈活的設計選擇

假設你想要在系統中有不同的員工類別來自不同來源(例如,有些是來自第三方系統的員工類別),這時接口更有彈性。因為接口只是行為的契約,不關心具體類別是如何組成的,第三方類別可以輕鬆地實現該接口而被系統接受。如果用抽象類,第三方類別必須繼承特定的基類才可兼容,這會增加不必要的依賴。

例子:缺少接口的設計

假設你在系統中只有 EmployeeBase 抽象類,而不使用 IEmployee 接口。這樣一來:

  1. **所有員工類別必須繼承 EmployeeBase**:這限制了其他類別的設計。例如,如果你想要引入來自外部的其他員工類別,但不希望它們依賴 EmployeeBase,則會出現問題。
  2. 擴展性受限:無法輕易為系統中所有員工類別強制實現特定方法,也無法輕鬆替換或添加不同的行為。這會在大型系統中造成擴展和維護困難。

總結

接口主要用來提供一組契約,讓各類別可以根據自己的需求靈活實現。而抽象類則是提供共通的基礎行為或屬性,並可將具體方法留給子類實現。因此,在設計中同時使用接口與抽象類,能夠達到最大靈活性和良好的擴展性。

我:
擴展困難
如果將來你需要為不同員工類型引入更多操作(例如計算年終獎金),使用接口可以直接新增一個新的方法至接口,並讓所有實現該接口的類別繼續實現這個方法;但如果只用抽象類,添加新操作會強制每個子類重新實作並可能會帶來兼容性問題。
這個妳可以具體解釋嗎?

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

情境假設

假設我們的系統中已經有不同的員工類別(例如 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())讓你可以選擇哪些類別需要實現,而不強制所有子類別進行修改,這樣的設計更有彈性,也不會影響系統中不需要此功能的類別。而僅使用抽象類的設計,會迫使所有繼承的類別都去實現這些功能,增加了不必要的代碼負擔和維護成本。

我:
IOvertimeCalculable 中文是什麼?

GPT:
IOvertimeCalculable 的中文可以翻譯為「可計算加班」。在這個上下文中,它表示的是一個接口名稱,意指實現這個接口的類別能夠進行「加班費計算」。

我:
所以設計接口是為了考慮擴展性?

GPT:
是的,設計接口主要是為了提高系統的擴展性靈活性。接口允許我們定義一組行為契約,讓不同類別在實現時只需關注它們需要的功能,避免不必要的依賴。

為什麼接口提高擴展性?

  1. 靈活增加新功能:接口可以讓不同類別按需實現所需的功能,這樣可以輕鬆地引入新功能而不破壞現有代碼。每個類別可以根據需要選擇實現不同的接口,從而使系統具有更高的擴展能力。

  2. 解耦代碼:接口將方法定義和具體實現分離,系統只需知道接口中的方法,而不需了解具體實現,這樣可以讓不同部分的代碼更獨立,減少相互影響。

  3. 支持多態性:接口允許類別提供不同的實現,並在運行時進行不同的操作。這種多態性讓你可以更輕鬆地在系統中使用不同的類別而不需要修改邏輯。

接口設計是一種面向擴展的設計方式,讓系統能夠靈活應對需求變更和增長。