Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Design Patterns — #07

Adapter Pattern: Third-Party SDK'yı Kendi Interface'ine Uydurmak

Mert Özen 7 Kas 2026 14 dk 3 görüntülenme
Adapter Pattern: Third-Party SDK'yı Kendi Interface'ine Uydurmak

Adapter, değiştiremediğin bir class'ı client'ın beklediği interface'e uydurur; C#'ta neredeyse her zaman object adapter, yani composition ile. İki hayalî ödeme SDK'sını tek IPaymentGateway'e bağlamak, callback'i TaskCompletionSource ile Task'a çevirmek, hata kodlarını domain exception'larına eşlemek ve iki adapter'ı aynı contract test ile doğrulamak.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore kartla ödeme için bir ödeme kuruluşuyla anlaştı ve kuruluş bir SDK (Software Development Kit) gönderdi. Yazıdaki SDK'lar hayalî; gerçek bir ürünü temsil etmiyor, ama imzaları sahada sık gördüğüm şekilde: tutar kuruş cinsinden long, sonuç iki haneli bir string kodu. SDK'nın kaynağı bizde değil, yalnız public yüzünü kullanabiliyoruz:

namespace Vendor.CardSdk;

// Satıcının SDK'sı: kaynağı bizde değil, değiştiremiyoruz
public sealed class CardProcessorClient(string apiKey)
{
    private int _sequence = 4100;

    public ChargeResponse Charge(long amountInMinorUnits, string currencyCode, string cardToken)
    {
        ArgumentException.ThrowIfNullOrEmpty(apiKey);
        Console.WriteLine($"[card-sdk] charge {amountInMinorUnits} minor units {currencyCode}");
        return cardToken switch
        {
            "tok_insufficient" => new ChargeResponse("51", null),
            "tok_issuer_down" => new ChargeResponse("91", null),
            "tok_stolen" => new ChargeResponse("43", null),
            _ => new ChargeResponse("00", $"AUTH-{++_sequence}")
        };
    }
}

public sealed record ChargeResponse(string ResultCode, string? AuthorizationId);

Sipariş tarafını yazan arkadaş SDK'yı doğrudan OrderService'e enjekte etti:

using Vendor.CardSdk;

Order order = new("BK-1001", 129.90m);
OrderService service = new(new CardProcessorClient("sk_test_bookstore"));
Console.WriteLine(service.Pay(order, "tok_visa"));

public sealed record Order(string Number, decimal Total);

public sealed class OrderService(CardProcessorClient cardClient)
{
    public string Pay(Order order, string cardToken)
    {
        ChargeResponse response = cardClient.Charge((long)order.Total, "TRY", cardToken);
        return response.ResultCode == "00"
            ? $"Order {order.Number} paid: {order.Total} TRY"
            : $"Order {order.Number} failed: code {response.ResultCode}";
    }
}

Sipariş tutarı 129,90 TL. Kartından ne kadar çekildi ve uygulama müşteriye ne söyledi?

Adapter Pattern: Target, Adaptee ve Adapter

Çıktı:

[card-sdk] charge 129 minor units TRY
Order BK-1001 paid: 129.90 TRY

Kartından 1,29 TL çekildi, ekranda 129,90 TL ödendi yazıyor. (long)129.90m ondalık kısmı atıp 129 verdi; SDK ise bunu 129 kuruş olarak okudu. Derleyici bir şey demedi, çünkü tipler uyuşuyor; uyuşmayan şey iki tarafın para modeli. Asıl sorun bu satırın OrderService'in içinde olması: sipariş kodu artık kuruşun ne olduğunu, "00"'ın onay demek olduğunu ve SDK'nın hangi class'ı istediğini biliyor. İkinci bir ödeme kuruluşu eklendiğinde bu bilgiler sipariş koduna bir kez daha, farklı biçimde yazılacak.

