Adapter Design Pattern: Uyumsuz API’ların Yönetimi

Adapter Design Pattern: Uyumsuz API’ların Yönetimi

Merhabalar,

Bu yazıda Adapter (Adaptör) Tasarım Deseni üzerine konuşacağız. Adapter, Yapısal (Structural) tasarım desenleri ailesine ait, yazılım dünyasındaki en pratik ve en sık kullanılan desenlerden biridir. Temel amacı, bir sınıfın arayüzünü (interface), istemcinin (client) beklediği başka bir arayüze dönüştürmektir. Bu sayede, normalde arayüz uyumsuzlukları nedeniyle birlikte çalışamayan sınıfların birlikte çalışmasına olanak tanır.

Yazılım geliştirirken Coupling (Bağlılık) ve Cohesion (Bağdaşım) kelimelerini sıkça duymuşuzdur. Biz yazılım geliştirirken her zaman High Cohesion (Yüksek Bağdaşım) — yani bir sınıfın veya modülün içindeki elemanların birbirine olan bağlılığının ve benzerliğinin yüksek olmasını — isteriz. Bu, aslında SOLID prensiplerinden Single Responsibility Principle (Tek Sorumluluk Prensibi) ile de yakından ilgilidir. Bununla beraber Loose Coupling (Gevşek Bağımlılık) yani sınıflar veya modüller arasındaki ilişkinin olabildiğince az olmasını hedefleriz. Bunu istememizdeki amaç, projenin ileriki safhalarında bir modülde yapılacak değişikliklerin diğer modülleri minimum düzeyde etkilemesidir. Böylece hem gelişim hem de bakım çok daha kolay olacaktır.

Ancak gerçek dünya projeleri nadiren bu kadar ideal koşullarda ilerler. Çoğu zaman, kodunu değiştiremediğimiz harici kütüphaneler (third-party libraries) veya dokunmaktan çekindiğimiz eski, “miras” sistemler (legacy systems) ile çalışmak zorunda kalırız. İşte bu noktada karşımıza kaçınılmaz bir sorun çıkar: “Uyumsuz Arayüz” (Incompatible Interface) sorunu. Elimizdeki bileşen, tam olarak ihtiyacımız olan işi yapsa da, bizim sistemimizin “konuştuğu dili” konuşmaz. Metot isimleri farklıdır, beklediği parametreler farklıdır, geri dönüş tipleri farklıdır.

Bu sorunu çözmek için Adapter deseni, tıpkı gerçek hayatta farklı ülkelerin priz standartlarını birbirine uyduran seyahat adaptörleri gibi çalışır. Bizim fişimiz (istemci kodumuz) ile duvardaki priz (harici kütüphane) arasına girerek bir “tercüman” veya “dönüştürücü” görevi görür.

Bu yazıda, öncelikle bu uyumsuzluk ihtiyacının neden kaynaklandığına ve somut bir senaryo üzerinden ne gibi sorunlara yol açtığına bakacağız. Ardından Adapter deseninin bu sorunu SOLID prensiplerine uygun bir şekilde nasıl çözdüğünü inceleyeceğiz.

Uyumsuzluk Sorununun Kaynakları

Yazılım projelerinde “uyumsuzluk” sorununun ortaya çıkmasının birkaç yaygın nedeni vardır:

  1. Harici Kütüphaneler ve API’ler: Günümüzde her projeyi sıfırdan yazmıyoruz. Ödeme almak için, e-posta göndermek için veya karmaşık analizler yapmak için harici servislere ve kütüphanelere güveniriz. Bu kütüphanelerin kaynak kodunu biz yazmadığımız için arayüzlerini de değiştiremeyiz. Onların ExecutePayment metodu, bizim sistemimizin beklediği ProcessTransaction metoduna uymak zorunda değildir.
  2. Miras Kod (Legacy Systems): Şirketinizde 10 yıldır sorunsuz çalışan, kritik bir işi (örneğin faturalandırma veya stok yönetimi) yapan eski bir modül olabilir. Bu modülü yazanlar artık şirkette olmayabilir ve kimse o koda dokunmaya cesaret edemeyebilir. Yeni, modern sisteminizin bu eski modülle konuşması gerektiğinde, iki sistemin “dili” ve “protokolü” büyük olasılıkla tamamen farklı olacaktır.
  3. Farklı Ekiplerin Geliştirdiği Modüller: Büyük ölçekli projelerde, farklı ekipler projenin farklı modülleri üzerinde eş zamanlı çalışabilir. İletişim eksiklikleri veya değişen gereksinimler nedeniyle, “Sipariş” modülünün beklediği CheckStockAvailability(string sku, int quantity) arayüzü, “Stok” modülünün sunduğu IsInStock(Product product, int amount) arayüzü ile uyumsuz olabilir.

