SOLID Prensipleri
Liskov Substitution Principle
L — Liskov Yerine Geçme Prensibi
Amaç: Alt sınıflar, üst sınıflarının yerine sorunsuzca kullanılabilmelidir. Üst sınıfla çalışan kod, alt sınıf verildiğinde de doğru çalışmaya devam etmelidir.
Basit Anlatım
Teknik terim yok — günlük hayattan bir benzetme
Bir araç kiralama şirketinden "bir otomobil" kiraladınız. Size hangi marka ve model gelirse gelsin, şunları beklersiniz: direksiyonu çevirince döner, frene basınca durur, gaza basınca hızlanır.
Şimdi düşünün: size "oyuncak araba" verdiler. Evet, o da bir "araba"; ama içine binemezsiniz. Ya da size verilen araç frene basınca hızlanıyor. Kâğıt üzerinde araba ama beklediğiniz gibi davranmıyor. İşte bu Liskov ihlalidir.
Ördek testi
"Ördek gibi görünüyor, ördek gibi vaklıyor, ama pil istiyorsa… muhtemelen yanlış soyutlamaya sahipsiniz." Bir oyuncak ördek, ördek gibi görünür ama gerçek bir ördeğin yerine geçemez: göle bırakırsanız yüzmez, acıkmaz, pili biter.
Teknik Anlamı
Bir yazılımcının bakış açısıyla
Barbara Liskov'un 1987'de tanımladığı prensibe göre: S, T'nin bir alt tipiyse, T tipindeki nesneler programın doğruluğu bozulmadan S tipindeki nesnelerle değiştirilebilmelidir.
Kalıtım "is-a" (bir türüdür) ilişkisi kurar. Ancak gerçek dünyadaki "is-a" ilişkisi, kodda davranışsal olarak her zaman geçerli değildir. Matematikte her kare bir dikdörtgendir; ama kodda Square : Rectangle yazmak LSP'yi ihlal eder.
Alt sınıfın uyması gereken kurallar (Design by Contract)
- Ön koşulları güçlendiremez: Üst sınıf her pozitif sayıyı kabul ediyorsa, alt sınıf "sadece 100'den büyük" diyemez.
- Son koşulları zayıflatamaz: Üst sınıf "sonuç asla null değildir" diyorsa alt sınıf null döndüremez.
- Değişmezleri (invariant) korumalıdır: Üst sınıfın garanti ettiği durumlar alt sınıfta da geçerli olmalıdır.
- Beklenmeyen istisna fırlatamaz: Üst sınıfın metodunu override edip
NotImplementedException/NotSupportedExceptionfırlatmak klasik bir ihlaldir.
İhlal belirtileri
- Alt sınıfta override edilmiş ama boş bırakılmış ya da istisna fırlatan metotlar.
- İstemci kodunda
if (shape is Square)gibi tip kontrolleri.
.NET'ten gerçek bir örnek
Diziler IList<T> arayüzünü uygular; ama ((IList<int>)new int[3]).Add(5) çağrısı NotSupportedException fırlatır. IList<T> ile çalışan bir metot, dizi verildiğinde beklenmedik şekilde çöker. Bu, framework'teki bilinen bir LSP ihlalidir.
C# ile Uygulama
Adım adım, çalışan kod örneği
Senaryo: Meşhur Dikdörtgen–Kare problemi. Önce ihlali ve neden olduğu hatayı göreceğiz, sonra doğru tasarımı.
Adım 1 YANLIŞ Kare, dikdörtgenden türetiliyor
Kare her iki kenarı eşit tutmak için setter'ları override ediyor. Matematiksel olarak mantıklı görünüyor, ama dikdörtgenin davranış sözleşmesini bozuyor: "Genişliği değiştirmek yüksekliği etkilemez."
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; } // sürpriz yan etki!
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
public static class AreaTests
{
// Rectangle için yazılmış, tamamen makul bir metot:
public static void Resize(Rectangle r)
{
r.Width = 5;
r.Height = 4;
string result = r.Area == 20 ? "DOĞRU" : "HATALI!";
Console.WriteLine($" {r.GetType().Name,-9} alan = {r.Area,2} (beklenen 20) -> {result}");
}
}Adım 2 DOĞRU Ortak soyutlama, bağımsız şekiller
Kare ve dikdörtgen birbirinden türemiyor; ikisi de IShape sözleşmesini eksiksiz yerine getiriyor. Nesneler immutable olduğu için "bir kenarı değiştirince diğeri de değişti" gibi sürprizler mümkün değil.
public interface IShape
{
string Name { get; }
double Area { get; }
}
public sealed record Rectangle(double Width, double Height) : IShape
{
public string Name => $"Dikdörtgen {Width}x{Height}";
public double Area => Width * Height;
}
public sealed record Square(double Side) : IShape
{
public string Name => $"Kare {Side}x{Side}";
public double Area => Side * Side;
}
public sealed record Circle(double Radius) : IShape
{
public string Name => $"Daire r={Radius}";
public double Area => Math.PI * Radius * Radius;
}Adım 3 DOĞRU İstemci: her IShape'e güvenle çalışır
public static class ShapeReport
{
public static void Print(IEnumerable<IShape> shapes)
{
foreach (var s in shapes)
Console.WriteLine($" {s.Name,-16} alan = {s.Area,7:0.00}");
Console.WriteLine($" Toplam alan: {shapes.Sum(s => s.Area):0.00}");
}
}Adım 4 Kullanım
Tüm parçaları bir araya getiren istemci (client) kodu:
Console.WriteLine("İhlal eden tasarım:");
Kotu.AreaTests.Resize(new Kotu.Rectangle());
Kotu.AreaTests.Resize(new Kotu.Square()); // Rectangle yerine geçemedi!
Console.WriteLine("\nDoğru tasarım:");
ShapeReport.Print(new IShape[] { new Rectangle(5, 4), new Square(4), new Circle(1) });
Console.WriteLine("\n.NET'teki gerçek ihlal:");
IList<int> list = new int[3];
try { list.Add(5); }
catch (NotSupportedException) { Console.WriteLine(" int[] bir IList ama Add() desteklemiyor!"); }Konsol çıktısı
İhlal eden tasarım: Rectangle alan = 20 (beklenen 20) -> DOĞRU Square alan = 16 (beklenen 20) -> HATALI! Doğru tasarım: Dikdörtgen 5x4 alan = 20,00 Kare 4x4 alan = 16,00 Daire r=1 alan = 3,14 Toplam alan: 39,14 .NET'teki gerçek ihlal: int[] bir IList ama Add() desteklemiyor!
Kodda dikkat edilmesi gerekenler
Resizemetodu hiçbir yanlış yapmadı; sadeceRectangle'ın vaat ettiği davranışa güvendi. Hatanın kaynağı, sözleşmeyi bozanSquarealt sınıfı.- LSP'nin ana mesajı: kalıtımı "gerçek hayatta X bir Y'dir" diye değil, "X her durumda Y gibi davranabilir mi?" diye sorarak kurun.
- Emin değilseniz kalıtım yerine composition ya da ortak arayüz tercih edin.