Cấu trúc Cơ bản 7 phút đọc

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
  1. Adapter pattern giải quyết bài toán gì trong C#?
  2. Viết Adapter class cơ bản trong .NET 10 thế nào?
  3. Khi nào extension method là đủ?
  4. Khi nào Adapter pattern bắn trật?
  5. So sánh Adapter với Decorator, Facade, và Proxy thế nào?
  6. Một ví dụ thật trong C# / .NET 10 trông thế nào?
  7. Đọc tiếp gì trong series?

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:

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ể:

  1. 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.
  2. Code legacy không muốn động. Một class Repository từ 2014 với method đồng bộ. Hiện đại hoá call site là project sáu tháng; bọc nó trong adapter IAsyncRepository là một buổi chiều.
  3. 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:

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:

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:

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?

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ì?
Adapter đổi interface của object có sẵn để dùng được ở chỗ mong đợi interface khác; hành vi giữ nguyên. Decorator giữ interface và thêm hành vi mới quanh cái có sẵn. Nếu bạn không truyền object cho code mình mà không có wrapper được, bạn cần Adapter. Nếu truyền được nhưng muốn thêm log hoặc retry, bạn cần Decorator.
Khi nào extension method là đủ thay vì Adapter class?
Extension method đủ khi API lạ có hình dạng đúng (cùng async pattern, cùng error model) và chỉ tên method hoặc một biến đổi argument nhỏ là sai. Khoảnh khắc bạn cần bridge sync sang async, exception sang 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?
Anti-Corruption Layer (ACL) là cùng pattern ở quy mô kiến trúc. Một số class Adapter (cộng translator cho khác biệt từ vựng) đặt ở biên giữa domain của bạn với hệ thống ngoài, để model ngoài không bao giờ rò vào. Bên trong ACL, mỗi class riêng lẻ là một Adapter giáo khoa.
Adapter nên throw hay trả Result type?
Khớp với code gọi. Nếu codebase dùng exception, adapter bắt exception SDK lạ và rethrow exception typed của bạn. Nếu codebase dùng 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.