Bu senaryoların hepsi, sistemimizde yönetilmesi zor, kırılgan ve “kirli” koda yol açma potansiyeline sahiptir. Şimdi bu sorunu daha net görebilmek için somut bir e-ticaret senaryosu üzerinden ilerleyelim.

E-Ticaret Ödeme Sistemleri

Bir e-ticaret platformu geliştirdiğimizi ve sipariş tamamlama sürecinde müşteriden ödeme almamız gerektiğini varsayalım. Bizim sistemimiz (istemci kodumuz), ödeme işlemleri için standart, temiz ve kendi iş mantığımıza uygun bir arayüz (interface) beklemelidir. Bu bizim “ideal” dünyamızdır.

Bizim İdeal Dünyamız: The Target (Hedef)

Sistemimizin ana iş mantığı, örneğin OrderService gibi sınıflar, tüm ödeme sistemleriyle konuşmak için IPaymentGateway adında tek bir arayüze bağımlı olmalıdır. Bu, Dependency Inversion (SOLID’in ‘D’si) prensibinin de bir gereğidir.

// BİZİM İDEAL ARAYÜZÜMÜZ (TARGET)
// Sistemimizdeki 'OrderService' gibi sınıflar SADECE bu arayüzü tanır.
public interface IPaymentGateway
{
    // Bizim sistemimiz bir ödeme işleminin 'decimal' tutar alıp,
    // kart bilgilerini alıp 'bool' bir başarı durumu dönmesini bekliyor.
    bool ProcessPayment(decimal amount, string creditCardNumber, string cvv, int expiryYear, int expiryMonth);
}

Bu bizim Target (Hedef) arayüzümüzdür. Gayet temiz, basit ve bizim ihtiyacımıza yöneliktir.

Gerçek Dünya: The Adaptees (Adapte Edilecekler)

Ancak yönetim, sisteme iki farklı, popüler ödeme sağlayıcısını entegre etmemizi istiyor: “LegacyBank” ve “ModernPay”. Bu iki sağlayıcının bize verdiği SDK’lar (kütüphaneler) ise bizim ideal IPaymentGateway arayüzümüzle hiç uyumlu değil.

Bu sınıflar harici kütüphanelerden gelir ve kaynak kodlarını değiştiremeyiz.

1. LegacyBank (Eski Sistem): Bu, 10 yıl önce yazılmış, belki SOAP tabanlı eski bir API’nin C# kütüphanesi.

// HARİCİ KÜTÜPHANE 1: LegacyBank (Bunu değiştiremiyoruz!)
// Bu sınıfın varlığından haberdarız ama koduna dokunamayız.
public class LegacyBankApi
{
    // Metot adı farklı: 'ProcessPayment' değil
    // Parametre tipleri farklı: 'decimal' değil 'double'
    // Ekstra ve gereksiz parametre istiyor: 'userToken'
    // Parametre sırası farklı ve kart bilgilerini birleşik istiyor.
    public string ExecuteTransaction(string userToken, double totalAmount, string combinedCardDetails)
    {
        // ... (Eski sistemle karmaşık SOAP/XML iletişimi) ...
        Console.WriteLine($"LegacyBank: Transaction executed for {totalAmount} TRY.");
        
        // Geri dönüş tipi farklı: 'bool' değil, 'string' (transaction ID)
        // Başarısız olursa 'null' veya 'ERROR_CODE' dönebilir.
        return Guid.NewGuid().ToString(); // Başarıyı simüle edelim
    }
}

2. ModernPay (Yeni Sistem): Bu, JSON tabanlı modern bir REST API’nin kütüphanesi. Parametreleri tek tek değil, bir “request object” olarak istiyor ve sonucu da bir “result object” olarak dönüyor.

// HARİCİ KÜTÜPHANE 2: ModernPay (Bunu da değiştiremiyoruz!)
public class ModernPaymentProvider
{
    // Metot adı farklı: 'Charge'
    // Parametreler tek bir nesne içinde
    public PaymentResult Charge(PaymentRequest request)
    {
        // ... (API'ye JSON gönderiliyor) ...
        Console.WriteLine($"ModernPay: Charged {request.Amount} from {request.CardNumber}.");
        
        // Geri dönüş tipi farklı: 'bool' değil, 'PaymentResult' nesnesi
        return new PaymentResult 
        { 
            IsSuccess = true, 
            TransactionId = "MP-987654" 
        };
    }
}

