Yükleniyor

If-Else Bloklarını Geride Bırakmak: .NET 10 ile Strategy Pattern ve Esnek Mimari

Blog Kategorileri

.Net Core Mimariler
Tasarım Desenleri
ORM Araçları
API Geliştirme
Web Geliştirme
Veritabanları
26 Mart 2026 Perşembe
If-Else Bloklarını Geride Bırakmak: .NET 10 ile Strategy Pattern ve Esnek Mimari

Merhaba,

Yazılım geliştirme süreçlerinde asıl zorluk, bir kod parçasını çalıştırmak değildir; o kodun aylar, hatta yıllar sonra gelecek değişim taleplerine ne kadar dayanıklı olacağını tasarlamaktır. Sürdürülebilir bir backend mimarisi inşa ederken geliştiriciler genellikle şu kritik kararla karşı karşıya kalır: kısa vadede hızlı çözüm üretmek mi, yoksa uzun vadede genişlemeye açık bir yapı kurmak mı?

Gerçek dünya senaryoları bu ikilemin ne kadar önemli olduğunu net bir şekilde gösterir. Örneğin, bir e-ticaret platformunda başlangıçta yalnızca kullanıcıya hoş geldin e-postası gönderen basit bir sisteminiz olduğunu düşünelim. İlk implementasyon oldukça yalındır: tek bir SendEmail() çağrısı.

Ancak sistem büyüdükçe gereksinimler değişir. Pazarlama ekibi SMS entegrasyonu talep eder, ardından mobil uygulama ile birlikte push notification ihtiyacı doğar. Kısa vadeli çözümlerle ilerlerseniz, bu yeni ihtiyaçları karşılamak için mevcut yapıya koşullu bloklar eklersiniz. Zamanla bu yaklaşım, birbirine bağımlı ve yönetilmesi zor bir karar yapısına dönüşür.

Bu noktada ortaya çıkan problem sadece kodun okunabilirliğinin azalması değildir. Aynı zamanda: Yeni bir kanal eklemek mevcut kodu değiştirmeyi zorunlu kılar, unit test yazmak karmaşık hale gelir, küçük bir değişiklik beklenmeyen yan etkilere neden olabilir. Tam da bu tür problemlerin çözümü için tasarlanmış olan Strategy Design Pattern, davranışları birbirinden ayrıştırarak yönetilebilir hale getirir. Bu desen sayesinde her bir iş yapma biçimi bağımsız bir strateji olarak modellenir. Böylece sisteminiz: Yeni bir davranış eklerken mevcut kodu değiştirmek zorunda kalmaz, çalışma zamanında farklı stratejiler arasında geçiş yapabilir ve daha test edilebilir ve daha öngörülebilir hale gelir

Kısacası Strategy Pattern, kodunuzu yalnızca “çalışan” bir yapı olmaktan çıkarır; değişime uyum sağlayabilen, evrilebilen bir mimariye dönüştürür.

Strategy Design Pattern Nedir

Strategy Design Pattern, bir işin birden fazla gerçekleştirilme yolu olduğu durumlarda, bu davranışları birbirinden bağımsız sınıflar halinde tanımlayıp, çalışma zamanında uygun olanı seçmemizi sağlayan davranışsal bir tasarım kalıbıdır.

Bu yaklaşımın temel amacı, değişken olan davranışı sabit olan yapıdan ayırmaktır. Böylece sistemin geri kalanı bu değişimden etkilenmeden genişletilebilir hale gelir.

Kavramsal olarak düşündüğümüzde, Strategy Pattern üç ana bileşen etrafında şekillenir:

  1. Ortak bir arayüz
  2. Bu arayüzü implemente eden farklı stratejiler
  3. Stratejiyi kullanan istemci

Bu yapıyı daha somut hale getirmek için gündelik bir örnek üzerinden ilerleyelim.

Şehir merkezine gitmek istediğinizi düşünün. Önünüzde birden fazla alternatif vardır ve her biri farklı bir strateji'yi temsil eder:

  1. Otobüs: Düşük maliyetli ancak zaman açısından verimsiz
  2. Taksi: Hızlı ancak maliyetli
  3. Yürüyüş: Ücretsiz ancak fiziksel olarak yorucu

Burada dikkat edilmesi gereken kritik nokta şudur: hedef değişmez, ancak hedefe ulaşma yöntemi değişkendir. Karar mekanizması ise tamamen o anki koşullara bağlıdır; hava durumu, zaman kısıtı veya bütçe gibi faktörler seçimi doğrudan etkiler.

Yazılım tarafında da durum aynıdır. Örneğin bir bildirim sistemi tasarladığınızda, hedef her zaman kullanıcıya bildirim ulaştırmak'tır. Ancak bu hedefe ulaşma yöntemi değişebilir: e-posta, SMS veya push notification.

