SOLID Prensipleri Nelerdir?

Yazılım geliştirmede geliştiriciler tarafından dikkat edilmesi gereken çeşitli hususlar mevcuttur. Nesne yönelimli programlama kapsamınca ele alınabilecek hususlardan biri de Solid prensipleridir. Peki solid prensipleri nedir? Solid prensiplerinin uygulanmasındaki amaç nedir? Solid prensiplerinin yazılım uygulamalarına kattığı yararlar nelerdir?

Solid prensipleri, geliştirilmekte olan yazılım uygulamalarına esneklik kazandıran, kodların yeniden kullanılabilirliğini sağlayan ve anlaşılır olmasında etkili olan bir disiplindir. Kod tekrarını önleyerek sürdürülebilir kod yazma alışkanlığını da yazılım geliştiricilere kazandıran solid prensipleri yazılım uygulamasının verimliliğini de arttırmaktadır. 

SOLID Prensipleri Yararları Nedir?

Özellikle nesne yönelimli programlama dillerinde uygulama geliştirirken solid prensiplerinden yararlanarak uygulamaların daha esnek, yeniden kullanılabilir kodlara sahip olmasını ve anlaşılırlığın yüksek olmasını sağlayabilirsiniz. Solid prensiplerinin gerek yazılım geliştiricileri için gerekse de yazılım uygulamaları için yararları genel olarak aşağıdaki gibidir:

  • Kod tekrarlarının önüne geçilir
  • Daha temiz ve yeniden kullanılabilir kodların yazılması sağlanır
  • Yazılım uygulamasının daha hızlı çalışması sağlanır
  • Esnek çalışma prensibine sahip yazılım sistemleri yazılabilir
  • Kod karmaşıklığı önlenmiş olur
  • Daha okunabilir kod yazılabilir

Solid prensiplerinin hem geliştiriciler hem de yazılım uygulaması için başlıca yararları bunlar olup daha da arttırılabilir. Son derece yararlı, projelere esneklik kazandıran ve kod tekrarının önüne geçerek hem hız hem de verimlilik açısından avantajlı olan solid prensiplerini temelde beş farklı başlıkta ele alabiliriz. Başlıkları sırasıyla inceleyelim.

4 Başlıkta Solid Prensipleri Nedir?

Solid prensiplerini 4 başlık altında toplayabiliriz. Daha anlaşılır kolay okunan kod yazmanızı sağlayan Solid prensipleri şunlardır:

  • Single Responsibility Principle (Tek Sorumluluk Prensibi)
  • Open/Closed Principle (Açık Kapalı Tasarım Prensibi)
  • Liskov ‘s Substitution Principle (Liskov Yerine Geçme Prensibi)
  • Interface Segregation Principle (Arayüz Ayırım Prensibi)
  • Dependency Inversion Principle

Single Responsibility Principle – Tek Sorumluluk Prensibi

Tek sorumluluk prensibi olarak adlandırılan single responsibility principle, binlerce satır kod yazmanın önüne geçen bir standarttır.

C# ve Java gibi nesne yönelimli programlamayı destekleyen programlama dillerinde uygulama geliştirirken dikkat edilmesi gereken bu yapıda her oluşturulan sınıf veya metodun tek bir görevi mevcuttur.

Bir sınıfın veya metodun birden fazla görevi olmamakla birlikte zaman içerisinde yazılım uygulamasının genişleyebilmesi, tekrar kullanılabilir tasarımının oluşturulması ve test edilebilir yapıların kurulması da oldukça kolaydır.

Tek sorumluluk prensibi ilkesine göre bir sınıf sadece kendini ilgilendiren işi yapmaktadır. Günlük hayattan örnek vermek gerekirse bir market çalışanını düşünelim. Bu çalışana hem kasaya bakma hem de depo sorumluluğu görevi verilirse o çalışan işlere yetişemez, işlerinde aksama yapar ve dolayısıyla verimlilik de düşer. Yazılım sistemlerini de böyle düşünebiliriz. 

Oluşturulan her bir sınıf ve metodun sadece bir sorumluluğu yerine getirme görevini nesne yönelimli programlama disiplini olarak single responsibility principle yapısında görebiliriz. Tek bir sorumluluğu üstlenen sınıf ya da metodun değiştirilmesi için yalnızca bir sebep olacaktır. İkinci bir sebebe yer vermeyen bu sistem sayesinde yazılım uygulamalarında yük dağılımı daha esnek olacak, kod kalitesi de artacaktır.

Tek sorumluluk prensibiyle alakalı örnek vermek gerekirse, bir banka uygulamasında transfer yapılma senaryosu düşünelim. Transfer yaparken bakiyenin yeterliliği ve transferin gerçekleşme aşaması tek sorumluluk prensibine göre farklı metotlarda, farklı sınıflarda olması gerekir.

Örnek 1: 

public class ParaTransferi
{
    public decimal IslemTutari { get; set; }

    public bool BakiyeKontrol()
    {
        return true;
    }

    public bool TransferYap()
    {
        return true;
    }
}

Tek sorumluluk prensibine aykırı olan yukarıdaki kod parçasında ParaTransferi sınıfında hem bakiye kontrolünün hem de transfer işleminin bir arada tanımlandığı görülmektedir. Bu işlemin tek sorumluluk prensibine uygun olması için aşağıdaki yolun izlenmesinde yarar vardır:

class Bakiye

{
    
 public bool BakiyeKontrol()
    
 {
        
   return true;
    
 }

}


class TransferiTamamla

{
    
 public bool Transfer()
    
 {
        
   return true;
    
 }

}

Bakiye ve TransferiTamamla adında iki farklı sınıf oluşturularak her bir sınıf içerisine sadece kendisiyle alakalı metotlar tanımlanmıştır. Bu sayede ilgili işlemde sonradan değiştirilecek tüm her şey için ilgili sınıfa gidilerek güncelleme yapılması gerekir. 

Open/Closed Principle – Açık Kapalı Tasarım Prensibi

Open/Closed, bir diğer ifadeyle açık kapalı tasarım prensibi ise yazılım geliştirirken kullanılan sınıf ya da metotların gelişime açık, değişime kapalı olarak tasarlanması ilkesidir. 

Açık Kapalı sistemine örnek olarak dijital bankacılık uygulamalarındaki otomatik ödeme talimatlarını verebiliriz.

Otomatik ödeme talimatları kendi içerisinde farklı işlevselliklere sahiptir. Fatura, kurum, transfer ödemeleri gibi farklı ödeme talimatlarının verilebileceği sistem düşünüldüğünde aşağıdaki işlemler açık kapalı prensibine aykırı olacaktır.

Örnek:

public class FaturaOtomatikOdemeTalimati //Fatura ödemeleri için yazılmış sınıf
{
    public bool FaturaOtomatikOdeme()
    {
        return true;
    }
}
public class KurumOtomatikOdemeTalimati //Kurum ödemeleri için yazılmış sınıf
{
    public bool KurumOtomatikOdeme()
    {
        return true;
    }
}
public class TransferOtomatikOdemeTalimati //Transfer ödemeleri için yazılmış sınıf
{
    public bool TransferOtomatikOdeme()
    {
        return true;
    }
}
public class OtomatikOdemeTalimati //Otomatik ödeme talimatlarının tümü için yazılmış bir sınıf
{
    public FaturaOtomatikOdemeTalimati _faturaOtomatikOdemeTalimati;
    public KurumOtomatikOdemeTalimati _kurumOtomatikOdemeTalimati;
    public TransferOtomatikOdemeTalimati _transferOtomatikOdemeTalimati;

    public OtomatikOdemeTalimati(FaturaOtomatikOdemeTalimati faturaOtomatikOdemeTalimati,
        KurumOtomatikOdemeTalimati kurumOtomatikOdemeTalimati,
        TransferOtomatikOdemeTalimati transferOtomatikOdemeTalimati)
    {
        _faturaOtomatikOdemeTalimati = faturaOtomatikOdemeTalimati;
        _kurumOtomatikOdemeTalimati = kurumOtomatikOdemeTalimati;
        _transferOtomatikOdemeTalimati = transferOtomatikOdemeTalimati;
    }

    public enum Talimat
    {
        fatura,
        kurum,
        transfer
    }

    public void OtomatikTalimatVer(Talimat talimat)
    {
        switch (talimat)
        {
            case Talimat.fatura:
                _faturaOtomatikOdemeTalimati.FaturaOtomatikOdeme();
                break;
            case Talimat.kurum:
                _kurumOtomatikOdemeTalimati.KurumOtomatikOdeme();
                break;
            case Talimat.transfer:
                _transferOtomatikOdemeTalimati.TransferOtomatikOdeme();
                break;
        }
    }
}

Yukarıdaki işlemler ele alındığında otomatik ödeme talimatlarını yeni bir tanımlama gelecek olursa OtomatikOdeme
Talimati sınıfı içerisinde değişiklik yapılması gerekmektedir.

Değişikliğin yapılması ve bağımlılığın söz konusu
olması yazılım uygulamasının solid prensiplerine aykırı davrandığını da gösterir.

Değişime kapalı ve gelişime açık olacak şekilde ilgili modülün aşağıdaki gibi yazılması gerekir:

Öncelikle bağımlılıktan kurtulmak için Interface tanımlanması ve ortak bir metodun sınıf içerisinde tanımlanması
gerekir.

Ardından ise Interface’den türeyecek olan sınıfların oluşturularak metotlara atıf yapılması gerekir.

public interface IOtomatikOdemeTalimati
{
    public bool TalimatVer();
}

class FaturaTalimati : IOtomatikOdemeTalimati
{
    public bool TalimatVer()
    {
        Console.WriteLine("Fatura");
        return true;
    }
}

class TransferTalimati : IOtomatikOdemeTalimati
{
    public bool TalimatVer()
    {
        Console.WriteLine("Transfer");
        return true;
    }
}

class KurumTalimati : IOtomatikOdemeTalimati
{
    public bool TalimatVer()
    {
        Console.WriteLine("Kurum");
        return true;
    }
}

Yukarıdaki kod örneği bir Interface’den türetilmiş sınıfların davranışını göstermektedir.

Bu sayede bir ana merkez üzerinden yüzlerce sınıf oluşturarak işlemleri tek elden yönetmek mümkündür. İşlemin yapılacağı asıl sınıfta ise sadece Interface çağrılarak metoda erişim sağlanabilir.

class OtomatikOdeIslem
{
    public IOtomatikOdemeTalimati _otomatikOdemeTalimati;

    public OtomatikOdeIslem(IOtomatikOdemeTalimati otomatikOdemeTalimati)
    {
        _otomatikOdemeTalimati = otomatikOdemeTalimati;
    }

    public void islemYap()
    {
        _otomatikOdemeTalimati.TalimatVer();
    }
}


Görüldüğü üzere X bir ödeme talimatı gelse bile OtomatikOdeIslem sınıfında değişiklik yapılmayacak sadece X ödeme talimatının sisteme entegre edilmesi, yani geliştirilmesi, gerekecektir. Değişime kapalı, gelişime açık ve bağımlılıktan uzak bir modülün tasarlanması ile birlikte solid prensiplerine uygun şekilde örneğimizi tamamladık.

Liskov ‘s Substitution Principle

Solid prensipleri arasında yer alan Liskov yerine geçme prensibi oluşturulan alt sınıf nesnelerinin üst sınıf nesneleri ile yer değiştirdiğinde aynı davranması ilkesine dayanır.

Türetilen sınıfların türeyen sınıflardaki tüm özellikleri kullanma zorunluluğu taşıyan Liskov’un yerine geçme prensibinde alt sınıf üst sınıftaki bir nesneyi kullanmak istemese dahi kullanmak zorunda kalacağı için dummy data yazılması gerekir.

Liskov’un yerine geçme prensibi örneği verecek olursak:

Örnek : 

public interface ICanlilar
{
    public bool Yuru();
    public bool Yuz();
    public bool Uc();
}
public class Balik : ICanlilar
{
    public bool Yuru()
    {
        return false;
    }

    public bool Yuz()
    {
        return true;
    }

    public bool Uc()
    {
        return false;
    }
}

public class Kedi : ICanlilar
{
    public bool Yuru()
    {
        return true;
    }

    public bool Yuz()
    {
        return false;
    }

    public bool Uc()
    {
        return false;
    }
}


ICanlilar adında bir Interface içerisine Yuz, Yuru ve Uc şeklinde üç farklı fonksiyon tanımlanmıştır. Kedi ve Balik
adında iki farklı sınıf ICanlilar sınıfını implement ettiği için tüm özellikleri kendi içerisinde göstermek
zorundadır.

Dolayısıyla Balik sınıfı içerisinde Uc ve Yuru fonksiyonları dummy data olarak false atanmıştır.
Aynı şekilde Kedi sınıfında da Uc ve Yuz fonksiyonları dummy data olarak false atanmıştır. Bu gibi yapılar ile
Liskov’un yerine geçme prensibi uygulanabilir.

Interface Segregation Principle

Interface Segregation Principle, yani arayüz ayırım prensibi, bir arayüze gerektiğinden fazla özellik yüklenmemesi ilkesine dayanmaktadır.

Liskov’un yerine geçme prensibindeki örneği arayüz ayırım prensibine çevirirsek Kedi ve Balik sınıflarının sadece kendisinin kullanabileceği fonksiyonları almasında, dolayısıyla dummy data kullanmanın da önüne geçilmesinde etkili olunur.

Örnek :

public interface ICanlilar 
{
    public bool Hareket();
}

public interface IYuzenCanlilar : ICanlilar
{
    public bool Yuz();
}

public interface IYuruyenCanlilar : ICanlilar
{
    public bool Yuru();
}


ICanlilar sınıfı içerisinde tüm canlılar için ortak olan Hareket fonksiyonunu ekledik. IYuzenCanlilar sınıfı
Yuz metodunu kullanmakta ve aynı zamanda ICanlilar sınıfından da türemektedir. IYuruyenCanlilar sınıfı ise
içerisinde Yuru metoduna yer vermekte ve yine ICanlilar sınıfından türemektedir.

public class Balik : IYuzenCanlilar
{
    public bool Hareket()
    {
        return true;
    }

    public bool Yuz()
    {
        return true;
    }
}

public class Kedi : IYuruyenCanlilar
{
    public bool Hareket()
    {
        return true;
    }

    public bool Yuru()
    {
        return true;
    }
}



Liskov’un yerine koyma prensibindeki gibi Kedi ve Balik sınıfları bu sefer ICanlilar sınıfından türeyen sırasıyla IYuzenCanlilar ve IYuruyenCanlilar sınıflarını implement etmişlerdir. Böylelikle Balik sınıfı içerisinde Yuru metoduna, Kedi sınıfı içerisinde ise Yuz metoduna yer verilmemiş, dummy data basılmasına da gerek kalmamıştır.

Arayüz ayırım prensibinin en önemli mantıklarından biri ortak durumların en tepe Intarface’de toplanması, ayrım yapılması istenen her bir metot ya da nesnenin ise farklı Interfacelerde, ana sınıftan türetilerek toplanmasıdır.

Oluşturulacak somut sınıflarda ise hangi özelliklerin olması isteniyorsa ilgili alt Interfacelerden implement edilmesi en doğru yaklaşım olacaktır.

Dependency Inversion Principle

Bağımlılığın ters çevrilmesi ilkesine göre üst sınıfların, metotların veya modüllerin alt sınıflara bağımlı olmaması gerekir. Alt sınıflarda yapılan herhangi bir değişikliğin üst sınıfı etkilememesi gerekir.

Open/Closed temel prensibinde verdiğimiz örnek bağımlılığın ters çevrilmesini de kapsamaktadır. Ancak konunun anlaşılması için bir kısa örnek vermek gerekirse:

Örnek: 

public class Email
{
    public void send()
    {
        Console.WriteLine("Mail");
    }
}

public class Sms
{
    public void send()
    {
        Console.WriteLine("Sms");
    }
}

class MesajGonder
{
    public Email _email;
    public Sms _Sms;

    public MesajGonder(Email email, Sms sms)
    {
        _email = email;
        _Sms = sms;
    }

    void smsIleti()
    {
        _Sms.send();
    }

    void mailIleti()
    {
        _email.send();
    }

Aynı görevi gören ancak Email ve Sms adında açılan iki farklı sınıfı direkt bağımlılık gösterecek şekilde MesajGonder sınıfı içerisinde yer verilmiş olup bağımlılıkların tersine çevrilmesi ilkesine aykırı hareket gösterilmiştir.

Örneği solid prensiplerine uygun hale getirmek, bağımlılıkları ortadan kaldırmak için ISend şeklinde bir Interface tanımlanabilir ve MesajGonder sınıfı içerisinde ISend sınıfı çağrılabilir. Email ve Sms sınıfları ISend sınıfını implement etmelidir.

public interface ISend
{
    public bool Send();
}
public class Email : ISend
{
    public bool Send()
    {
        return true;
    }
}

public class Sms : ISend
{
    public bool Send()
    {
        return true;
    }
}

class MesajGonder
{
    public ISend _send;

    public MesajGonder(ISend send)
    {
        _send = send;
    }

    void ileti()
    {
        _send.Send();
    }

Görüldüğü üzere MesajGonder sınıfında Email, Sms veya sonra eklenebilecek her hangi bir sınıfın çağrılmasına gerek kalmadan direkt olarak ISend sınıfı çağrılarak bağımlılıkların önüne geçilmiştir.

Solid prensiplerinin 5 temel ilkesini örnekler halinde anlatmaya çalıştık. Yazılım uygulamalarında mutlaka yer verilmesi gereken solid prensipleri ile daha esnek, temiz ve tekrar kullanılabilir kodların yazılması mümkündür.

Nesne yönelimli programlama disiplini gereği olarak solid prensiplerinin uygulanması uzun vadede değişikliklerin olmasının da projeye ek maliyetinin önüne geçecektir.

Olası değişikliklerde zamandan tasarrufu da sağlayacak olan solid prensiplerinin yazılım geliştiricileri tarafından önemle dikkat edilmesi ve projelerde uygulamaları gereklidir.

Kod okunabilirliğini de arttıran bu yöntem sayesinde yazılım geliştiricileri kendi içerisinde clean code kültürünü de bir hayli geliştirecektir.

Yorum yapın