// ModernPay'in ihtiyaç duyduğu yardımcı sınıflar (Bunları da değiştiremiyoruz)
public class PaymentRequest
{
    public decimal Amount { get; set; }
    public string CardNumber { get; set; }
    public string Cvv { get; set; }
    // Bizim arayüzümüzden farklı olarak, yıl ve ayı birleşik bir string istiyor
    public string ExpiryDate { get; set; } // "MM/YYYY" formatında
}

public class PaymentResult
{
    public bool IsSuccess { get; set; }
    public string TransactionId { get; set; }
    public string ErrorMessage { get; set; }
}

Gördüğünüz gibi tam bir kaos ortamı var. Elimizdeki beklenti ve gerçekler şunlar:

  • Bizim Beklentimiz (Target): bool ProcessPayment(decimal amount, string cardNo, string cvv, int year, int month)
  • LegacyBank (Adaptee 1): string ExecuteTransaction(string userToken, double totalAmount, string combinedCardDetails)
  • ModernPay (Adaptee 2): PaymentResult Charge(PaymentRequest request)

Bu üç arayüz birbiriyle tamamen uyumsuzdur.

İlk Yaklaşım: Koşul Blokları ile Çözüm Denemesi

Adapter desenini bilmeyen veya projeyi hızlıca “ayağa kaldırmak” isteyen bir geliştirici, bu sorunu ana iş mantığı sınıfının (örneğin OrderService) içine bir sürü if-else veya switch-case bloğu doldurarak çözmeye çalışır.

İşte bu yaygın (ama sorunlu) yaklaşımın kod örneği:

// SORUNLU YAKLAŞIM: Doğrudan Bağımlılık ve Koşul Blokları
public class OrderService
{
    // Bu metot, siparişi tamamlamak için ödemeyi alır
    public void FinalizeOrder(string paymentProviderName, decimal total, string cardNo, string cvv, int year, int month)
    {
        Console.WriteLine($"Finalizing order with amount: {total} TRY");
        bool paymentSuccess = false;
        // Ana iş akışı sınıfı, tüm ödeme sistemlerinin
        // detaylarını bilmek zorunda kalıyor.
        if (paymentProviderName == "LegacyBank")
        {
            try
            {
                // 1. Doğrudan somut sınıfa bağımlılık
                LegacyBankApi legacyApi = new LegacyBankApi();
                
                // 2. Gerekli veri dönüşümleri
                double amountAsDouble = (double)total;
                string userToken = "get_current_user_token()"; // Eksik veriyi bul
                string cardDetails = $"{cardNo};{cvv};{month}/{year}"; // Beklenen format
                // 3. Harici kütüphane metodunu çağır
                string transactionId = legacyApi.ExecuteTransaction(userToken, amountAsDouble, cardDetails);
                // 4. Dönen sonucu sistemin beklediği formata çevir
                if (!string.IsNullOrEmpty(transactionId) && !transactionId.Contains("ERROR"))
                {
                    paymentSuccess = true;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("LegacyBank payment failed: " + ex.Message);
                paymentSuccess = false;
            }
        }
        else if (paymentProviderName == "ModernPay")
        {
            try
            {
                // 1. Diğer somut sınıfa doğrudan bağımlılık
                ModernPaymentProvider modernApi = new ModernPaymentProvider();
                // 2. Gerekli veri dönüşümleri (Request nesnesi oluştur)
                PaymentRequest request = new PaymentRequest
                {
                    Amount = total,
                    CardNumber = cardNo,
                    Cvv = cvv,
                    ExpiryDate = $"{month.ToString("D2")}/{year}" // Format dönüşümü
                };
                // 3. Harici kütüphane metodunu çağır
                PaymentResult result = modernApi.Charge(request);
                // 4. Dönen sonucu sistemin beklediği formata çevir
                paymentSuccess = result.IsSuccess;
            }
            catch (Exception ex)
            {
                Console.WriteLine("ModernPay payment failed: " + ex.Message);
                paymentSuccess = false;
            }
        }
        else
        {
            Console.WriteLine("Invalid payment provider!");
            return;
        }
        // Ana iş akışının (OrderService'in asıl işi) devamı
        if (paymentSuccess)
        {
            Console.WriteLine("Payment was successful. Order is being prepared.");
            // ... (Stoktan düş, kargo oluştur, fatura kes, e-posta at, vs.) ...
        }
        else
        {
            Console.WriteLine("Payment failed. Please check your details.");
        }
    }
}

Bu kod ilk bakışta “çalışıyor” gibi görünebilir, ancak yazılım mimarisi açısından ciddi sorunlar barındırır.

Bu Yaklaşımın Gelişim ve Bakım Zorlukları