Strategy Pattern tam olarak bu değişkenliği yönetilebilir hale getirir. Her bir gönderim yöntemi ayrı bir strateji olarak modellenir ve sistem, çalışma anında uygun stratejiyi seçerek işlemi gerçekleştirir.

Neden "If-Else" Yerine Bu Deseni Kullanıyoruz?

public void Send(string type)
{
    if (type == "Email") { /* 50 satır email kodu */ }
    else if (type == "SMS") { /* 40 satır SMS kodu */ }
    else if (type == "Push") { /* 30 satır Push kodu */ }
}

Bu yaklaşım küçük ölçekli bir senaryoda çalışır. Ancak sistem büyüdükçe bu yapı hızla kontrol edilemez hale gelir. Asıl problem, if-else kullanımı değil; değişen davranışların tek bir noktada toplanmasıdır.

Bu kod parçası zamanla şu problemleri üretir:

  1. Her yeni kanal eklediğinizde mevcut metodu değiştirmek zorunda kalırsınız. 
  2. Tüm iş mantığı tek bir metoda yığılır; bu da okunabilirliği ve bakım maliyetini ciddi şekilde düşürür.
  3. Unit test yazmak zorlaşır çünkü her senaryo için farklı branch’leri tetiklemeniz gerekir.
  4. En kritik nokta: Küçük bir değişiklik beklenmedik yan etkilere yol açabilir.

Daha da önemlisi, bu yapı genelde burada kalmaz. Gerçek hayatta type kontrolüne ek olarak kullanıcı tercihleri, retry mekanizmaları, loglama, fallback senaryoları gibi yeni koşullar eklenir ve kod şu hale evrilir:

if (type == "Email")
{
    if(user.IsActive && user.EmailVerified)
    {
        // send email
    }
}

Artık problem sadece okunabilirlik değil; davranışın yönetilememesi haline gelir. Strategy Pattern bu noktada devreye girerek bu karmaşayı yapısal olarak çözer. Her bir davranışı ayrı bir sınıfa taşıyarak sistemi parçalara böler. Böylece:

Değişiklikler izole edilir. Örneğin e-posta gönderiminde bir hata varsa, sadece ilgili strateji üzerinde çalışırsınız. Sistemin geri kalanı bu değişimden etkilenmez. Geliştirme paralelleştirilebilir. Yeni bir bildirim kanalı eklemek isteyen bir geliştirici, mevcut kodu değiştirmek zorunda kalmaz. Sadece yeni bir strateji implement eder.

Davranış çalışma zamanında seçilebilir hale gelir. Kullanıcı tercihleri, feature flag’ler veya konfigürasyonlar üzerinden hangi stratejinin kullanılacağı dinamik olarak belirlenebilir.

Ancak burada kritik bir noktayı gözden kaçırmamak gerekir: Strategy Pattern, if-else’i tamamen ortadan kaldırmaz; sadece doğru yere taşır. Stratejiyi seçtiğiniz bir nokta her zaman olacaktır. Eğer bu seçim mekanizmasını da yanlış tasarlarsanız, sadece problemi başka bir katmana taşımış olursunuz.

Şimdi Bu Teoriyi Gerçeğe Dönüştürelim

Teorik temelleri anladıysak, sıra bunları .NET 10 dünyasında performanslı, temiz ve sürdürülebilir bir şekilde uygulamaya koymaya geldi. Burada örnek olarak bir bildirim sistemi üzerinden ilerleyeceğiz.

Gerçek Dünya Senaryosu: Bildirim Sistemi

Bir e-ticaret uygulamanız olduğunu düşünün. Kullanıcılar, tercih ettikleri kanallardan bildirim almak istiyor. Örneğin bazı kullanıcılar e-posta isterken bazıları SMS veya push notification tercih ediyor. Amacımız, bu farklı kanalları yönetirken kodu karmaşıklaştırmadan, esnek ve test edilebilir bir yapı kurmak.

Interface ve Stratejiler

Her bir bildirim yönteminin uyması gereken ortak sözleşmeyi INotificationStrategy interface’i ile tanımlıyoruz. Bu sayede sistem, hangi stratejinin kullanılacağını interface üzerinden yönetebiliyor.

public interface INotificationStrategy
{
    string Method { get; }
    Task SendAsync(string to, string message);
}
public class EmailNotification : INotificationStrategy
{
    public string Method => "Email";
    public async Task SendAsync(string to, string message)
    {
        // Gerçek SMTP kodları buraya gelir
        await Task.Delay(50);
        Console.WriteLine($"Email sent to {to}: {message}");
    }
}
 public class SmsNotification : INotificationStrategy
 {
     public string Method => "SMS";
     public async Task SendAsync(string to, string message)
     {
         // sms servis entegrasyonu
         await Task.Delay(50);
         Console.WriteLine($"SMS sent to {to}: {message}");
     }
 }

Burada dikkat edilmesi gereken nokta: her bir strateji kendi işini bağımsız olarak yapıyor ve başka stratejilerden etkilenmiyor

NotificationProvider