GoF (Gang of Four: Erich Gamma, Richard Helm, Ralph Johnson ve John Vlissides'in 1994 tarihli Design Patterns kitabı) Adapter'ı structural pattern'lerin ilki olarak anlatır. Amacı bir class'ın interface'ini, client'ın beklediği başka bir interface'e çevirmek; böylece interface'leri uyumsuz olduğu için birlikte çalışamayan class'lar birlikte çalışabilir. Kitap pattern'in diğer adını da verir: Wrapper. Üç rol var. Target, client'ın konuştuğu interface; burada Bookstore'un kendi IPaymentGateway'i. Adaptee, uydurulacak mevcut class; burada değiştiremediğimiz CardProcessorClient. Adapter ise target'ı implement edip işi adaptee'ye devreden class.

Önce target'ı Bookstore'un diliyle yazalım. Para bir Money, ret ve kesinti iki ayrı exception, hiçbirinde SDK'dan tek bir kelime yok:

namespace Bookstore.Payments;

// Bookstore'un kendi sözleşmesi: client yalnız bunu tanır (target)
public interface IPaymentGateway
{
    Task<PaymentReceipt> ChargeAsync(PaymentRequest request, CancellationToken cancellationToken = default);
}

public sealed record Money(decimal Amount, string Currency);

public sealed record PaymentRequest(string OrderNumber, Money Amount, string PaymentToken);

public sealed record PaymentReceipt(string Provider, string TransactionId);

public sealed class PaymentDeclinedException(string orderNumber, DeclineReason reason)
    : Exception($"Payment for {orderNumber} was declined: {reason}.")
{
    public DeclineReason Reason { get; } = reason;
}

public sealed class PaymentProviderUnavailableException(string provider, string detail)
    : Exception($"Payment provider '{provider}' is unavailable ({detail}).");

public enum DeclineReason { InsufficientFunds, SuspectedFraud, Other }

Adapter bu sözleşme ile SDK arasındaki tek yer:

using Vendor.CardSdk;

namespace Bookstore.Payments;

// Adapter: IPaymentGateway'i implement eder, işi SDK'ya (adaptee) devreder
public sealed class CardGatewayAdapter(CardProcessorClient client) : IPaymentGateway
{
    public Task<PaymentReceipt> ChargeAsync(PaymentRequest request, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();

        // Bookstore yalnız iki ondalıklı para birimleriyle çalışıyor (TRY, EUR, USD)
        long minorUnits = decimal.ToInt64(decimal.Round(request.Amount.Amount * 100m, 0));
        ChargeResponse response = client.Charge(minorUnits, request.Amount.Currency, request.PaymentToken);

        return response.ResultCode switch
        {
            "00" => Task.FromResult(new PaymentReceipt("card", response.AuthorizationId!)),
            "51" => throw new PaymentDeclinedException(request.OrderNumber, DeclineReason.InsufficientFunds),
            "43" => throw new PaymentDeclinedException(request.OrderNumber, DeclineReason.SuspectedFraud),
            "91" => throw new PaymentProviderUnavailableException("card", "issuer unavailable"),
            // Bilinmeyen kodu yutmuyoruz: ret say ama kodu Data'da taşı
            string code => throw new PaymentDeclinedException(request.OrderNumber, DeclineReason.Other)
                { Data = { ["ResultCode"] = code } }
        };
    }
}

Kuruş dönüşümü, SDK'nın sonuç kodları ve hangi kodun ret, hangisinin kesinti olduğu bilgisi artık tek bir class'ta. decimal.Round ile decimal.ToInt64, açılıştaki cast'in sessizce kestiği kısmı doğru hesaplıyor; para birimlerinin ondalık hanesi ve yuvarlama kuralları Çözüm Desenleri serisinin "Para ve Yuvarlama" yazısında ayrıntılı. Bilinmeyen bir kod geldiğinde adapter onu onay saymıyor ve yutmuyor; ret olarak işaretleyip kodu exception'ın Data'sında taşıyor, böylece log'da görünüyor.

Class Adapter mı, Object Adapter mı?

GoF iki çeşit adapter tanımlar. Object adapter, yukarıdaki gibi adaptee'yi bir field'da tutar ve çağrıları ona iletir: composition. Class adapter ise multiple inheritance ile hem target'tan hem adaptee'den türer. C#'ta bir class'ın tek bir base class'ı olabildiği için class adapter ancak target bir interface ise ve adaptee türetmeye açıksa mümkün:

using Bookstore.Payments;
using Vendor.CardSdk;

// Class adapter: adaptee'den türe, target'ı implement et
public sealed class CardClassAdapter() : CardProcessorClient("sk_test_bookstore"), IPaymentGateway
{
    public Task<PaymentReceipt> ChargeAsync(PaymentRequest request, CancellationToken cancellationToken = default)
    {
        ChargeResponse response = Charge(decimal.ToInt64(request.Amount.Amount * 100m), request.Amount.Currency, request.PaymentToken);
        return Task.FromResult(new PaymentReceipt("card", response.AuthorizationId!));
    }
}
Program.cs(5,42): error CS0509: 'CardClassAdapter': cannot derive from sealed type 'CardProcessorClient'

SDK'yı yazan taraf class'ı sealed yapmış; çoğu SDK'da da öyle. Türetmeye açık olsaydı bile class adapter'ın iki bedeli olurdu. Birincisi, adapter adaptee'nin tüm public member'larını da miras alır: CardClassAdapter instance'ı üzerinden Charge'ı kuruşla çağırmak hâlâ mümkün olurdu ve adapter'ın gizlemek istediği model dışarı sızardı. İkincisi, adapter tek bir concrete class'a bağlanır; test'te sahte bir SDK client'ı vermek, constructor'a farklı bir API (Application Programming Interface) key'i geçmek ya da adaptee'nin bir subclass'ını kullanmak zorlaşır. GoF de object adapter'ın tek bir adapter ile adaptee'nin tüm subclass'larıyla çalışabildiğini söyler. C#'ta varsayılan cevap object adapter; class adapter'ı C++ döneminin bir kalıntısı olarak tanıman yeterli. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisindeki "Composition over Inheritance: Fragile Base Class, Delegation ve Davranışı Takmak" yazısı bu tercihin genel gerekçesini anlatıyor.

İkinci Sağlayıcı: Callback'li SDK'yı Task'a Uydurmak

Birkaç ay sonra bir dijital cüzdan entegrasyonu geliyor. Bu SDK tamamen farklı bir dünyadan: async/await'ten önceki tarzda callback ile sonuç veriyor, tutarı string olarak istiyor ve hatayı exception yerine bir hata koduyla bildiriyor:

namespace Vendor.WalletSdk;

// İkinci satıcının SDK'sı: callback tabanlı, tutarı string olarak istiyor
public sealed class WalletApiClient
{
    public void BeginPayment(WalletPaymentRequest request, Action<WalletPaymentResult> onCompleted)
    {
        Console.WriteLine($"[wallet-sdk] pay {request.Amount} {request.Currency}");
        ThreadPool.QueueUserWorkItem(_ =>
        {
            Thread.Sleep(50);
            WalletPaymentResult result = request.WalletToken switch
            {
                "wlt_low_balance" => new(false, null, "INSUFFICIENT_BALANCE"),
                "wlt_timeout" => new(false, null, "UPSTREAM_TIMEOUT"),
                _ => new(true, $"WP-{request.MerchantReference}", null)
            };
            onCompleted(result);
        });
    }
}

public sealed record WalletPaymentRequest(string MerchantReference, string Amount, string Currency, string WalletToken);

public sealed record WalletPaymentResult(bool Succeeded, string? PaymentId, string? ErrorCode);

Bookstore'un interface'i Task dönüyor. Callback'i task'a çevirmenin standart aracı TaskCompletionSource<T>: bir task yaratır ve onu ne zaman, hangi sonuçla tamamlayacağına sen karar verirsin.

using System.Globalization;
using Vendor.WalletSdk;

namespace Bookstore.Payments;

// Callback tabanlı SDK'yı Task dönen interface'e uyduran adapter
public sealed class WalletGatewayAdapter(WalletApiClient client) : IPaymentGateway
{
    public Task<PaymentReceipt> ChargeAsync(PaymentRequest request, CancellationToken cancellationToken = default)
    {
        // İptal edilmiş istek SDK'ya hiç gitmemeli
        cancellationToken.ThrowIfCancellationRequested();

        TaskCompletionSource<PaymentReceipt> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
        CancellationTokenRegistration registration = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));

        WalletPaymentRequest walletRequest = new(
            MerchantReference: request.OrderNumber,
            // SDK "129.90" bekliyor; kültürden bağımsız biçimlendirme şart
            Amount: request.Amount.Amount.ToString("F2", CultureInfo.InvariantCulture),
            Currency: request.Amount.Currency,
            WalletToken: request.PaymentToken);

        client.BeginPayment(walletRequest, result =>
        {
            registration.Dispose();
            if (result.Succeeded)
            {
                tcs.TrySetResult(new PaymentReceipt("wallet", result.PaymentId!));
                return;
            }

            tcs.TrySetException(result.ErrorCode switch
            {
                "INSUFFICIENT_BALANCE" => new PaymentDeclinedException(request.OrderNumber, DeclineReason.InsufficientFunds),
                "UPSTREAM_TIMEOUT" => new PaymentProviderUnavailableException("wallet", "upstream timeout"),
                _ => new PaymentDeclinedException(request.OrderNumber, DeclineReason.Other)
            });
        });

        return tcs.Task;
    }
}