  • Sıkı Bağımlılık (Tight Coupling): OrderService sınıfı, LegacyBankApi ve ModernPaymentProvider somut (concrete) sınıflarına doğrudan bağımlıdır. Bu, OrderService sınıfını bu kütüphaneler olmadan derleyemeyeceğimiz veya kolayca test edemeyeceğimiz anlamına gelir.
  • SOLID Prensiplerinin İhlali: Bu tasarım, en temel yazılım prensiplerinden ikisini doğrudan ihlal eder:
  • Tek Sorumluluk Prensibi (Single Responsibility Principle — SRP): OrderService sınıfının artık birden fazla sorumluluğu var. Asıl sorumluluğu (sipariş iş akışını yönetmek) dışında, artık LegacyBank ve ModernPay için bir “veri çevirmeni” sorumluluğunu da üstlenmiştir. ModernPay, PaymentRequest nesnesine yeni bir zorunlu alan eklerse (OrderService değişmeli). LegacyBank, ExecuteTransaction metodunun adını değiştirirse (OrderService yine değişmeli). Bu, sınıfın değişmesi için çok fazla neden yaratır ve bu çok kırılgan (fragile) bir yapıdır.
  • Açık/Kapalı Prensibi (Open/Closed Principle — OCP): Bu prensip, sınıfların genişlemeye açık (open for extension), ancak değişime kapalı (closed for modification) olması gerektiğini söyler. Bu kod bu prensibi tamamen yok sayar. Yarın yönetim gelip “Artık NewPay adında üçüncü bir sağlayıcıyı ekliyoruz” derse ne yapacaksınız? OrderService sınıfının kodunu açıp, o devasa if-else bloğuna bir else if (paymentProviderName == "NewPay") bloğu daha eklemek (yani sınıfı değiştirmek) zorundasınız. Her yeni sağlayıcı, çalışan ve test edilmiş bu ana iş mantığı sınıfının tekrar tekrar değiştirilmesine, risk alınmasına ve yeniden test edilmesine neden olacaktır. Bu, sürdürülebilir bir mimari değildir.
  • Test Edilebilirlik (Testability) Zorluğu: Bu FinalizeOrder metodunu unit tests yazmak oldukça zorlayıcı olur. Hem LegacyBankApi hem de ModernPaymentProvider sınıflarını “mock”lamamız gerekir. Kodun karmaşıklığı nedeniyle tüm if-else yollarını test etmek çok zordur ve ana iş mantığını (stoktan düşme, kargo oluşturma) test etmek isterken bile kendimizi ödeme sistemlerinin detaylarıyla boğuşurken buluruz.

İşte tam bu noktada, bu sorumlulukları ayırmak ve daha esnek bir yapı kurmak için Adapter desenine ihtiyaç duyarız.

Adapter Deseni ile Uyum Sağlamak

Adapter deseni bize bu sorunları çözmek için çok net bir çözüm önerir:

“Ana iş mantığını (OrderService) bu çeviri karmaşasından kurtar. Bırak o sadece kendi ‘ideal’ dünyasındaki arayüz (IPaymentGateway) ile konuşsun. Sen, uyumsuz olan her bir harici sistem (LegacyBank, ModernPay) için ayrı ayrı ‘tercüman’ sınıflar yaz. Bu tercüman sınıfların her biri, senin ‘ideal’ IPaymentGateway arayüzünü uygulasın (implements) ve arka planda tüm o kirli çeviri işlerini (veri formatlama, metot çağırma, sonuç dönüştürme) kendisi yapsın.”

Bu kalıbı uyguladığımızda, üç temel oyuncumuz olur:

1. Hedef (Target)

  • Nedir? Bu, bizim istemci (client) kodumuzun (yani OrderService) çalışmak istediği, beklediği, “ideal” arayüzdür.
  • Bizim Senaryoda: IPaymentGateway arayüzü.

2. Adapte Edilen (Adaptee)

  • Nedir? Bu, uyumsuz arayüze sahip olan, “eski” veya “harici” sınıftır. Kodunu değiştiremediğimiz sınıf budur.
  • Bizim Senaryoda: LegacyBankApi sınıfı ve ModernPaymentProvider sınıfı.

3. Adaptör (Adapter)

