Adapter Pattern trong C#: Khớp API Lạ Vào Code Của Bạn
Adapter pattern trong C# / .NET 10: bọc lệnh gọi SDK legacy vào interface của bạn — sync sang async, snake_case sang PascalCase, exception sang Result.
Mục lục
Bạn thừa kế một payment integration viết năm 2017 với SDK "Stripely" v1. SDK expose:
// Lạ - bạn không thể đổi.
public class StripelyClient
{
public StripelyResult charge_amount(int amount_in_cents,
string customer_id, string idempotency_key);
}
public class StripelyResult
{
public bool succeeded;
public string error_message;
public string transaction_id;
}
Ba thứ sai, không cái nào lỗi của SDK:
- Tên. Codebase của bạn PascalCase; SDK snake_case.
- Type. Domain của bạn dùng
decimalcho tiền; SDK dùngintcents. - Style. SDK đồng bộ và phát lỗi qua field boolean; phần còn lại
code của bạn là
async Task<Result<T>>.
Bạn không truyền StripelyClient cho consumer IPaymentProcessor
(cái dựng ở chương Factory
Method) được. Interface không khớp.
Adapter pattern là câu trả lời. Bọc type lạ vào class implement interface của bạn; làm conversion ở một chỗ; để phần còn lại codebase quên type lạ từng tồn tại. Đây là pattern ít được tôn vinh nhất trong sách GoF — mọi service thực tế có một cái cho mỗi SDK ngoài.
Adapter pattern giải quyết bài toán gì trong C#?
Pattern xứng chỗ khi bạn có hai interface lẽ ra khớp nhau nhưng không, và bạn không thể hoặc không nên sửa hai bên. Ba hình dạng cụ thể:
- SDK ngoài hình dạng sai. SDK Stripely phía trên. Bạn không sở hữu source; không đổi tên hay signature được.
- Code legacy không muốn động. Một class
Repositorytừ 2014 với method đồng bộ. Hiện đại hoá call site là project sáu tháng; bọc nó trong adapterIAsyncRepositorylà một buổi chiều. - Hai library API chồng nhưng không tương thích. Logging abstraction là ví dụ kinh điển: NLog, Serilog, Microsoft.Extensions.Logging — cùng ý đồ, hình dạng khác. Adapter thống nhất chúng sau abstraction codebase chọn.
Cái không phải bài toán Adapter: "API lạ ổn nhưng tôi muốn thêm retry" — đó là Decorator. "API lạ ổn nhưng tôi muốn giấu sau bề mặt đơn giản hơn" — đó là Facade. Adapter đặc biệt nói về interface mismatch.
Viết Adapter class cơ bản trong .NET 10 thế nào?
Dịch type lạ sang interface của bạn. Body mỗi method là "convert input → gọi lạ → convert output":
// Interface của bạn (cái codebase muốn thấy)
public sealed record PaymentResult(string Status, string? TransactionId, string? Error);
public interface IPaymentProcessor
{
Task<PaymentResult> PayAsync(decimal amount, string customerId, string idemKey,
CancellationToken ct);
}
// Adapter
public sealed class StripelyAdapter : IPaymentProcessor
{
private readonly StripelyClient _legacy;
public StripelyAdapter(StripelyClient legacy) => _legacy = legacy;
public Task<PaymentResult> PayAsync(decimal amount, string customerId, string idemKey,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var cents = (int)(amount * 100m); // chuyển kiểu
var raw = _legacy.charge_amount(cents, customerId, idemKey); // gọi sync
var result = raw.succeeded
? new PaymentResult("succeeded", raw.transaction_id, null)
: new PaymentResult("failed", null, raw.error_message);
return Task.FromResult(result); // bridge sync sang async
}
}
// Composition root
builder.Services.AddSingleton<StripelyClient>();
builder.Services.AddSingleton<IPaymentProcessor, StripelyAdapter>();
Ba phép biến đổi xảy ra trong một class:
- Type:
decimalsangintcents. - Naming:
charge_amountsangPayAsync;transaction_idsangTransactionId. - Style: call lạ sync bọc trong
Task.FromResult; kết quả boolean map sangPaymentResulttyped.
Bức tranh cấu trúc:
classDiagram
class IPaymentProcessor {
<<interface>>
+PayAsync(amount, customerId, idemKey) PaymentResult
}
class StripelyAdapter {
-StripelyClient legacy
+PayAsync(amount, customerId, idemKey) PaymentResult
}
class StripelyClient {
+charge_amount(cents, customer_id, idem_key) StripelyResult
}
IPaymentProcessor <|.. StripelyAdapter
StripelyAdapter o-- StripelyClient : forward sang
CheckoutController của bạn inject IPaymentProcessor và không bao
giờ thấy StripelyClient. Ngày Stripely ship v2, bạn thay
StripelyAdapter và controller không đổi.
Khi nào extension method là đủ?
Class adapter riêng là câu trả lời đúng đa số, nhưng không phải lựa chọn duy nhất. Ba cách nhẹ hơn:
- Extension method. Khi API lạ có hình dạng async/error đúng và bạn chỉ muốn call site dễ chịu hơn. Không implement được interface, không giữ state được.
- Lambda + DI registration. Một
Func<TInput, TOutput>đăng ký như adapter cho conversion tầm thường. Hữu ích cho mapping một-shot, không phải adapter có state. - Mapper class không interface. Đôi khi bạn chỉ cần dịch type
output và không có consumer interface. Một static
StripelyMapper.ToPaymentResult(StripelyResult)là đủ.
Quy tắc quyết định: có ai trong code muốn phụ thuộc vào target interface không? Có thì cần class adapter. Không thì extension method hay static mapper nhẹ hơn.
Khi nào Adapter pattern bắn trật?
Ba bẫy thực tế cần biết:
- Adapter giấu bug conversion lặng lẽ. Cast decimal-sang-cents
(int)(amount * 100m)chặt 0.999 thành 99 cents thay vì 100. Code adapter cần unit test kỷ luật nhất codebase vì interface làm nó trông tầm thường. - Adapt một chiều. Adapter xử
decimalsang cents khi đi vào nhưng quên convert ngược ở response. Consumer đọcresult.AmountCentstưởng là dollars. Convert đối xứng; không bỏ qua đường về. - Chuỗi adapter. Bọc Adapter mà bọc Adapter khác gần như luôn là dấu hiệu một trong hai không nên tồn tại. Ba lớp gián tiếp là ba chỗ debug một bug. Gập chuỗi lại khi có thể.
So sánh Adapter với Decorator, Facade, và Proxy thế nào?
Bốn wrapper này — Adapter, Decorator, Facade, Proxy — trông gần như giống hệt ở call site. Tách ra ở ý đồ:
| Pattern | Đổi interface? | Thêm hành vi? | Kiểm soát truy cập? |
|---|---|---|---|
| Adapter | Có (mismatch → match) | Không | Không |
| Decorator | Không | Có (log, retry, cache) | Không |
| Facade | Có (nhiều → một) | Không | Không |
| Proxy | Không | Không (hoặc forward trong suốt) | Có (lazy load, ACL, remote) |
Câu rõ ràng nhất: Adapter đổi hình dạng; Decorator đổi hành vi; Facade giấu phức tạp; Proxy kiểm soát truy cập. Phân vân thì hỏi từ nào trong bốn từ tả đúng nhất việc bạn sắp làm.
Một ví dụ thật trong C# / .NET 10 trông thế nào?
Adapter đầy đủ xử lý mọi phép biến đổi codebase thật cần — type, tên, async, lỗi, cancellation:
// SDK lạ (không đổi được) -----------------------------------------
public sealed class StripelyClient
{
public StripelyResult charge_amount(int amount_in_cents,
string customer_id, string idempotency_key)
{
// đồng bộ, block thread gọi
// throw StripelyTimeoutException khi network fail
// set succeeded=false cho lỗi logic
return /* ... */;
}
}
public sealed class StripelyResult
{
public bool succeeded;
public string error_message = "";
public string transaction_id = "";
}
public sealed class StripelyTimeoutException : Exception { }
// Interface của bạn -----------------------------------------------
public sealed record PaymentResult(string Status, string? TransactionId, string? Error);
public interface IPaymentProcessor
{
Task<PaymentResult> PayAsync(decimal amount, string customerId, string idemKey,
CancellationToken ct);
}
// Adapter ---------------------------------------------------------
public sealed class StripelyAdapter : IPaymentProcessor
{
private readonly StripelyClient _legacy;
private readonly ILogger<StripelyAdapter> _log;
public StripelyAdapter(StripelyClient legacy, ILogger<StripelyAdapter> log)
=> (_legacy, _log) = (legacy, log);
public Task<PaymentResult> PayAsync(decimal amount, string customerId, string idemKey,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var cents = checked((int)Math.Round(amount * 100m, MidpointRounding.AwayFromZero));
try
{
// SDK đồng bộ. Đẩy ra thread pool để không block thread request.
// Đây là một trong số ít lần Task.Run là chính đáng.
return Task.Run(() =>
{
var raw = _legacy.charge_amount(cents, customerId, idemKey);
return raw.succeeded
? new PaymentResult("succeeded", raw.transaction_id, null)
: new PaymentResult("failed", null, raw.error_message);
}, ct);
}
catch (StripelyTimeoutException ex)
{
_log.LogWarning(ex, "Stripely timeout cho customer {Customer}", customerId);
return Task.FromResult(new PaymentResult("timeout", null, ex.Message));
}
}
}
// Composition root
builder.Services.AddSingleton<StripelyClient>();
builder.Services.AddKeyedSingleton<IPaymentProcessor, StripelyAdapter>("stripely");
// Test
public sealed class FakeStripely : StripelyClient
{
public StripelyResult Result { get; set; } = new() { succeeded = true, transaction_id = "tx_test" };
public new StripelyResult charge_amount(int cents, string c, string k) => Result;
}
[Fact]
public async Task Adapter_converts_decimal_to_cents_and_succeeded_to_status()
{
var fake = new FakeStripely();
var sut = new StripelyAdapter(fake, NullLogger<StripelyAdapter>.Instance);
var result = await sut.PayAsync(9.99m, "cust_1", "idem_1", default);
Assert.Equal("succeeded", result.Status);
Assert.Equal("tx_test", result.TransactionId);
}
Adapter khoảng hai mươi dòng business code. Đa phần giá trị là cái
không có: phần còn lại codebase không bao giờ import
StripelyClient, không bao giờ thấy int cents, không bao giờ rẽ
nhánh trên succeeded. Ngày provider thanh toán mới ship, bạn viết
adapter anh em và đăng ký dưới key khác — xem Factory
Method cho hình dạng đăng ký.
Đọc tiếp gì trong series?
- Bài trước: Prototype — pattern Creational cuối; clone thay vì dựng.
- Bài kế: Bridge — khi có hai trục biến đổi và bạn không muốn class explosion.
- Tham chiếu chéo: Decorator — cùng hình dạng (class bọc class khác), khác ý đồ (thêm hành vi vs đổi interface).
- Tham chiếu chéo: Factory Method — adapter thường là concrete type factory trả về.
- Cây quyết định: Cách chọn design pattern phù hợp.
Một ghi chú thực dụng áp dụng cho mọi Adapter bạn sẽ viết: adapter là chỗ duy nhất mà từ vựng API lạ được phép vào codebase. Mọi phép dịch phải sống ở đó. Ngày một class không-adapter bắt đầu import type lạ, biên giới đã rò, và chi phí đổi API lạ vừa nhân lên. Coi adapter là cửa khẩu đóng kín.
Câu hỏi thường gặp
Adapter khác Decorator ở điểm gì?
Khi nào extension method là đủ thay vì Adapter class?
Result, hay batch sang per-call, bạn cần class — extension method không giữ state và không implement interface được.Adapter pattern liên hệ với Anti-Corruption Layer trong DDD thế nào?
Adapter nên throw hay trả Result type?
Result<T> hay OneOf<>, adapter bắt exception SDK và trả Result.Failure(...). Việc của adapter là làm hành vi lạ khớp với quy ước của bạn, gồm cả xử lý lỗi.