C# 開發過程中,經常會看到 interface 這個關鍵字
在 C# 開發過程中,經常會看到 interface 這個關鍵字,尤其是在 ASP.NET Core、Web API、企業系統或採用 Dependency Injection(DI)的專案中。
例如:
public interface IShippingFeeCalculator
{
decimal CalculateFee(decimal orderAmount);
}初次接觸時,可能會有幾個疑問:
- 為什麼要多建立一個 Interface?
- 直接使用 Class 不就可以完成相同功能?
- Interface 和 Abstract Class 有什麼不同?
- Interface 與 Dependency Injection(DI)有什麼關係?
- 是不是每一個 Service 都需要建立 Interface?
這篇文章將透過「電商運費計算系統」,從實際需求開始,逐步理解 Interface 的設計目的、實際應用方式,以及何時應該使用它。
一、什麼是 Interface?
Interface(介面)可以理解為一份功能契約(Contract)。
它主要用來定義「一個類別必須提供哪些功能」,而不需要指定「功能內部應該如何實作」。
舉個例子:
假設我們正在開發一個購物網站,需要計算商品訂單的運費。
網站目前提供兩種運送方式:
一般配送
- 訂單金額滿 NT$1,000,免運費。
- 未滿 NT$1,000,運費 NT$80。
快速配送
- 不論訂單金額,固定運費 NT$150。
雖然兩種配送方式的計算規則不同,但它們都具備相同的能力:
「根據訂單金額計算運費。」
這時候,就可以利用 Interface 定義共同契約。
/// <summary>
/// 運費計算介面。
/// 所有運費計算類別都必須提供 CalculateFee 方法。
/// </summary>
public interface IShippingFeeCalculator
{
/// <summary>
/// 根據訂單金額計算運費。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>計算後的運費</returns>
decimal CalculateFee(decimal orderAmount);
}這個 Interface 只規定:
接收訂單金額 orderAmount,回傳一個 decimal 型別的運費。
至於如何計算,由實作這個介面的類別自行決定。
可以簡單理解成:
Interface 定義能做什麼,Class 負責實際怎麼做。
二、如果不使用 Interface,可以嗎?
當然可以。
假設整個購物網站只有一種運送方式,直接建立一個 Class:
using System;
namespace ShippingInterfaceDemo.Services;
/// <summary>
/// 一般配送運費計算服務。
/// </summary>
public class StandardShippingFeeCalculator
{
/// <summary>
/// 計算一般配送運費。
/// 滿 1000 元免運費,否則收取 80 元。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>運費金額</returns>
public decimal CalculateFee(decimal orderAmount)
{
// 訂單金額不允許小於零。
if (orderAmount < 0m)
{
throw new ArgumentOutOfRangeException(
nameof(orderAmount),
"訂單金額不可小於零。");
}
// 訂單金額滿 1000 元免運費。
if (orderAmount >= 1000m)
{
return 0m;
}
// 其他情況收取 80 元運費。
return 80m;
}
}接著在程式中使用:
// 直接建立一般配送計算物件。
var calculator = new StandardShippingFeeCalculator();
// 計算訂單金額 500 元的運費。
decimal shippingFee = calculator.CalculateFee(500m);
// shippingFee = 80這完全沒有問題。
對於功能單純、需求穩定,而且沒有多種實作需求的程式而言,直接使用 Class 是合理的做法。
但如果未來新增快速配送呢?
原本使用 StandardShippingFeeCalculator 的程式,可能就需要調整成另一個類別。
如果有很多地方直接使用特定的運費計算類別,後續修改和替換就會比較麻煩。
這就是 Interface 可以協助改善的問題。
三、使用 Interface 改善程式設計
接下來將原本的運費計算功能改成使用 Interface。
步驟 1:建立運費計算介面
檔案:Services/IShippingFeeCalculator.cs
namespace ShippingInterfaceDemo.Services;
/// <summary>
/// 運費計算服務介面。
/// 定義各種配送方式共同的運費計算功能。
/// </summary>
public interface IShippingFeeCalculator
{
/// <summary>
/// 根據訂單金額計算運費。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>運費金額</returns>
decimal CalculateFee(decimal orderAmount);
}這個 Interface 不包含具體的運費計算邏輯。
它只定義所有運費計算類別都必須遵守的方法契約。
步驟 2:實作一般配送服務
檔案:Services/StandardShippingFeeCalculator.cs
using System;
namespace ShippingInterfaceDemo.Services;
/// <summary>
/// 一般配送運費計算服務。
/// 實作 IShippingFeeCalculator 介面。
/// </summary>
public class StandardShippingFeeCalculator : IShippingFeeCalculator
{
/// <summary>
/// 計算一般配送運費。
/// 訂單滿 1000 元免運費,否則收取 80 元。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>運費金額</returns>
public decimal CalculateFee(decimal orderAmount)
{
// 驗證訂單金額。
if (orderAmount < 0m)
{
throw new ArgumentOutOfRangeException(
nameof(orderAmount),
"訂單金額不可小於零。");
}
// 滿額免運費。
if (orderAmount >= 1000m)
{
return 0m;
}
// 未達免運門檻。
return 80m;
}
}注意這一行:
public class StandardShippingFeeCalculator : IShippingFeeCalculator代表:
StandardShippingFeeCalculator 必須實作 IShippingFeeCalculator 所要求的成員。
如果沒有完成必要的方法實作,編譯器就會顯示錯誤。
因此 Interface 不只是用來分類 Class,也能在編譯階段協助確認類別是否符合既定契約。
步驟 3:實作快速配送服務
檔案:Services/ExpressShippingFeeCalculator.cs
using System;
namespace ShippingInterfaceDemo.Services;
/// <summary>
/// 快速配送運費計算服務。
/// 實作與一般配送相同的介面。
/// </summary>
public class ExpressShippingFeeCalculator : IShippingFeeCalculator
{
/// <summary>
/// 計算快速配送運費。
/// 不論訂單金額,固定收取 150 元。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>運費金額</returns>
public decimal CalculateFee(decimal orderAmount)
{
// 訂單金額不可小於零。
if (orderAmount < 0m)
{
throw new ArgumentOutOfRangeException(
nameof(orderAmount),
"訂單金額不可小於零。");
}
// 快速配送固定運費。
return 150m;
}
}現在我們有兩個不同的 Class:
StandardShippingFeeCalculator
ExpressShippingFeeCalculator
它們的運費計算規則不同,但是都實作了:
IShippingFeeCalculator
所以都可以透過相同的 Interface 被使用。
這正是**多型(Polymorphism)**在實際開發中的常見應用。
四、Interface 最重要的特性:可以替換實作
假設有一個方法需要計算運費:
/// <summary>
/// 透過運費計算介面取得運費。
/// 不需要知道實際是哪一種配送方式。
/// </summary>
/// <param name="calculator">運費計算服務</param>
/// <param name="orderAmount">訂單金額</param>
/// <returns>運費金額</returns>
public static decimal GetShippingFee(
IShippingFeeCalculator calculator,
decimal orderAmount)
{
// 呼叫共同的運費計算契約。
return calculator.CalculateFee(orderAmount);
}現在可以將不同的實作傳入同一個方法:
// 建立一般配送服務。
IShippingFeeCalculator standard = new StandardShippingFeeCalculator();
// 建立快速配送服務。
IShippingFeeCalculator express = new ExpressShippingFeeCalculator();
// 同樣是 500 元的訂單。
decimal standardFee = GetShippingFee(standard, 500m);
decimal expressFee = GetShippingFee(express, 500m);
// standardFee = 80
// expressFee = 150GetShippingFee() 並不需要知道傳入的是一般配送或快速配送。
它只需要確定傳入的物件符合 IShippingFeeCalculator 契約,就能呼叫 CalculateFee()。
這種方式可以讓使用功能的程式與實際計算邏輯保持分離。
未來如果再新增「海外配送」,只要建立另一個實作相同 Interface 的 Class,就可以讓既有的呼叫程式使用新的計算方式。
前提是新增的實作仍然符合原本的介面契約與使用情境。
五、Interface 與 Dependency Injection(DI)的關係
在 ASP.NET Core 專案中,Interface 經常搭配 Dependency Injection(依賴注入)使用。
不過要先釐清一個觀念:
Interface 並不是 DI 的必要條件。
ASP.NET Core 的 DI 容器也可以直接註冊與注入具體 Class。
例如:
// 直接註冊具體類別,同樣可以使用 DI。
builder.Services.AddScoped<StandardShippingFeeCalculator>();Interface 的價值在於,讓程式依賴一份抽象契約,而不是直接依賴某一個具體類別。
步驟 1:在 Program.cs 註冊服務
以下以 ASP.NET Core Web API 專案為例。
檔案:Program.cs
using ShippingInterfaceDemo.Services;
var builder = WebApplication.CreateBuilder(args);
// 註冊 MVC / Web API Controller。
builder.Services.AddControllers();
// 註冊運費計算服務。
// 當程式要求 IShippingFeeCalculator 時,
// DI 容器會建立並提供 StandardShippingFeeCalculator。
builder.Services.AddScoped<IShippingFeeCalculator, StandardShippingFeeCalculator>();
var app = builder.Build();
// 啟用 Controller 路由。
app.MapControllers();
// 啟動應用程式。
app.Run();最重要的是:
builder.Services.AddScoped<IShippingFeeCalculator, StandardShippingFeeCalculator>();這段程式設定:
當系統需要 IShippingFeeCalculator 時,使用 StandardShippingFeeCalculator 作為實際實作。
其中 AddScoped 代表在一般 ASP.NET Core HTTP 要求的範圍內,共用同一個服務實例。
步驟 2:在 Controller 使用 Interface
檔案:Controllers/ShippingController.cs
using Microsoft.AspNetCore.Mvc;
using ShippingInterfaceDemo.Services;
namespace ShippingInterfaceDemo.Controllers;
/// <summary>
/// 運費計算 API。
/// </summary>
[ApiController]
[Route("api/shipping")]
public class ShippingController : ControllerBase
{
// 依賴 Interface,而不是特定的實作類別。
private readonly IShippingFeeCalculator _shippingFeeCalculator;
/// <summary>
/// 使用建構式注入運費計算服務。
/// </summary>
/// <param name="shippingFeeCalculator">運費計算服務</param>
public ShippingController(IShippingFeeCalculator shippingFeeCalculator)
{
_shippingFeeCalculator = shippingFeeCalculator;
}
/// <summary>
/// 根據訂單金額計算運費。
/// GET /api/shipping/fee?orderAmount=500
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>訂單金額及運費</returns>
[HttpGet("fee")]
public ActionResult<ShippingFeeResponse> GetShippingFee([FromQuery] decimal orderAmount)
{
// 驗證使用者輸入。
if (orderAmount < 0m)
{
return BadRequest("訂單金額不可小於零。");
}
// 呼叫介面提供的運費計算功能。
decimal shippingFee = _shippingFeeCalculator.CalculateFee(orderAmount);
// 建立 API 回傳結果。
var result = new ShippingFeeResponse
{
OrderAmount = orderAmount,
ShippingFee = shippingFee
};
return Ok(result);
}
}
/// <summary>
/// 運費計算結果模型。
/// </summary>
public class ShippingFeeResponse
{
/// <summary>
/// 原始訂單金額。
/// </summary>
public decimal OrderAmount { get; set; }
/// <summary>
/// 計算後的運費。
/// </summary>
public decimal ShippingFee { get; set; }
}這個 Controller 只依賴:
IShippingFeeCalculator
不需要知道實際的服務究竟是一般配送還是快速配送。
步驟 3:測試 API
啟動 Web API 後,可以透過以下網址測試:
GET /api/shipping/fee?orderAmount=500
使用一般配送時,預期結果:
{
"orderAmount": 500,
"shippingFee": 80
}如果訂單金額改成 1,200 元:
GET /api/shipping/fee?orderAmount=1200
結果:
{
"orderAmount": 1200,
"shippingFee": 0
}因為一般配送的免運門檻為 1,000 元。
步驟 4:改成快速配送
現在假設網站全面改採快速配送計費。
只需要把 Program.cs 的註冊修改成:
// 將預設運費計算服務改為快速配送。
builder.Services.AddScoped<IShippingFeeCalculator, ExpressShippingFeeCalculator>();Controller 不需要修改。
再次呼叫:
GET /api/shipping/fee?orderAmount=1200
預期結果:
{
"orderAmount": 1200,
"shippingFee": 150
}同一個 API、同一個 Controller、相同的方法契約,只需要更換 DI 註冊,就能使用不同的運費計算實作。
這就是 Interface 搭配 DI 的主要優勢。
需要注意: 以上範例是在系統設定階段決定預設的運費計算實作。如果實際業務要求使用者每次下單時都能選擇一般或快速配送,就需要進一步設計 Strategy Pattern、Factory,或使用其他方式選取對應實作。
單純把兩個實作都註冊成同一個 Interface,並不代表系統會根據配送選項自動選擇正確的服務。
六、使用 Interface 的實際優點
1. 降低程式之間的耦合
如果 Controller 直接依賴 StandardShippingFeeCalculator,就需要知道具體的實作類型。
使用 IShippingFeeCalculator 後,Controller 只需要知道功能契約。
這種依賴方式,可以減少上層程式與底層實作之間的直接關聯。
2. 更容易更換實作
例如運費計算未來可能有:
- 一般配送。
- 快速配送。
- 國際配送。
- 第三方物流報價。
只要這些計算方式能合理遵守相同的契約,就可以設計成不同的 Interface 實作。
不需要讓上層程式直接處理每種運送方式的內部細節。
3. 更容易進行單元測試
假設正式環境的運費計算服務,需要查詢資料庫、外部物流 API,甚至使用特定的商業規則。
測試時不一定希望連接這些真實資源。
如果程式依賴 Interface,就可以提供一個測試用實作:
namespace ShippingInterfaceDemo.Services;
/// <summary>
/// 測試用運費計算服務。
/// 不連接外部系統,固定回傳零元。
/// </summary>
public class FakeShippingFeeCalculator : IShippingFeeCalculator
{
/// <summary>
/// 回傳測試用運費。
/// </summary>
/// <param name="orderAmount">訂單金額</param>
/// <returns>固定零元</returns>
public decimal CalculateFee(decimal orderAmount)
{
return 0m;
}
}測試時就可以將 FakeShippingFeeCalculator 注入需要運費計算功能的類別。
這種方式有助於隔離外部依賴,讓測試更容易控制。
不過,Interface 並不是進行單元測試的唯一方式,具體類別也能透過適當設計進行測試。
4. 方便團隊分工
當專案規模較大時,可以先討論並定義 Interface 的功能契約。
例如:
IShippingFeeCalculator.CalculateFee(decimal orderAmount)
當契約確認後,不同開發人員就可以分別進行:
- API 與業務流程開發。
- 一般配送的計算實作。
- 快速配送的計算實作。
- 單元測試與整合測試。
只要契約穩定,各部分就能減少因實作細節變動而互相影響的機會。
七、Interface 和 Abstract Class 有什麼不同?
這也是 C# 開發中經常出現的問題。
| 比較項目 | Interface | Abstract Class |
|---|---|---|
| 主要用途 | 定義能力與契約 | 定義共用基底與行為 |
| 多重實作/繼承 | 一個 Class 可以實作多個 Interface | 一個 Class 只能直接繼承一個基底 Class |
| 成員實作 | 可以宣告成員,現代 C# 也支援預設實作 | 可以同時包含一般方法與抽象方法 |
| 實例欄位 | 不能宣告一般實例欄位 | 可以 |
| 建構子 | 不能定義實例建構子 | 可以 |
| 適合場景 | 不同類別需要遵守共同契約 | 多個類別需要共用狀態或實作邏輯 |
例如:
如果不同物流服務只是需要提供相同的運費計算能力,使用 Interface 通常比較適合。
但如果不同物流服務都有大量共用的驗證、設定、狀態與基礎計算邏輯,就可以考慮 Abstract Class。
而且兩者並不是互斥的:
一個 Class 可以繼承 Abstract Class,同時實作多個 Interface。
因此沒有必要把 Interface 和 Abstract Class 視為只能擇一的設計。
八、是不是每個 Class 都應該建立 Interface?
答案是:不需要。
這是很多 C# 專案在導入分層架構與 DI 後,容易出現的問題。
例如:
Services/
├── IUserService.cs
├── UserService.cs
├── IUserFormatterService.cs
├── UserFormatterService.cs
├── IUserHelperService.cs
├── UserHelperService.cs
├── IStringUtilityService.cs
└── StringUtilityService.cs如果每一個 Class 都機械式建立一個 Interface,卻沒有明確的抽象需求,就可能增加不必要的程式碼與維護成本。
例如單純字串格式化:
namespace ShippingInterfaceDemo.Utilities;
/// <summary>
/// 訂單文字格式化工具。
/// </summary>
public static class OrderTextFormatter
{
/// <summary>
/// 將訂單編號格式化為顯示文字。
/// </summary>
/// <param name="orderId">訂單編號</param>
/// <returns>格式化後的訂單文字</returns>
public static string FormatOrderNumber(int orderId)
{
return $"ORD-{orderId:D6}";
}
}像這類簡單、沒有外部依賴,也沒有替換需求的工具,不一定需要額外建立 Interface。
判斷是否建立 Interface,應該考慮:
- 是否存在多種合理的實作方式?
- 是否需要隔離資料庫、外部 API 等依賴?
- 是否有跨模組共用的穩定契約?
- 是否需要透過抽象讓上層程式與實作細節分離?
- 額外增加的抽象是否真的能改善維護性與測試性?
即使目前只有一種實作,也可能因為架構邊界或測試需求而適合使用 Interface。
反過來說,即使某個 Class 很複雜,也不代表一定要建立 Interface;它可能真正需要的是拆分職責。
九、Interface 與 SOLID 設計原則
理解 Interface 後,可以進一步認識 SOLID 裡的兩個重要原則。
1. Dependency Inversion Principle(DIP,依賴反轉原則)
核心觀念:
高層模組不應直接依賴低層模組的具體細節,兩者應該依賴抽象。
以本文的範例而言:
ShippingController 不需要依賴 StandardShippingFeeCalculator 的具體實作。
而是依賴:
IShippingFeeCalculator
實際使用哪一個計算服務,交由 DI 註冊決定。
這讓 API 層與運費計算實作之間的依賴關係更加清楚。
2. Interface Segregation Principle(ISP,介面隔離原則)
核心觀念:
不應強迫使用者依賴自己不需要的介面成員。
例如,不應該只為了方便,把以下功能全部塞進同一個龐大的介面:
- 使用者管理。
- 訂單查詢。
- 商品庫存。
- 金流付款。
- 運費計算。
- 報表匯出。
如果這些功能本身有清楚的職責邊界,通常應該拆分成不同服務或介面。
不過也不能為了拆分而拆分。
真正重要的是,介面的設計是否符合使用者的需求,以及服務的職責是否合理。
十、常見的 Interface 設計誤區
誤區一:使用 DI 就一定要建立 Interface
不正確。
ASP.NET Core DI 同樣可以注入具體 Class。
Interface 和 DI 是兩個不同概念。
DI 負責管理與提供依賴,Interface 負責定義抽象契約。
誤區二:Interface 可以讓程式執行得更快
一般而言,Interface 並不是效能最佳化工具。
使用 Interface 的主要目的,是改善程式結構、降低耦合,以及支援多型與替換實作。
實際執行效能仍然取決於程式邏輯、資料存取、編譯器與執行環境等因素。
誤區三:只要建立 Interface,程式就會容易維護
不一定。
如果一個 Interface 包含數十甚至數百個沒有直接關聯的方法,即使全部透過 DI 使用,仍然可能很難維護。
良好的 Interface 設計應該有清楚的責任邊界,而不是單純將 Class 的所有公開方法複製到 Interface。
誤區四:Interface 定義後就永遠不需要修改
Interface 是契約,因此修改它可能影響所有實作類別及使用端。
如果在既有 Interface 新增必要成員,原本實作它的類別可能需要配合修改。
所以 Interface 應該追求穩定、合理的抽象,而不是把目前想到的所有功能都加入。
誤區五:所有實作相同 Interface 的類別都可以無條件互換
技術上符合相同的方法簽章,不代表商業語意一定相同。
例如某種配送方式可能不支援部分地區,或需要額外的重量、地址等條件。
若原本的 Interface 無法合理表達這些限制,就應該重新檢視契約,而不是勉強讓所有配送方式遵守過度簡化的設計。
十一、實際開發時,應該如何選擇?
可以從專案規模及需求考量:
| 開發情境 | 建議方式 |
|---|---|
| 簡單文字處理 | 一般 Class 或 static 方法 |
| 單純且穩定的內部邏輯 | 直接使用 Class 即可 |
| Controller 依賴業務服務 | 視服務邊界考慮 Interface + DI |
| 多種物流或計算策略 | Interface + 不同實作 |
| 外部 API 或第三方服務整合 | 通常適合建立抽象契約 |
| 需要隔離外部依賴進行測試 | Interface 是常用選擇 |
| Service 職責過多 | 優先檢視責任拆分,再決定介面設計 |
設計時不應該只問:
「這個 Class 要不要建立 Interface?」
更值得問的是:
「這個功能有沒有需要與具體實作分離的理由?」
如果沒有,可能不需要多增加一層抽象。
如果有,那麼 Interface 就可能是合適的設計工具。
十二、總結:Interface 真正解決的是什麼問題?
回顧本文的運費計算範例:
我們有兩種不同的運費計算方式:
StandardShippingFeeCalculatorExpressShippingFeeCalculator
兩者都遵守相同的契約:
IShippingFeeCalculator
而 Controller 只需要知道:
「我可以要求這個服務計算運費。」
至於實際的運費規則,則交由不同的實作類別負責。
最後再透過 DI 決定使用哪一個實作。
整個概念可以歸納成三句話:
Interface:定義能做什麼。
Class:負責實際怎麼做。
Dependency Injection:負責提供要使用的實作。
Interface 並不是讓程式變得更複雜的工具,也不是每個 Class 都需要套用的標準格式。
它真正的價值,是讓程式的依賴關係與責任邊界更加清楚。
對於大型 C#、ASP.NET Core 或企業應用系統而言,理解這個觀念,比單純記住 Interface 的語法更加重要。
好的程式設計,不是使用越多 Interface 越好,而是在真正需要抽象的地方,建立恰當且穩定的契約。
文章關鍵字: C#、Interface、介面設計、ASP.NET Core、Dependency Injection、DI、SOLID、物件導向、OOP、軟體架構、Service Design