SOLID Prensipleri
Open/Closed Principle
O — Açık/Kapalı Prensibi
Amaç: Yazılım varlıkları (sınıflar, modüller, fonksiyonlar) genişlemeye açık, değişikliğe kapalı olmalıdır.
Basit Anlatım
Teknik terim yok — günlük hayattan bir benzetme
Bilgisayarınızın USB girişlerini düşünün. Fare, klavye, harici disk, kamera, mikrofon… Yeni bir cihaz aldığınızda bilgisayarın kasasını açıp anakartı yeniden lehimlemezsiniz. Cihazı USB'ye takarsınız, çalışır.
Bilgisayar değişikliğe kapalıdır (içini açıp kurcalamıyorsunuz) ama genişlemeye açıktır (istediğiniz kadar yeni cihaz ekleyebiliyorsunuz). Bunu mümkün kılan, herkesin uyduğu ortak bir standart: USB.
Priz örneği
Evinizdeki duvar prizine buzdolabı, ütü, şarj aleti takabilirsiniz. Yeni bir cihaz aldığınızda elektrikçi çağırıp duvarı kırmazsınız. Priz bir kez yapılır; ona uyan her şey çalışır.
Teknik Anlamı
Bir yazılımcının bakış açısıyla
Bertrand Meyer tarafından ortaya konan ve Robert C. Martin tarafından popülerleştirilen bu prensibe göre, yeni bir davranış eklemek için mevcut kodu değiştirmek yerine yeni kod eklemek gerekir. Bu genellikle soyutlama (interface / abstract class) ve polimorfizm ile sağlanır.
Neden önemli?
- Çalışan ve test edilmiş koda dokunmak, regresyon hatası riski taşır.
- Her yeni gereksinimde aynı sınıfı değiştirmek, o sınıfı sürekli büyüyen ve kırılgan bir yapıya dönüştürür.
- Takım çalışmasında aynı dosyayı herkes değiştirdiği için merge conflict'ler artar.
İhlal belirtileri
- Tip kontrolü yapan, büyüyen
switch/if-else ifzincirleri:if (type == "A") ... else if (type == "B") ... - Yeni bir tür eklendiğinde kodda "bu türü nerelerde kontrol ediyorduk?" diye arama yapmak zorunda kalmak.
Nasıl uygulanır?
- Değişen davranışı bir arayüzün arkasına alın (Strategy deseni).
- Algoritma iskeleti sabitse Template Method, davranış eklemek için Decorator kullanın.
- Eklenti (plugin) mimarileri, ASP.NET Core middleware'leri ve DI ile kayıt edilen
IEnumerable<IHandler>koleksiyonları bu prensibin uygulamalarıdır.
C# ile Uygulama
Adım adım, çalışan kod örneği
Senaryo: Bir e-ticaret sitesinde müşteri tipine göre indirim hesaplama. Pazarlama ekibi sık sık yeni indirim türleri istiyor.
Adım 1 YANLIŞ Her yeni indirim için sınıfı değiştirmek
"Öğrenci indirimi" eklemek istediğimizde bu metodu açıp yeni bir case yazmak zorundayız. Her değişiklik, mevcut indirimleri bozma riski taşır.
public class DiscountCalculator
{
public decimal Calculate(string customerType, decimal amount)
{
switch (customerType)
{
case "Normal": return amount;
case "Gold": return amount * 0.90m;
case "Premium": return amount * 0.80m;
// Yeni tür = bu sınıfı tekrar açıp değiştirmek!
default: throw new ArgumentException("Bilinmeyen müşteri tipi");
}
}
}Adım 2 DOĞRU Soyutlama: genişleme noktası
Değişen kısım (indirim kuralı) bir arayüzün arkasına alındı.
public interface IDiscountPolicy
{
string Name { get; }
bool AppliesTo(Customer customer);
decimal Apply(decimal amount);
}
public record Customer(string Name, string Type, bool IsStudent = false, int Years = 0);Adım 3 DOĞRU Mevcut politikalar
public class GoldDiscount : IDiscountPolicy
{
public string Name => "Gold %10";
public bool AppliesTo(Customer c) => c.Type == "Gold";
public decimal Apply(decimal amount) => amount * 0.90m;
}
public class PremiumDiscount : IDiscountPolicy
{
public string Name => "Premium %20";
public bool AppliesTo(Customer c) => c.Type == "Premium";
public decimal Apply(decimal amount) => amount * 0.80m;
}Adım 4 DOĞRU Değişikliğe kapalı hesaplayıcı
Bu sınıf bir daha değişmeyecek. Politikalar ona dışarıdan verilir; en avantajlı olanı seçer.
public class PriceCalculator
{
private readonly IEnumerable<IDiscountPolicy> _policies;
public PriceCalculator(IEnumerable<IDiscountPolicy> policies) => _policies = policies;
public void PrintPrice(Customer customer, decimal amount)
{
var best = _policies
.Where(p => p.AppliesTo(customer))
.OrderBy(p => p.Apply(amount))
.FirstOrDefault();
decimal final = best?.Apply(amount) ?? amount;
Console.WriteLine($" {customer.Name,-6} ({best?.Name ?? "indirim yok",-14}) " +
$"-> {final,8:N2} TL");
}
}Adım 5 DOĞRU Yeni gereksinim: SADECE yeni sınıf ekliyoruz
Pazarlama "öğrenci" ve "sadakat" indirimi istedi. Mevcut hiçbir dosyaya dokunmadan iki yeni sınıf ekledik.
public class StudentDiscount : IDiscountPolicy
{
public string Name => "Öğrenci %25";
public bool AppliesTo(Customer c) => c.IsStudent;
public decimal Apply(decimal amount) => amount * 0.75m;
}
public class LoyaltyDiscount : IDiscountPolicy
{
public string Name => "Sadakat -150TL";
public bool AppliesTo(Customer c) => c.Years >= 5;
public decimal Apply(decimal amount) => Math.Max(0, amount - 150m);
}Adım 6 Kullanım
Tüm parçaları bir araya getiren istemci (client) kodu:
var policies = new List<IDiscountPolicy>
{
new GoldDiscount(), new PremiumDiscount(),
new StudentDiscount(), new LoyaltyDiscount(), // sonradan eklenenler
};
var calculator = new PriceCalculator(policies);
var customers = new[]
{
new Customer("Ali", "Normal"),
new Customer("Ayşe", "Gold"),
new Customer("Can", "Normal", IsStudent: true),
new Customer("Derya", "Gold", Years: 7),
};
foreach (var c in customers)
calculator.PrintPrice(c, 1000m);
var oldWay = new Kotu.DiscountCalculator().Calculate("Gold", 1000m);
Console.WriteLine($"Eski yöntem (Gold): {oldWay:N2} TL");Konsol çıktısı
Ali (indirim yok ) -> 1.000,00 TL Ayşe (Gold %10 ) -> 900,00 TL Can (Öğrenci %25 ) -> 750,00 TL Derya (Sadakat -150TL) -> 850,00 TL Eski yöntem (Gold): 900,00 TL
Kodda dikkat edilmesi gerekenler
PriceCalculatoryazıldıktan sonra hiç değişmedi; yeni indirimler ona "takıldı".- Gerçek bir ASP.NET Core uygulamasında politikalar DI container'a kaydedilir (
services.AddScoped<IDiscountPolicy, StudentDiscount>()) veIEnumerable<IDiscountPolicy>olarak otomatik enjekte edilir. - Bu yapı aslında Strategy deseninin bir uygulamasıdır.