  • Nedir? Bu, sihrin gerçekleştiği yerdir. Bu, bizim yazdığımız yeni “tercüman” sınıftır.
  • Hedef (Target) arayüzünü uygular (implements). (Yani dışarıdan bakınca bir IPaymentGateway gibi görünür).
  • İçerisinde, Adapte Edilen (Adaptee) sınıfın bir örneğini (referansını) tutar. (Buna Composition denir. “has-a” ilişkisi).
  • İşleyişi: İstemci (Client), Adaptör’ü IPaymentGateway sanarak çağırır (örn. ProcessPayment metodunu çağırır). Adaptör, bu çağrıyı alır, gerekli tüm dönüşümleri (veri tipi, parametre sırası, nesne oluşturma) yapar ve içerisindeki Adaptee‘nin asıl metodunu (örn. ExecuteTransaction veya Charge) çağırır. Son olarak, Adaptee‘den gelen sonucu da Target‘ın beklediği formata (örn. bool) çevirip istemciye döner.

Bu yapı sayesinde, istemci kodumuz (OrderService), LegacyBankApi veya ModernPaymentProvider gibi sınıfların varlığından hiç haberdar olmaz. O sadece IPaymentGateway arayüzünü bilir ve onunla konuşur. Hangi adaptörün ona verildiğine bağlı olarak, arka planda doğru bankaya yönlendirme yapılır.

Senaryonun Adapter Deseni ile Yeniden Düzenlenmesi

Şimdi, sorunlu kodu Adapter kalıbı daha temiz, modüler bir şekilde inşa edelim.

Adım 1: Target Arayüz (Değişiklik Yok)

Bu arayüzü zaten tanımlamıştık. Bu bizim standartımızdır ve istemci kodumuzun dayanak noktasıdır.

// ADIM 1: HEDEF (TARGET) ARAYÜZ
public interface IPaymentGateway
{
    bool ProcessPayment(decimal amount, string creditCardNumber, string cvv, int expiryYear, int expiryMonth);
}

Adım 2: Adaptee Sınıfları (Değişiklik Yok)

Bunlar da harici kütüphanelerimizdi, değişmediler: LegacyBankApi ve ModernPaymentProvider.

Adım 3: Adapter Sınıflarının Oluşturulması

Şimdi “tercümanlarımızı” yazıyoruz. Her bir uyumsuz sistem (Adaptee) için bir tane Adapter sınıfı oluşturacağız. Bu sınıflar Nesne Adaptörü (Object Adapter) yapısını kullanacak (Composition yoluyla).

Adaptör 1: LegacyBank için

Bu adaptör, IPaymentGateway (Target) gibi görünecek ama içinde LegacyBankApi‘yi (Adaptee) çalıştıracak ve tüm çeviri işini yapacak.

// ADIM 3.1: LEGACYBANK İÇİN ADAPTÖR
// Bu sınıf, Target (IPaymentGateway) arayüzünü uygular
public class LegacyBankAdapter : IPaymentGateway
{
    // İçerisinde Adaptee'nin (LegacyBankApi) bir örneğini tutar (Composition)
    private readonly LegacyBankApi _legacyApi;
    // Adaptörü oluştururken, çevireceği asıl servisi (adaptee) alırız.
    // Bu, Dependency Injection (DI) için de uygundur.
    public LegacyBankAdapter(LegacyBankApi legacyApi)
    {
        _legacyApi = legacyApi;
    }
    // Bu metot, bizim "ideal" (Target) arayüzümüzden gelir
    public bool ProcessPayment(decimal amount, string creditCardNumber, string cvv, int expiryYear, int expiryMonth)
    {
        // Gerekli adaptasyon (çeviri) işlemleri burada yapılır.
        // 1. Veri tipi dönüşümü: decimal -> double
        double amountAsDouble = (double)amount;
        
        // 2. Eksik 'userToken' parametresini sağla
        string userToken = GetUserTokenFromSession(); 
        
        // 3. Parametreleri beklenen formata birleştir
        string cardDetails = $"{creditCardNumber};{cvv};{expiryMonth}/{expiryYear}";
        // 4. Adaptee'nin metodunu çağır
        try
        {
            string transactionId = _legacyApi.ExecuteTransaction(userToken, amountAsDouble, cardDetails);
            // 5. Geri dönüş tipi dönüşümü: string -> bool
            if (!string.IsNullOrEmpty(transactionId) && !transactionId.Contains("ERROR"))
            {
                return true; // Başarılı
            }
            else
            {
                return false; // Başarısız
            }
        }
        catch (Exception ex)
        {
            // Gerçek dünyada burada loglama yapılır
            Console.WriteLine("LegacyBank adapter exception: " + ex.Message);
            return false;
        }
    }

