C# Interface 是什麼?為什麼需要介面?從實際範例理解 Interface 與依賴注入(DI)

 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 = 150

GetShippingFee() 並不需要知道傳入的是一般配送或快速配送。

它只需要確定傳入的物件符合 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# 開發中經常出現的問題。

比較項目InterfaceAbstract 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,應該考慮:

  1. 是否存在多種合理的實作方式?
  2. 是否需要隔離資料庫、外部 API 等依賴?
  3. 是否有跨模組共用的穩定契約?
  4. 是否需要透過抽象讓上層程式與實作細節分離?
  5. 額外增加的抽象是否真的能改善維護性與測試性?

即使目前只有一種實作,也可能因為架構邊界或測試需求而適合使用 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 真正解決的是什麼問題?

回顧本文的運費計算範例:

我們有兩種不同的運費計算方式:

  • StandardShippingFeeCalculator
  • ExpressShippingFeeCalculator

兩者都遵守相同的契約:

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