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:
- 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
ExecutePaymentmetodu, bizim sistemimizin beklediğiProcessTransactionmetoduna uymak zorunda değildir. - 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.
- 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ğuIsInStock(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):
OrderServicesınıfı,LegacyBankApiveModernPaymentProvidersomut (concrete) sınıflarına doğrudan bağımlıdır. Bu,OrderServicesı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):
OrderServicesınıfının artık birden fazla sorumluluğu var. Asıl sorumluluğu (sipariş iş akışını yönetmek) dışında, artıkLegacyBankveModernPayiçin bir “veri çevirmeni” sorumluluğunu da üstlenmiştir.ModernPay,PaymentRequestnesnesine yeni bir zorunlu alan eklerse (OrderServicedeğişmeli).LegacyBank,ExecuteTransactionmetodunun adını değiştirirse (OrderServiceyine 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?
OrderServicesınıfının kodunu açıp, o devasaif-elsebloğuna birelse 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
FinalizeOrdermetodunu unit tests yazmak oldukça zorlayıcı olur. HemLegacyBankApihem deModernPaymentProvidersınıflarını “mock”lamamız gerekir. Kodun karmaşıklığı nedeniyle tümif-elseyolları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’IPaymentGatewayarayü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:
IPaymentGatewayarayü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:
LegacyBankApisınıfı veModernPaymentProvidersı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
IPaymentGatewaygibi 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’ü
IPaymentGatewaysanarak çağırır (örn.ProcessPaymentmetodunu ç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çerisindekiAdaptee‘nin asıl metodunu (örn.ExecuteTransactionveyaCharge) çağırır. Son olarak,Adaptee‘den gelen sonucu daTarget‘ı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ı:
OrderServicesınıfımız artıkLegacyBankApiveyaModernPaymentProvidergibi somut sınıflara kilitli değil. Yalnızca soyutIPaymentGatewayarayüzünü biliyor. YarınModernPaymentProvidersınıfı,Chargemetodunun adınıExecuteChargeV2olarak değiştirirse, bizimOrderServicesınıfımızın haberi bile olmaz. O etkilenmez. Sadece ve sadeceModernPaymentAdaptersı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: SadeceIPaymentGatewayarayüzünüLegacyBankApiarayüzüne çevirmekten sorumlu. Değişmesi için tek neden, LegacyBankApi’nin değişmesidir.ModernPaymentAdapter: SadeceIPaymentGatewayarayüzünüModernPaymentProviderarayü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:
OrderServicesınıfını açıp,if-elsebloğuna yeni birelse ifeklerdik. Yani çalışan, test edilmiş, stabil bir sınıfı değiştirirdik. Bu, OCP ihlalidir ve risklidir. - Adapter Çözümünde:
OrderServicesı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ığınaelse 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ı:
OrderServicesınıfını test etmek artık çok kolaydır.IPaymentGatewaybir 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 birIPaymentGatewaymock’u oluşturupProcessPaymentmetodununtruedönmesini sağlamamız yeterlidir. Artık testlerimizdeLegacyBankApi‘ninstringformatları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
ITargetarayü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,Encodergibi 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 birLoggingPaymentDecoratoryazıp,ProcessPaymentmetodunu ç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.