    private string GetUserTokenFromSession()
    {
        // Gerçekte burada bir Session/Auth servisinden token alınır
        return "SESSION_TOKEN_12345";
    }
}

Adaptör 2: ModernPay için

Bu adaptör de IPaymentGateway (Target) gibi görünecek ama içinde ModernPaymentProvider‘ı (Adaptee) çalıştıracak.

// ADIM 3.2: MODERNPAY İÇİN ADAPTÖR
// Bu sınıf da Target (IPaymentGateway) arayüzünü uygular
public class ModernPaymentAdapter : IPaymentGateway
{
    // İçerisinde Adaptee'nin (ModernPaymentProvider) bir örneğini tutar
    private readonly ModernPaymentProvider _modernProvider;
public ModernPaymentAdapter(ModernPaymentProvider modernProvider)
    {
        _modernProvider = modernProvider;
    }
    // Bu metot da bizim "ideal" (Target) arayüzümüzden gelir
    public bool ProcessPayment(decimal amount, string creditCardNumber, string cvv, int expiryYear, int expiryMonth)
    {
        // ModernPay için adaptasyon (çeviri) işlemleri
        // 1. Parametreleri 'PaymentRequest' nesnesine haritala
        PaymentRequest request = new PaymentRequest
        {
            Amount = amount,
            CardNumber = creditCardNumber,
            Cvv = cvv,
            // Tarih formatını dönüştür
            ExpiryDate = $"{expiryMonth.ToString("D2")}/{expiryYear}"
        };
        // 2. Adaptee'nin metodunu çağır
        try
        {
            PaymentResult result = _modernProvider.Charge(request);
            // 3. Geri dönüş nesnesini 'bool' tipine dönüştür
            return result.IsSuccess;
        }
        catch (Exception ex)
        {
            Console.WriteLine("ModernPay adapter exception: " + ex.Message);
            return false;
        }
    }
}

Adım 4: İstemci (Client) Sınıfının Düzenlenmesi

Şimdi OrderService sınıfımızı (yani İstemci’yi) yeniden yazalım. O karmaşık if-else bloğu olmadan, tertemiz ve sadece kendi işine odaklanmış bir şekilde.

// ADIM 4: TEMİZ MÜŞTERİ (CLIENT) SINIFI
public class OrderService
{
    // Bağımlılık artık somut sınıflara değil, soyut arayüze.
    // Bu, "Gevşek Bağımlılıktır" (Loose Coupling).
    private readonly IPaymentGateway _paymentGateway;
    // Bu yapıya "Constructor Injection" (Bağımlılık Enjeksiyonu) denir.
    // OrderService, hangi ödeme sistemiyle çalışacağını kendi seçmiyor.
    // Dışarıdan (constructor üzerinden) ona "enjekte ediliyor".
    public OrderService(IPaymentGateway paymentGateway)
    {
        _paymentGateway = paymentGateway;
    }
    // Metot imzası da basitleşti, sadece gerekli bilgileri alıyor
    public void FinalizeOrder(decimal total, string cardNo, string cvv, int year, int month)
    {
        Console.WriteLine($"Finalizing order with amount: {total} TRY");
        // Hangi somut adaptörün enjekte edildiğinden bağımsız olarak
        // standart arayüz metodu çağrılır (Polimorfizm).
        bool paymentSuccess = _paymentGateway.ProcessPayment(total, cardNo, cvv, year, month);
        // Ana iş akışının (OrderService'in asıl işi) devamı
        if (paymentSuccess)
        {
            Console.WriteLine("Payment was successful. Order is being prepared.");
            // ... (Stoktan düş, kargo oluştur, fatura kes, vs.) ...
        }
        else
        {
            Console.WriteLine("Payment failed. Please check your details.");
        }
    }
}

Adım 5: Sistemin Birleştirilmesi (Wiring)

“Peki OrderService sınıfı, LegacyBankAdapter mı yoksa ModernPaymentAdapter mı kullanacağını nasıl biliyor?”

İşte bu noktada Inversion of Control (IoC) prensibi ve bir Dependency Injection (DI) Container (veya basit bir Factory deseni) devreye girer. Bu “kurulum” (wiring) işlemi, ana iş mantığının dışında, genellikle uygulamanın başlangıç noktasında (örneğin Program.cs veya Startup.cs gibi bir “Composition Root” içinde) yapılır.

İşte basit bir Program.cs simülasyonu:

// ADIM 5: SİSTEMİN BAŞLANGIÇ NOKTASI (Örn: Program.cs)
public class Program
{
    public static void Main(string[] args)
    {
        // Kullanıcının ödeme sayfasında yaptığı seçimi simüle edelim
        string userPaymentChoice = "ModernPay"; // veya "LegacyBank"
        decimal orderAmount = 150.75m;
        
        // Kart bilgileri UI'dan veya bir formdan gelir...
        string cardNumber = "1234-5678-9012-3456";
        string cvv = "123";
        int expiryYear = 2028;
        int expiryMonth = 12;
        IPaymentGateway selectedGateway;
        // Bu "seçim" mantığı, ana iş akışının DIŞINDADIR.
        // Burası IoC Container'ın (Autofac, .NET Core DI) devreye girdiği yerdir.
        // Bu konfigürasyonel kod, iş mantığı kodundan ayrıdır.
        if (userPaymentChoice == "LegacyBank")
        {
            // 1. Adapte edilecek asıl nesneyi oluştur
            LegacyBankApi legacyApi = new LegacyBankApi();
            // 2. Onu LegacyBankAdapter ile "sar" (wrap)
            selectedGateway = new LegacyBankAdapter(legacyApi);
        }
        else if (userPaymentChoice == "ModernPay")
        {
            // 1. Adapte edilecek asıl nesneyi oluştur
            ModernPaymentProvider modernApi = new ModernPaymentProvider();
            // 2. Onu ModernPaymentAdapter ile "sar"
            selectedGateway = new ModernPaymentAdapter(modernApi);
        }
        else
        {
            throw new Exception("Invalid payment provider");
        }
        // 3. OrderService'i, seçilen adaptör ile (soyut arayüz üzerinden) oluştur
        // OrderService'in hangi somut adaptörün geldiğinden haberi yok.
        OrderService orderService = new OrderService(selectedGateway);
        // 4. İşlemi tamamla
        orderService.FinalizeOrder(orderAmount, cardNumber, cvv, expiryYear, expiryMonth);
    }
}

İşte bu kadar! OrderService sınıfı artık tertemiz, sadece kendi işine odaklanmış ve dış dünyadaki ödeme sağlayıcılarının karmaşıklığından tamamen izole edilmiş durumda.

Adapter Kullanımının Avantajları

Bu desenin bize sağladığı faydaları, ilk yaklaşımdaki sorunlar üzerinden tekrar değerlendirelim:

  • Gevşek Bağımlılık (Loose Coupling) Sağlandı: OrderService sınıfımız artık LegacyBankApi veya ModernPaymentProvider gibi somut sınıflara kilitli değil. Yalnızca soyut IPaymentGateway arayüzünü biliyor. Yarın ModernPaymentProvider sınıfı, Charge metodunun adını ExecuteChargeV2 olarak değiştirirse, bizim OrderService sınıfımızın haberi bile olmaz. O etkilenmez. Sadece ve sadece ModernPaymentAdapter sınıfını açar, içindeki çeviri mantığını güncelleriz. Değişikliğin etkisini tek bir noktada (adaptör sınıfı) izole etmiş oluruz.
  • Tek Sorumluluk Prensibi (SRP) Sağlandı: Her sınıfın artık değişmek için tek bir nedeni var:
  • OrderService: Sadece siparişin iş akışını yönetmekten sorumlu. Değişmesi için tek neden, sipariş akışının (stoktan düş, kargo hazırla vb.) değişmesidir.
  • LegacyBankAdapter: Sadece IPaymentGateway arayüzünü LegacyBankApi arayüzüne çevirmekten sorumlu. Değişmesi için tek neden, LegacyBankApi’nin değişmesidir.
  • ModernPaymentAdapter: Sadece IPaymentGateway arayüzünü ModernPaymentProvider arayüzüne çevirmekten sorumlu. Değişmesi için tek neden, ModernPaymentProvider API’sinin değişmesidir. Bu sorumluluk ayrımı, kodun anlaşılmasını, test edilmesini ve bakımını kat kat kolaylaştırır.
  • Açık/Kapalı Prensibi (OCP) Sağlandı: Bu, en büyük mimari kazançtır.
  • Senaryo: Yönetim geldi ve “Harika! Şimdi bir de NewPay’i ekleyelim” dedi.
  • Eski Sorunlu Kodda: OrderService sınıfını açıp, if-else bloğuna yeni bir else if eklerdik. Yani çalışan, test edilmiş, stabil bir sınıfı değiştirirdik. Bu, OCP ihlalidir ve risklidir.
  • Adapter Çözümünde:
  • OrderService sınıfına kesinlikle dokunmayız (Değişime kapalı).
  • public class NewPayAdapter : IPaymentGateway { ... } adında yeni bir sınıf oluştururuz. (Genişlemeye açık).
  • Program.cs‘teki o başlangıç “kurulum” (wiring) mantığına else if (userPaymentChoice == "NewPay") satırını ekleriz (veya IoC container’a bu yeni kaydı yaparız). Sistemimiz, var olan kodu değiştirmeden, sadece yeni bir adaptör sınıfı ekleyerek genişlemiş oldu. Bu, sürdürülebilir yazılım mimarisinin temel taşlarından biridir.
  • Test Edilebilirlik (Testability) Arttı: OrderService sınıfını test etmek artık çok kolaydır. IPaymentGateway bir arayüz olduğu için, onu taklit etmek (mock) son derece basittir. OrderService‘in ödeme başarılı olduğunda stoktan düştüğünü test etmek için sahte bir IPaymentGateway mock’u oluşturup ProcessPayment metodunun true dönmesini sağlamamız yeterlidir. Artık testlerimizde LegacyBankApi‘nin string formatlarıyla uğraşmak zorunda değiliz.

Adapter, Facade ve Decorator Arasındaki Farklar

Adapter deseni, yapısal desenler ailesinin bir üyesidir ve bazen diğer desenlerle karıştırılabilir. Aradaki farkları bilmek önemlidir:

  • Adapter (Adaptör) vs. Facade (Cephe): Bu ikisi en çok karıştırılanlardır.
  • Adapter: Amacı arayüzü dönüştürmektir. İki uyumsuz arayüzü (Target ve Adaptee) birbirine uydurur. İstemci, var olan bir ITarget arayüzünü bekler.
  • Facade: Amacı sistemi basitleştirmektir. Çok karmaşık bir alt sistemi (örn: bir video işleme kütüphanesindeki AudioCodec, VideoCodec, FileStreamer, Encoder gibi 10 farklı sınıfı) alır ve onu tek, basit bir arayüz (örn: ConvertVideo(string input, string output)) arkasına gizler. Facade, genellikle yeni bir basit arayüz tanımlar; Adapter ise var olan bir arayüze uyum sağlar.
  • Adapter (Adaptör) vs. Decorator (Dekoratör):
  • Adapter: Nesnenin arayüzünü değiştirir.
  • Decorator: Nesnenin arayüzünü değiştirmez, ona yeni sorumluluklar/davranışlar ekler. Decorator, Target ile aynı arayüzü uygular ve içinde Target’ın bir örneğini tutar (tıpkı Object Adapter gibi). Ancak amacı çeviri yapmak değil, zenginleştirmektir. (Örneğin, IPaymentGateway‘i implemente eden bir LoggingPaymentDecorator yazıp, ProcessPayment metodunu çağırmadan önce ve sonra log atmasını sağlayabilirsiniz).

Sonuç

Bu yazımızda, yazılım geliştirmenin kaçınılmaz bir gerçeği olan “arayüz uyumsuzluğu” sorununu inceledik. Bu sorunun, harici kütüphaneler veya eski sistemler gibi kontrolümüz dışındaki bileşenlerden nasıl kaynaklandığını gördük.

Bu uyumsuzluğu, ana iş mantığımızı if-else bloklarıyla kirleterek çözmeye çalışmanın, kodumuzu nasıl SOLID prensiplerine (özellikle OCP ve SRP) aykırı, sıkı bağımlı (tightly coupled), test edilmesi zor ve bakımı imkansız bir hale getirdiğini somut C# örnekleriyle gördük.

Ardından, Adapter deseninin bu kaosa nasıl bir çözüm getirdiğini keşfettik. Adapter’ı, uyumsuz bir sınıfı (Adaptee), istemcimizin beklediği ideal arayüze (Target) uyan bir “tercüman” veya “dönüştürücü” (Adapter) içine alarak nasıl uyguladığımızı adım adım inceledik.

Adapter deseni sayesinde OrderService gibi istemci sınıflarımız tertemiz, sadece kendi işine odaklanmış, test edilebilir ve en önemlisi değişime kapalı ama genişlemeye açık bir hale geldi. Adapter, yazılım mimarisinde bir “diplomat” gibidir; farklı dilleri konuşan, farklı kültürlerden (eski kod, yeni kod, harici kod) gelen sistemlerin bir araya gelip uyum içinde, sorunsuzca çalışmasını sağlar. Bu yazıda adapter tasarım desenini inceledik bir sonraki yazılarda görüşmek üzere.

İyi çalışmalar.

Yorum bırakın