適配器模式 是結構型設計模式 中最實用、應用最廣泛的模式之一。它的核心目的是:**將一個類別的介面轉換成客戶端期望的另一個介面,使原本介面不相容的類別能夠合作**。就像旅行轉接頭讓美規插頭能在歐洲插座使用,適配器在軟體架構中扮演著關鍵的橋樑角色。
透過多重繼承 (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() 被呼叫
透過持有 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 介面完全不同,但業務邏輯 (訂單服務) 希望使用統一介面處理付款。
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 }
// 第三方 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
};
}
}
// 第三方 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 實作 | 說明 |
|---|---|---|
| 資料庫存取 | DbProviderFactory, IDbConnection | ADO.NET 提供者模式標準適配器 |
| 記錄框架 | ILogger, ILoggerProvider | Microsoft.Extensions.Logging 適配 Serilog, NLog, log4net |
| 設定系統 | IConfiguration, ConfigurationProvider | 適配 JSON, XML, 環境變數, Azure Key Vault 等來源 |
| HTTP 用戶端 | HttpClient, IHttpClientFactory | 適配不同訊息處理器 |
| 快取 | IDistributedCache | 適配 Redis, SQL Server, Memory Cache |
| 序列化 | JsonSerializer, XmlSerializer | 適配不同格式 |
// 適配器單元測試: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("");
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("", result.FormHtml);
mockSdk.Verify(s => s.GenerateCheckMacValue(It.IsAny>()), Times.Once);
}
適配器模式是解決「介面不相容」問題的標準方案。在 C# 中,**物件適配器 (組合/委派) 是首選**,符合 SOLID 原則且彈性最高。核心價值在於:
掌握適配器模式,能讓你在整合第三方服務、重構舊系統、建立插件架構時遊刃有餘。