Burada adapter'ın üç ayrı uyumsuzluğu tek yerde çözdüğünü görüyorsun. Callback ile Task arasındaki model farkı: RunContinuationsAsynchronously, await sonrasındaki kodun SDK'nın thread'inde, callback'in içinde çalışmasını önlüyor. Hata modeli farkı: SDK'nın string kodları Bookstore'un iki exception'ına çevriliyor. Ve veri biçimi farkı: tutar string'e kültürden bağımsız biçimde yazılıyor. Bu sonuncusu Türkiye'de çalışan bir sunucuda ciddi bir hata kaynağı:

using System.Globalization;

decimal amount = 129.90m;
CultureInfo.CurrentCulture = new CultureInfo("tr-TR");

Console.WriteLine(amount.ToString());
Console.WriteLine(amount.ToString("F2", CultureInfo.InvariantCulture));
129,90
129.90

Sunucunun kültürü tr-TR ise düz ToString(), SDK'ya "129,90" gönderir. SDK'nın bunu ret mi edeceği, 12990 mı yoksa 129 mu okuyacağı onun implementasyonuna kalmış. İki adapter'ı aynı client koduyla çalıştıralım:

using Bookstore.Payments;
using Vendor.CardSdk;
using Vendor.WalletSdk;

IPaymentGateway[] gateways =
[
    new CardGatewayAdapter(new CardProcessorClient("sk_test_bookstore")),
    new WalletGatewayAdapter(new WalletApiClient())
];

