首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 設計模式深度解析:適配器模式 (Adapter Pattern) 完整實作指南

     編輯:關於C#

前言

適配器模式 是結構型設計模式 中最實用、應用最廣泛的模式之一。它的核心目的是:**將一個類別的介面轉換成客戶端期望的另一個介面,使原本介面不相容的類別能夠合作**。就像旅行轉接頭讓美規插頭能在歐洲插座使用,適配器在軟體架構中扮演著關鍵的橋樑角色。

模式動機:現實世界的類比

  • 電源轉接頭:台灣 110V 電器 → 220V 歐洲插座 → 需要轉壓器 (適配器)
  • 記憶卡讀卡機:MicroSD 卡 → USB 介面電腦 → 需要讀卡機 (適配器)
  • 資料庫驅動程式:統一的 IDbConnection 介面 → 不同資料庫 (SQL Server, Oracle, PostgreSQL) → ADO.NET 提供者模式即適配器應用

兩種實作形式

類別適配器 (Class Adapter) — 繼承方式

透過多重繼承 (C# 不支援類別多重繼承,故目標必須為介面) 實現:


// 目標介面:客戶端期望的介面
public interface ITarget
{
    void Request();
}

// 來源類別:現有、介面不相容的類別
public class Adaptee
{
    public void SpecificRequest()
    {
        Console.WriteLine("Adaptee.SpecificRequest() 被呼叫");
    }
}

// 適配器:繼承 Adaptee,實作 ITarget
public class ClassAdapter : Adaptee, ITarget
{
    public void Request()
    {
        // 將 Request 轉發為 SpecificRequest
        this.SpecificRequest(); // 或 base.SpecificRequest()
    }
}

// 客戶端使用
ITarget target = new ClassAdapter();
target.Request(); // 輸出: Adaptee.SpecificRequest() 被呼叫

物件適配器 (Object Adapter) — 組合/委派方式 (推薦)

透過持有 Adaptee 實例並委派呼叫,符合「組合優於繼承」原則:


// 目標介面 (也可以是抽象類別)
public interface ITarget
{
    void Request();
}

// 來源類別
public class Adaptee
{
    public void SpecificRequest()
    {
        Console.WriteLine("Adaptee.SpecificRequest() 被呼叫");
    }
}

// 適配器:包裝 Adaptee 實例
public class ObjectAdapter : ITarget
{
    private readonly Adaptee _adaptee;

    public ObjectAdapter(Adaptee adaptee)
    {
        _adaptee = adaptee ?? throw new ArgumentNullException(nameof(adaptee));
    }

    public void Request()
    {
        // 可在轉發前後加入額外邏輯 (記錄、轉換、驗證等)
        Console.WriteLine("[Adapter] 正在轉換請求...");
        _adaptee.SpecificRequest();
        Console.WriteLine("[Adapter] 請求轉換完成");
    }
}

// 客戶端使用
Adaptee adaptee = new Adaptee();
ITarget target = new ObjectAdapter(adaptee);
target.Request();

兩種形式比較

特性類別適配器物件適配器
實現方式繼承 (多重繼承)組合/委派
C# 適用性目標必須為介面目標可為介面或類別
靈活度靜態綁定,編譯期決定動態綁定,執行期可切換 Adaptee
覆寫行為可覆寫 Adaptee 方法難以覆寫 Adaptee 方法 (需子類別化)
符合原則違反「組合優於繼承」符合「組合優於繼承」
推薦度低 (受限於 C# 單一繼承)高 (通用、彈性)

實戰案例:第三方支付 API 整合

情境

系統原本整合「綠界支付」,現在需新增「藍新支付」。兩家 API 介面完全不同,但業務邏輯 (訂單服務) 希望使用統一介面處理付款。

統一介面 (目標)


public interface IPaymentGateway
{
    /// 建立付款交易,回傳導向 URL 或表單 HTML
    PaymentResult CreatePayment(PaymentRequest request);
    
    /// 驗證回調通知
    bool VerifyCallback(IDictionary parameters);
    
    /// 查詢交易狀態
    TransactionStatus QueryStatus(string merchantTradeNo);
}

public record PaymentRequest(string MerchantTradeNo, decimal Amount, string Description, string ReturnUrl);
public record PaymentResult(bool Success, string RedirectUrl, string FormHtml, string ErrorMessage);
public enum TransactionStatus { Pending, Paid, Failed, Cancelled, Expired }

綠界支付適配器 (來源 A)


// 第三方 SDK 介面 (模擬)
public class EcpaySdk
{
    public string GenerateCheckMacValue(IDictionary parameters) => "MAC_VALUE";
    public string BuildFormHtml(string actionUrl, IDictionary fields) => "
...
"; public bool VerifyMacValue(IDictionary parameters, string macValue) => true; public string QueryTradeInfo(string tradeNo) => "Paid"; } public class EcpayAdapter : IPaymentGateway { private readonly EcpaySdk _sdk; private readonly string _merchantId; private readonly string _hashKey; private readonly string _hashIV; public EcpayAdapter(EcpaySdk sdk, string merchantId, string hashKey, string hashIV) { _sdk = sdk; _merchantId = merchantId; _hashKey = hashKey; _hashIV = hashIV; } public PaymentResult CreatePayment(PaymentRequest request) { // 1. 組綠界專用參數 var parameters = new Dictionary { ["MerchantID"] = _merchantId, ["MerchantTradeNo"] = request.MerchantTradeNo, ["TotalAmount"] = ((int)request.Amount).ToString(), ["TradeDesc"] = request.Description, ["ReturnURL"] = request.ReturnUrl, // ... 其他必要參數 }; // 2. 計算檢查碼 string checkMac = _sdk.GenerateCheckMacValue(parameters); parameters["CheckMacValue"] = checkMac; // 3. 產生表單 HTML string formHtml = _sdk.BuildFormHtml("https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5", parameters); return new PaymentResult(true, null, formHtml, null); } public bool VerifyCallback(IDictionary parameters) { string receivedMac = parameters["CheckMacValue"]; parameters.Remove("CheckMacValue"); string calculatedMac = _sdk.GenerateCheckMacValue(parameters); return _sdk.VerifyMacValue(parameters, receivedMac); } public TransactionStatus QueryStatus(string merchantTradeNo) { string result = _sdk.QueryTradeInfo(merchantTradeNo); return result switch { "Paid" => TransactionStatus.Paid, "Pending" => TransactionStatus.Pending, _ => TransactionStatus.Failed }; } }

藍新支付適配器 (來源 B)


// 第三方 SDK 介面 (模擬,完全不同架構)
public class NewebpaySdk
{
    public string Encrypt(string data) => "ENCRYPTED_DATA";
    public string Decrypt(string data) => "DECRYPTED_DATA";
    public string GenerateMerchantOrderNo() => Guid.NewGuid().ToString("N")[..20];
}

public class NewebpayAdapter : IPaymentGateway
{
    private readonly NewebpaySdk _sdk;
    private readonly string _merchantId;

    public NewebpayAdapter(NewebpaySdk sdk, string merchantId)
    {
        _sdk = sdk;
        _merchantId = merchantId;
    }

    public PaymentResult CreatePayment(PaymentRequest request)
    {
        // 藍新使用 AES 加密傳輸
        var tradeInfo = new
        {
            MerchantID = _merchantId,
            MerchantOrderNo = request.MerchantTradeNo,
            Amt = (int)request.Amount,
            ItemDesc = request.Description,
            ReturnURL = request.ReturnUrl,
            TimeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
        };
        
        string json = System.Text.Json.JsonSerializer.Serialize(tradeInfo);
        string encrypted = _sdk.Encrypt(json);
        
        // 產生自動送出的表單
        string formHtml = $@"
            
"; return new PaymentResult(true, null, formHtml, null); } public bool VerifyCallback(IDictionary parameters) { string encrypted = parameters["TradeInfo"]; string decrypted = _sdk.Decrypt(encrypted); // 解析 JSON 驗證... return true; // 簡化 } public TransactionStatus QueryStatus(string merchantTradeNo) { // 藍新查詢 API 不同... return TransactionStatus.Paid; } }

客戶端:訂單服務完全不需知道具體支付商


public class OrderService
{
    private readonly IPaymentGateway _gateway;

    public OrderService(IPaymentGateway gateway)
    {
        _gateway = gateway;
    }

    public PaymentResult Checkout(Order order)
    {
        var request = new PaymentRequest(
            order.MerchantTradeNo,
            order.TotalAmount,
            $"訂單 {order.Id} - {order.Items.Count} 項商品",
            "https://myshop.com/payment/return"
        );
        
        return _gateway.CreatePayment(request);
    }
}

// DI 容器註冊 (ASP.NET Core 範例)
services.AddScoped(sp => 
{
    string provider = Configuration["Payment:Provider"]; // "Ecpay" 或 "Newebpay"
    return provider switch
    {
        "Ecpay" => new EcpayAdapter(new EcpaySdk(), "...", "...", "..."),
        "Newebpay" => new NewebpayAdapter(new NewebpaySdk(), "..."),
        _ => throw new NotSupportedException($"不支援的支付商: {provider}")
    };
});

進階變體:雙向適配器

當需要雙向轉換時 (如雙向通訊協定):


public interface ITwoWayTarget
{
    void Send(string message);
    event Action OnReceived;
}

public interface ITwoWayAdaptee
{
    void Transmit(byte[] data);
    event Action OnDataReceived;
}

public class TwoWayAdapter : ITwoWayTarget
{
    private readonly ITwoWayAdaptee _adaptee;

    public TwoWayAdapter(ITwoWayAdaptee adaptee)
    {
        _adaptee = adaptee;
        _adaptee.OnDataReceived += data => OnReceived?.Invoke(Encoding.UTF8.GetString(data));
    }

    public event Action OnReceived;

    public void Send(string message)
    {
        _adaptee.Transmit(Encoding.UTF8.GetBytes(message));
    }
}

.NET 生態系中的適配器應用

場景.NET 實作說明
資料庫存取DbProviderFactory, IDbConnectionADO.NET 提供者模式標準適配器
記錄框架ILogger, ILoggerProviderMicrosoft.Extensions.Logging 適配 Serilog, NLog, log4net
設定系統IConfiguration, ConfigurationProvider適配 JSON, XML, 環境變數, Azure Key Vault 等來源
HTTP 用戶端HttpClient, IHttpClientFactory適配不同訊息處理器
快取IDistributedCache適配 Redis, SQL Server, Memory Cache
序列化JsonSerializer, XmlSerializer適配不同格式

最佳實務與反模式

✅ 最佳實務

  • 介面隔離:目標介面應盡量精簡,只暴露客戶端需要的功能
  • 單一職責:適配器只負責介面轉換,不包含業務邏輯
  • 建構子注入:Adaptee 實例透過建構子注入,方便測試與替換
  • 不可變性:適配器建立後不應變更內部 Adaptee 參考
  • 錯誤轉換:將 Adaptee 的例外轉換為目標介面定義的例外類型

❌ 反模式

  • 過度適配:為了「以防萬一」建立適配器,實際只有一個實作 (YAGNI 原則)
  • 邏輯外洩:適配器包含業務規則、驗證邏輯,導致難以維護
  • 雙向耦合:適配器引用客戶端類別,形成循環依賴
  • 繼承濫用:在 C# 中強行使用類別適配器,限制擴充性

測試策略


// 適配器單元測試:Mock Adaptee,驗證轉換邏輯
[Test]
public void EcpayAdapter_CreatePayment_ShouldGenerateCorrectForm()
{
    // Arrange
    var mockSdk = new Mock();
    mockSdk.Setup(s => s.GenerateCheckMacValue(It.IsAny>()))
           .Returns("MOCK_MAC");
    mockSdk.Setup(s => s.BuildFormHtml(It.IsAny(), It.IsAny>()))
           .Returns("
MOCK_FORM
"); var adapter = new EcpayAdapter(mockSdk.Object, "MERCHANT", "KEY", "IV"); var request = new PaymentRequest("ORDER123", 1000, "Test", "https://return"); // Act var result = adapter.CreatePayment(request); // Assert Assert.IsTrue(result.Success); Assert.AreEqual("
MOCK_FORM
", result.FormHtml); mockSdk.Verify(s => s.GenerateCheckMacValue(It.IsAny>()), Times.Once); }

總結

適配器模式是解決「介面不相容」問題的標準方案。在 C# 中,**物件適配器 (組合/委派) 是首選**,符合 SOLID 原則且彈性最高。核心價值在於:

  1. 解耦:客戶端依賴抽象介面 (IPaymentGateway),不依賴具體實作 (Ecpay/Newebpay)
  2. 複用:讓現有、無法修改的類別 (第三方 SDK、Legacy Code) 在新架構中繼續發揮作用
  3. 擴充:新增支付商只需實作新適配器,完全不修改訂單服務 (開放封閉原則)

掌握適配器模式,能讓你在整合第三方服務、重構舊系統、建立插件架構時遊刃有餘。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved