Herkese merhaba. Uzun bir aradan sonra SOLID ilkesinin üçüncü prensibi ile bu yazı dizisine devam ediyorum. Liskov Substitution prensibi özetle kodlarımızda herhangi bir değişiklik yapmaya gerek duyulmadan türetilen sınıfların yerine kullanabilmesini ele alır. Bu sayede codebase içerisinde sağlam bir inheritance yapısı oluşur. Yanlış soyutlamaların önüne geçildiği için geliştirme yapılırken kodun tahmin edilebilirliği artar. Ayrıca bir önceki yazımda bahsettiğim Open Closed ilkesinin de öncül şartıdır.

Liskov Substitution Nedir?

LSP der ki: Bir alt sınıf, üst sınıfın beklediği davranışı bozmadan onun yerine geçebilmelidir. Yani bir sınıf miras aldığı üst (türetilen) sınıfın tüm özelliklerini ve davranışlarını eksiksiz ve doğru bir biçimde yapabilmelidir. Eğer alt (türeyen) sınıf, üst sınıfın yapabildiği bir şeyi yapamıyorsa veya yaparken hata veriyorsa orada bir tasarım hatası var demektir. Kısaca bu prensibin altın kuralı alt sınıfların, üst sınıflarının “yerine geçebilir” (substitutable) olmasıdır.

Bu prensip anlatılırken en çok kullanılan örneklerden birini inceleyelim. Elimizde 3 adet ördek nesnesi olsun. Bunlar; mallard duck (yeşilbaş ördek), marbled duck (yaz ördeği) ve rubber duck (plastik ördek) olsun. Tüm ördek türleri aynı temel sınıftan türeyeceği için önce ortak davranışları barındıran Duck sınıfını tanımlayalım.

public abstract class Duck
{
    public abstract void Quack();
}

public class MallardDuck : Duck
{
    public override void Quack()
    {
        Console.WriteLine("Quack!");
    }
}

public class MarbledDuck : Duck
{
    public override void Quack()
    {
        Console.WriteLine("Quack!");
    }
}

public class RubberDuck : Duck
{
    public override void Quack()
    {
        Console.WriteLine("Squeak!");
    }
}

Bütün ördekler (aynı şekilde olmasa da) ses çıkarma özelliğine sahip oldular. Şimdi ördeklerin bir başka özelliği olan yüzmeyi de örneğimize ekleyelim.

namespace _03_LiskovSubstitution.Ducks
{
    public abstract class Duck
    {
        public abstract void Quack();
        public abstract void Swim();
    }

    public class MallardDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Quack!");
        }

        public override void Swim()
        {
            Console.WriteLine("Swimming!");
        }
    }

    public class MarbledDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Quack!");
        }

        public override void Swim()
        {
            Console.WriteLine("Swimming!");
        }
    }

    public class RubberDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Squeak!");
        }

        public override void Swim()
        {
            Console.WriteLine("Floating!");
        }
    }
}

Bu özelliğimizde de gördüğümüz gibi her bir ördek türü farklı bir şekilde olsa da yüzme özelliğini doğru bir şekilde kullanıyor. Son olarak ördeklere bir de uçma özelliği eklememiz gerekir.

namespace _03_LiskovSubstitution.Ducks
{
    public abstract class Duck
    {
        public abstract void Quack();
        public abstract void Swim();
        public abstract void Fly();
    }

    public class MallardDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Quack!");
        }

        public override void Swim()
        {
            Console.WriteLine("Swimming!");
        }

        public override void Fly()
        {
            Console.WriteLine("Flying!");
        }
    }

    public class MarbledDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Quack!");
        }

        public override void Swim()
        {
            Console.WriteLine("Swimming!");
        }

        public override void Fly()
        {
            Console.WriteLine("Flying!");
        }
    }

    public class RubberDuck : Duck
    {
        public override void Quack()
        {
            Console.WriteLine("Squeak!");
        }

        public override void Swim()
        {
            Console.WriteLine("Floating!");
        }

        public override void Fly()
        {
            throw new NotImplementedException("Rubber ducks cannot fly!");
        }
    }
}