string[][] tokens = [["tok_visa", "tok_insufficient", "tok_issuer_down"], ["wlt_ada", "wlt_low_balance", "wlt_timeout"]];

for (int g = 0; g < gateways.Length; g++)
{
    foreach (string token in tokens[g])
    {
        PaymentRequest request = new("BK-1001", new Money(129.90m, "TRY"), token);
        try
        {
            PaymentReceipt receipt = await gateways[g].ChargeAsync(request);
            Console.WriteLine($"  paid via {receipt.Provider}: {receipt.TransactionId}");
        }
        catch (PaymentDeclinedException ex)
        {
            Console.WriteLine($"  declined: {ex.Reason}");
        }
        catch (PaymentProviderUnavailableException ex)
        {
            Console.WriteLine($"  unavailable: {ex.Message}");
        }
    }
}
[card-sdk] charge 12990 minor units TRY
  paid via card: AUTH-4101
[card-sdk] charge 12990 minor units TRY
  declined: InsufficientFunds
[card-sdk] charge 12990 minor units TRY
  unavailable: Payment provider 'card' is unavailable (issuer unavailable).
[wallet-sdk] pay 129.90 TRY
  paid via wallet: WP-BK-1001
[wallet-sdk] pay 129.90 TRY
  declined: InsufficientFunds
[wallet-sdk] pay 129.90 TRY
  unavailable: Payment provider 'wallet' is unavailable (upstream timeout).

Client döngüsü iki SDK'nın varlığından habersiz. İki sağlayıcı aynı üç durumu aynı biçimde raporluyor: onay, yetersiz bakiye nedeniyle ret ve sağlayıcı kesintisi. Hangi adapter'ın kullanılacağı kararı da client'ın değil, uygulamanın açılışındaki composition root'un işi; çalışma zamanında anahtara göre seçim yapmak bu serinin Strategy pattern yazısının konusu.

.NET'in İçindeki Adapter'lar

Adapter BCL (Base Class Library, .NET'in temel kütüphanesi) içinde o kadar yaygın ki çoğu zaman adını anmadan kullanırsın. En bilineni StreamReader: byte okuyan bir Stream'i, karakter ve satır okuyan TextReader sözleşmesine uydurur.

using System.Text;

// Byte okuyan Stream, satır okuyan bir TextReader'a uyduruluyor
using MemoryStream bytes = new(Encoding.UTF8.GetBytes("BK-1001;129.90\nBK-1002;54.00\n"));
using TextReader reader = new StreamReader(bytes, Encoding.UTF8);

while (reader.ReadLine() is { } line)
    Console.WriteLine(line.Split(';')[0]);
BK-1001
BK-1002

MemoryStream, FileStream ya da network stream'i fark etmez; StreamReader hepsini aynı ReadLine'a çevirir. Bu, object adapter'ın GoF'un söylediği avantajı: tek adapter, adaptee'nin tüm subclass'larıyla çalışıyor. Diğer örnekler:

  • Task.Factory.FromAsync: eski BeginXxx/EndXxx method çiftini (APM (Asynchronous Programming Model)) Task'a uydurur; C# Async serisinin Thread ve ThreadPool yazısı bu eski modelleri anlatıyor.
  • TaskCompletionSource<T>: callback ya da event tabanlı her API'yi task'a uydurmanın genel aracı; yukarıdaki cüzdan adapter'ının kalbi.
  • ChannelReader<T>.ReadAllAsync(): bir channel'ı IAsyncEnumerable<T> olarak sunar, böylece await foreach ile okunur.
  • PipeReader.AsStream() ve PipeReader.Create(stream): System.IO.Pipelines ile Stream dünyası arasında iki yönlü adapter.
  • Serilog gibi kütüphanelerin ILoggerProvider implementasyonları: kendi logging API'lerini ASP.NET Core'un ILogger'ına uydururlar.

Adapter ve Anti-Corruption Layer

Eric Evans, DDD (Domain-Driven Design, 2003) kitabında aynı fikrin daha büyüğünü anlatır: Anti-Corruption Layer. Kendi modeline sahip bir sistemle (eski bir ERP (Enterprise Resource Planning) sistemi, başka bir ekibin servisi, üçüncü parti bir platform) entegre olurken, onun kavramlarının senin modeline sızmasını önleyen bir çeviri katmanı. Evans bu katmanı facade, adapter ve translator'ların birleşimi olarak tarif eder.

Fark ölçekte. Adapter tek bir interface uyumsuzluğunu çözer: bir method imzası, bir hata modeli, bir veri biçimi. Anti-Corruption Layer ise iki modelin arasında durur: karşı tarafın "müşteri hesabı" ile senin "customer"ının aynı şey olmadığına, durum kodlarının senin state'lerine nasıl eşleneceğine, hangi alanın hangi tarafta doğruluk kaynağı olduğuna karar verir. Yukarıdaki iki adapter küçük bir anti-corruption layer gibi davranıyor, çünkü SDK'ların kod ve tutar kavramlarını Bookstore'un Money ve DeclineReason'ına çeviriyor. Bounded context'ler arasındaki büyük çeviri ve context map kavramı, planladığımız Domain-Driven Design serisinin konusu olacak.

Adapter'ı Test Etmek: Contract Test

İki adapter aynı interface'i implement ediyor, ama aynı anlamı taşıdıklarını kim garanti ediyor? Biri sağlayıcı kesintisini PaymentProviderUnavailableException olarak, diğeri ret olarak raporlarsa, client'ın "kesintide başka sağlayıcıyı dene" kuralı yalnız birinde çalışır. Bunun için her adapter'ın sağlaması gereken davranışı bir kez yazıp her adapter için çalıştıran contract test kullanılır. xUnit'te bunun en sade yolu abstract bir test class'ı:

using Bookstore.Payments;
using Vendor.CardSdk;
using Vendor.WalletSdk;

// Her adapter'ın sağlaması gereken davranış: bir kez yazılır, her adapter için çalışır
public abstract class PaymentGatewayContract
{
    protected abstract IPaymentGateway CreateGateway();
    protected abstract string ApprovedToken { get; }
    protected abstract string InsufficientFundsToken { get; }
    protected abstract string OutageToken { get; }

    private static PaymentRequest Request(string token) => new("BK-1001", new Money(129.90m, "TRY"), token);

    [Fact]
    public async Task Approved_payment_returns_a_transaction_id()
    {
        PaymentReceipt receipt = await CreateGateway().ChargeAsync(Request(ApprovedToken));
        Assert.False(string.IsNullOrWhiteSpace(receipt.TransactionId));
    }

    [Fact]
    public async Task Insufficient_funds_is_a_decline_with_reason()
    {
        PaymentDeclinedException ex = await Assert.ThrowsAsync<PaymentDeclinedException>(
            () => CreateGateway().ChargeAsync(Request(InsufficientFundsToken)));
        Assert.Equal(DeclineReason.InsufficientFunds, ex.Reason);
    }

    [Fact]
    public async Task Provider_outage_is_unavailable_not_declined()
    {
        await Assert.ThrowsAsync<PaymentProviderUnavailableException>(
            () => CreateGateway().ChargeAsync(Request(OutageToken)));
    }

    [Fact]
    public async Task Canceled_request_throws_OperationCanceledException()
    {
        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => CreateGateway().ChargeAsync(Request(ApprovedToken), new CancellationToken(canceled: true)));
    }
}