Geleneksel yaklaşımlarda her istekte IEnumerable üzerinden filtreleme yapmak gerekir; bu da performans açısından maliyetli olabilir. Biz burada tüm stratejileri uygulama ayağa kalkarken bir Dictionary içine alıyoruz. Böylece runtime’da strateji seçimi gerçekleşiyor.

  public class NotificationProvider : INotificationProvider
  {
      private readonly Dictionary<string, INotificationStrategy> _strategies;

      public NotificationProvider(IEnumerable<INotificationStrategy> strategies)
      {
          _strategies = strategies.ToDictionary(s => s.Method, s => s, StringComparer.OrdinalIgnoreCase);
      }

      public INotificationStrategy GetStrategy(string method)
      {
          if (!_strategies.TryGetValue(method, out var strategy))
              throw new NotSupportedException($"{method} desteklenmiyor.");

          return strategy;
      }
  }

Strateji seçimi anlık ve hızlıdır, yeni strateji eklemek için mevcut provider’a dokunmaya gerek yoktur. Hatalar izole edilmiş olur; bir stratejideki hata diğerlerini etkilemez.

Dependency Injection Kaydı

Stratejilerimiz ve provider birbirine bağlı olduğu için hepsini Singleton olarak kaydediyoruz. Bu, bellek verimliliği sağlar ve DI container üzerinden kolay yönetilebilir.

// Performans için tüm stratejileri ve sağlayıcıyı Singleton olarak kaydediyoruz
builder.Services.AddSingleton<INotificationStrategy, EmailNotification>();
builder.Services.AddSingleton<INotificationStrategy, SmsNotification>();
builder.Services.AddSingleton<INotificationProvider, NotificationProvider>();

Kritik Not: Dictionary Validation: Eğer IEnumerable<INotificationStrategy> içine aynı Method ismine sahip iki farklı sınıf kaydedilirse (örneğin yanlışlıkla iki tane "Email" stratejisi), ToDictionary metodu çalışma anında hata fırlatacaktır.

Çözüm:  ToDictionary yerine GroupBy veya DistinctBy kullanarak daha korumacı bir yapı kurabilirsiniz. 

Controller Seviyesi

Artık tek yapmamız gereken, controller üzerinden provider’dan ilgili stratejiyi almak ve bildirim göndermek. Kod oldukça temiz ve okunabilir:

private readonly INotificationProvider _provider;

public NotificationController(INotificationProvider provider)
{
    _provider = provider;
}

[HttpPost("send")]
public async Task<IActionResult> SendNotification(string method, string to, string message)
{
    var strategy = _provider.GetStrategy(method);
    await strategy.SendAsync(to, message);
    return Ok(new { Success = true, Provider = method });
}

Bu noktada, controller kodu hiçbir if-else içermiyor, strateji eklemek için kodu değiştirmeye gerek kalmıyor ve sistem tamamen esnek, test edilebilir ve genişlemeye açık hale geliyor.

Sonuç

Strategy Pattern, yazılım geliştirmede değişime dayanıklı, genişletilebilir ve test edilebilir bir yapı kurmanın en etkili yollarından biri olarak öne çıkıyor. .NET’in güçlü Dependency Injection mekanizmasıyla birleştiğinde, sadece kodun çalışmasını değil, uzun vadeli sürdürülebilirliğini de garanti altına alıyor.

Bu desen sayesinde, farklı davranışları bağımsız sınıflara ayırarak kodu modüler ve okunabilir hale getiriyoruz. Örneğin bir bildirim sistemi senaryosunda e-posta, SMS veya push notification gibi farklı kanalları eklemek veya değiştirmek, mevcut kodu bozmadan yapılabiliyor. Bu, hem Open/Closed Principle hem de Single Responsibility Principle gibi SOLID prensiplerine tam uyum sağlıyor.

Singleton kullanımı ve Dictionary tabanlı seçim mekanizması, runtime’da strateji seçimini performansı artırıyor. Stratejiler stateless olduğu sürece bellek verimliliği korunuyor ve sistem, yüksek yük altında dahi stabil kalabiliyor. Ayrıca bu yapı, ekipler arasında paralel geliştirmeyi kolaylaştırıyor. Bir geliştirici yeni bir strateji eklerken, diğerleri mevcut stratejiler üzerinde çalışmaya devam edebilir. Bu, hem kodun güvenliğini hem de bakım kolaylığını artırıyor.

Sonuç olarak, Strategy Pattern ile oluşturulmuş bir sistem, yalnızca çalışan bir kod'dan öte, yaşayan, evrilebilen ve değişime hazır bir mimari haline geliyor. Bu yaklaşım, modern .NET projelerinde kodun sürdürülebilirliğini ve performansını aynı anda artırmak için en etkili yöntemlerden biri olarak değerlendirilebilir.

Proje Github: https://github.com/Sinantosun/StrategyDesginPattern

Diğer Bloglarımda Görüşmek Üzere 👋