Plastik ördek uçamayacağı için Fly() metodunda bir hata fırlatmamız gerekiyor. Ancak bu Liskov Substitution prensibine doğrudan aykırı bir durumdur. Çünkü alt sınıf olan RubberDuck, üst sınıfın sunduğu davranışı yerine getirememektedir.

Aslında sorunun kökeninde RubberDuck sınıfının gerçek bir ördek olmaması yatar. Dolayısıyla Duck soyutlamasının gerektirdiği davranışları karşılamaz. Bu nedenle plastik ördeği diğer ördek türleriyle aynı sınıf hiyerarşisine yerleştirmek baştan hatalı bir tasarım tercihi olur.

Liskov Substitution Uygun Çözüm

LSP ihlali, alt sınıfın üst sınıfın beklediği yeteneği yerine getirememesiyle ortaya çıkar. Burada temel sorun Fly() metodunun “her ördek uçar” şeklinde tanımlanması, yanlış bir soyutlama seçimidir. Bu sorunu çözmek için ses çıkarmak, yüzmek ve uçmak gibi özellikleri ayrı interface’ler altında tanımlamalıyız. Böylece her bir özelliğin sadece tek bir sorumluluğu olur ve Single Responsibility prensibine uymuş oluruz.

public interface IFly
{
    void Fly();
}

public interface ISwim
{
    void Swim();
}

public interface IQuack
{
    void Quack();
}

public class MallardDuck : IFly, ISwim, IQuack
{
    public void Quack()
    {
        System.Console.WriteLine("Quack!");
    }

    public void Swim()
    {
        System.Console.WriteLine("Swimming!");
    }

    public void Fly()
    {
        System.Console.WriteLine("Flying!");
    }
}

public class MarbledDuck : IFly, ISwim, IQuack
{
    public void Quack()
    {
        System.Console.WriteLine("Quack!");
    }

    public void Swim()
    {
        System.Console.WriteLine("Swimming!");
    }

    public void Fly()
    {
        System.Console.WriteLine("Flying!");
    }
}

public class RubberDuck : ISwim, IQuack
{
    public void Quack()
    {
        System.Console.WriteLine("Squeak!");
    }

    public void Swim()
    {
        System.Console.WriteLine("Floating!");
    }
}

Bu sayede her ördek yapabildiği özelliğe sahip olmuş oldu. Burada dikkat edilmesi gereken önemli bir nokta var: LSP aslında OCP’nin uygulanabilmesi için bir ön koşuldur. Eğer alt sınıflar, üst sınıfın davranışlarını tam olarak yerine getiremiyor ve sözleşmeyi bozuyorsa, bu durum yalnızca LSP’yi değil, aynı zamanda OCP’yi de ihlal eder. Çünkü sınıfa yeni bir özellik eklediğinizde tüm alt sınıfları tekrar değiştirmek zorunda kalırsınız; bu da “değişikliğe kapalı” olma ilkesine aykırıdır.

Davranışları interface’lere bölmek ise hem LSP’yi hem OCP’yi aynı anda korur. Her sınıf yalnızca gerçekten sahip olduğu davranışları implemente eder, böylece yeni özellikler eklerken mevcut sınıfları değiştirmek zorunda kalmazsınız. Yani doğru soyutlama sayesinde alt sınıflar üst sınıfların yerine sorunsuzca geçer (LSP), proje de yeni sınıflar eklenerek genişleyebilir (OCP).

Github hesabımda biraz daha gerçekçi bir örneği tasarlamaya çalıştım. Onu da inceleyebilirsiniz. Bir sonraki yazımda Interface Segregation Prensibini inceleyeceğim. Bir sonraki yazıya kadar kendinize çok iyi bakın! 😊