public sealed class CardGatewayAdapterTests : PaymentGatewayContract
{
    protected override IPaymentGateway CreateGateway() => new CardGatewayAdapter(new CardProcessorClient("sk_test"));
    protected override string ApprovedToken => "tok_visa";
    protected override string InsufficientFundsToken => "tok_insufficient";
    protected override string OutageToken => "tok_issuer_down";
}

public sealed class WalletGatewayAdapterTests : PaymentGatewayContract
{
    protected override IPaymentGateway CreateGateway() => new WalletGatewayAdapter(new WalletApiClient());
    protected override string ApprovedToken => "wlt_ada";
    protected override string InsufficientFundsToken => "wlt_low_balance";
    protected override string OutageToken => "wlt_timeout";
}

Cüzdan adapter'ının ilk sürümünde UPSTREAM_TIMEOUT satırı yoktu; zaman aşımı _ dalına düşüp ret oluyordu. dotnet test bunu cüzdan için yakaladı:

  Failed WalletGatewayAdapterTests.Provider_outage_is_unavailable_not_declined [52 ms]
  Error Message:
   Assert.Throws() Failure: Exception type was not an exact match
Expected: typeof(Bookstore.Payments.PaymentProviderUnavailableException)
Actual:   typeof(Bookstore.Payments.PaymentDeclinedException)
---- Bookstore.Payments.PaymentDeclinedException : Payment for BK-1001 was declined: Other.

Failed!  - Failed:     1, Passed:     7, Skipped:     0, Total:     8, Duration: 169 ms

Satır eklendikten sonra:

Passed!  - Failed:     0, Passed:     8, Skipped:     0, Total:     8, Duration: 162 ms

Dört test, iki adapter, sekiz koşu. Yeni bir sağlayıcı geldiğinde dört satırlık yeni bir subclass yazmak, onun da aynı sözleşmeye uyduğunu kanıtlıyor. Buradaki SDK'lar bellekte çalışan sahte class'lar; gerçek bir SDK için aynı contract test'leri sağlayıcının sandbox ortamına karşı, ayrı bir integration test koşusunda çalıştırırsın. C# İleri serisindeki "C#'ta Unit Test: xUnit, Theory/InlineData ve Test Edilebilir Kod" yazısı xUnit'in temellerini anlatıyor.

Sık Düşülen Tuzaklar

Adapter'da iş kuralı biriktirmek. "Tutar 5.000 TL'yi geçerse 3D Secure iste", "kurumsal müşteride indirim uygula", "ikinci denemede diğer sağlayıcıya geç" gibi kurallar adapter'a girdiğinde, aynı kural her adapter'da ayrı ayrı yazılır ve birbirinden ayrılır. Adapter yalnız çevirir; karar veren kod target interface'in öbür tarafında, Bookstore'un kendi service'lerinde durur.

SDK tiplerini target interface'ten sızdırmak. IPaymentGateway.ChargeAsync bir ChargeResponse dönseydi, interface adı Bookstore'a ait olurdu ama sözleşmenin kendisi SDK'ya bağlı kalırdı. Target'taki her tip senin modelin olmalı.

Bütün hataları tek bir generic exception'a çevirmek. "Ödeme başarısız" demek kolay, ama client'ın ret ile kesintiyi ayırt edebilmesi gerekir: birinde müşteriye başka kart sorulur, diğerinde başka sağlayıcı denenir. Bilinmeyen kodları da sessizce onaya ya da rete çevirme; kodu taşı ve logla.

İptalin anlamını abartmak. Cüzdan adapter'ındaki CancellationToken, yalnız beklemeyi bırakır; istek SDK'dan çıktıysa ödeme sağlayıcı tarafında yine gerçekleşebilir. Para hareketlerinde iptal ve retry, aynı siparişin iki kez çekilmesine yol açabilir; Çözüm Desenleri serisindeki "Idempotency Key: Çift Tıklama ve Retry'da Aynı Siparişi İki Kez Oluşturmamak" yazısı bu problemi ele alıyor.

Her SDK method'u için ayrı bir adapter yazmak ya da SDK'nın tüm yüzünü target'a kopyalamak. Target interface, Bookstore'un ihtiyaç duyduğu kadar olmalı. SDK'nın yirmi method'u varsa ve sen üçünü kullanıyorsan target'ta üç method var. Retry ve circuit breaker gibi dayanıklılık davranışları da adapter'ın değil, ilerideki External Service Integration ve Resilience serisinin konusu olan ayrı katmanların işi.

Gerek yokken sarmak. Her üçüncü parti kütüphaneyi, onun API'sini birebir kopyalayan bir interface ve adapter ile sarmak çoğu zaman bir şey kazandırmaz. Kütüphane zaten senin modeline uygunsa (örneğin ILogger ya da HttpClient gibi .NET'in kendi soyutlamalarını kullanıyorsa), tek bir yerde kullanılıyorsa ve değiştirilmesi gündemde değilse, adapter yalnız fazladan bir dosya ve fazladan bir indirection demek. Adapter'ın değeri, modeller gerçekten uyumsuz olduğunda ya da aynı rol için birden fazla sağlayıcı olduğunda ortaya çıkar.

Küçük Bir Deneme

★☆☆ Açılıştaki OrderService'i IPaymentGateway'e bağımlı olacak şekilde değiştir ve CardGatewayAdapter ile çalıştır. Kuruş dönüşümünü test etmek için 0,01 TL, 0,005 TL ve 1.000.000 TL'lik siparişler dene; hangisinde ne beklediğini önce yaz.

★★☆ Kart SDK'sına "54" (kartın süresi dolmuş) kodunu ekle. DeclineReason'a yeni bir değer mi eklersin, yoksa Other mı kalır? Kararını contract test'e yeni bir test ekleyerek sabitle.

★★★ Kesinti durumunda ikinci sağlayıcıyı deneyen bir FailoverPaymentGateway yaz; kendisi de IPaymentGateway implement etsin. Bu class bir adapter mı? Değilse hangi pattern'e benziyor ve hangi durumda aynı siparişi iki kez çekme riski doğar?

Adapter, var olan iki parçayı sonradan birbirine uydurdu. Bir sonraki yazıda benzer görünen ama niyeti tam tersi olan bir pattern'e geçiyoruz: Bridge, abstraction ile implementation'ı baştan ayırıp ikisinin bağımsız büyümesini sağlıyor. Örneğimiz, mesaj tipleri ile bildirim kanallarının çarpımından doğan class patlaması.