SOLID nedir ?

SOLID nedir ?

Her yazılımcının bilmesi gereken SOLID Prensipleri hakkında konuşacağız. SOLID, Robert C. Martin tarafından ortaya atılmış nesne yönelimli programlamanın 5 prensibinin baş harflerinden oluşan bir kısaltmadır. İlk defa Michael Feathers tarafından ortaya atılmıştır.

Bu prensiplerde,

  • Gelecekte yapılacak geliştirmelere rahatlıkla adapte olabilmesi
  • Geliştirme yaparken yazılmış yerleri değiştirmeden yeni eklemelere açık olması
  • Anlaşılır kod yazılması
  • Değişiklik yapılırken bir yerlerde istemeden çıkan hataların önüne geçilmesi

amaçlanmıştır. Temiz kod yazabilmek için bu prensiplere bağlı kalmalıyız.

Single Responsibility Principle

Bu prensibe göre metot veya sınıf sadece ve sadece tek bir işi yapabilmesi gerekir. Bunu yapabilmesi için “Bu işlemi burada mı yapmalıyım?”, “Bu metot içerisinde birden fazla iş yapılıyor mu?” gibi soruları sormalıyız. Örnek vermek gerekirse bir üye olma metodu içerisinde mailde atılıyorsa bunu tek metot da yazmak yerine mail atma işlemi için ayrı bir metot, üyelik işlemi için ayrı bir metot yazarak bu prensibe uymuş oluruz. Aynı yapı sınıf içinde geçerlidir.

Open-Closed Principle

Bir metot veya sınıfın yeni davranışlar eklenmesine açık ancak temel yapısındaki değişikliklere kapalı olmasını, var olan yapının bozulmamasını gerektiren prensiptir. Open yeni geliştirmelere açık olması, Closed değiştirmeye kapalı olması anlamına gelir. Bu prensibin amacı geliştirilmiş yapıyı bozmadan istediğimiz şekilde yeni eklemeler yaparak sürekli yenilenmeye elverişli kodlar yazabilmektir.

Bu prensip; sürdürülebilir ve tekrar kullanılabilir yapıda kod yazmanın temelini oluşturur.

Robert C. Martin

Örnek olarak bu prensibi sağlayabilmek için OOP prensiplerini kullanıp birden fazla sınıf için ortak olan metotları abstract sınıf veya interface tanımlayarak diğer sınıflardaki metotları override edebiliriz. Bu şekilde yeni geliştirmelere açık hale getirmiş oluruz. Aşağıdaki örnekte setPaymentType() her sınıf için kendine özgü çalışmaktadır. Yeni bir ödeme tipini kodda bir değişiklik yapmadan ekleyebileceğiz.

public static void main(String[] args) {
        String input = null;
        if(input.equals("creditCard")){
           Payment payment = new Payment();
           payment.setPaymentType("creditCard");
           payment.pay();
        }else if(input.equals("debitCard")){        //Bu kullanım kötü bir kullanımdır.
            Payment payment = new Payment();        //Yeni bir ödeme tipi eklediğimizde
            payment.setPaymentType("debitCard");    //bu if-else yapısında değişiklikler
            payment.pay();                          //yapılması gerekecek.
        }
    }

    public class Payment{
        private String msg;
        public void pay(){
            System.out.println(msg +" ile ödeme yapıldı.");
        }
        public void setPaymentType(String msg){
            this.msg = msg;
        };
    }

Burada kötü bir kullanım için örnek verdik. Yapacağımız bazı değişikliklerle bu prensibe uygun hale getirebiliriz.

public static void main(String[] args) {
        Payment paymentCreditCard = new CreditCard();
        paymentCreditCard.setPaymentType();
        paymentCreditCard.pay();

        Payment paymentDebitCard = new DebitCard();   //Bu tanımlama ile yeni eklenecek tiplerde
        paymentDebitCard.setPaymentType();            //varolan kodda hiçbir değişiklik yapmadan
        paymentDebitCard.pay();                       //sadece yeni eklemeler ile 
                                                      //geliştirme yapabiliriz.
//        Payment paymentEasyCard = new EasyCard();
//        paymentDebitCard.setPaymentType();
//        paymentDebitCard.pay();
    }

    public abstract class Payment {

        public String msg;

        public void pay() {
            System.out.println(msg + " ile ödeme yapıldı.");
        }

        public abstract void setPaymentType();

    }

    public class CreditCard extends Payment {
        @Override
        public void setPaymentType() {
            this.msg = "Credit Card";
        }
    }

    public class DebitCard extends Payment {
        @Override
        public void setPaymentType() {
            this.msg = "Debit Card";
        }
    }

//    public class EasyCard extends Payment {
//        @Override
//        public void setPaymentType() {
//            this.msg = "Easy Card";
//        }
//    }

Liskov Substitution Principle

Bildiğimiz üzere kalıtım ile alt ve üst sınıf dediğimiz bir hiyerarşide yapılar oluşturabiliriz. Bu prensibin de amacı alt sınıftan bir nesne ile üst sınıfın nesnesi yer değiştirdiğinde çalışma mantığı anlamında hiç bir değişiklik olmamasını sağlayabilmektir. Bu prensibi sağlayabilmek için arayüzler(interface) kullanılmaktadır.

Örnek olarak bir üst sınıfımız olsun bir metot bu üst sınıftan türeyen tüm sınıflar için override edilerek kullanılması isteniyor. Gün geçtikçe yeni bir sınıf daha bu üst sınıfı miras alıyor ama kullanılması gereken metot burada kullanılması imkansız durumda mecbur override etmek zorundayız ne gibi çözüm bulacağız. Böyle bir durumda bu prensibi bozmuş bulunmaktayız.

public abstract class Ucak {            //Üst sınıf(base) 
        public abstract void kargoTasi();
        public abstract void roketAt();     
        public abstract void yangınSondur();
    }

    public class F16SavasUcagı extends Ucak {   //Alt sınıf
        @Override
        public void kargoTasi() {
        }

        @Override
        public void roketAt() {          //Savaş ucağı için bu metot mantıklı ancak
                                         //diğer metotlar bu sınıf içerisinde mantıksız
        }
        @Override
        public void yangınSondur() {
        }
    }

İşte çözüm olarak her bir override edilmesini istediğimiz davranışı bir interface olarak tanımlayarak istediğimiz sınıfa istediğimiz davranış verip istediğimiz sınıfa bu davranışı vermeyiz.

 public interface IKargoTasi {        //Her davranışı interface olarak tanımlayarak
        void KargoTasi();                //istediğimiz sınıf için implement edebiliriz.
    }

    public interface IRoketAt {
        void RoketAt();
    }

    public interface IYanginSondur {
        void YanginSondur();
    }

    public interface IHavalan {
        void havalan();
    }

    public class F16SavasUcagi implements IRoketAt,IHavalan {

        @Override
        public void RoketAt() {
        }

        @Override
        public void havalan() {
        }
    }

    public class C160DKargoUcagi implements IKargoTasi,IHavalan {
        
        @Override
        public void havalan() {
        }

        @Override
        public void KargoTasi() {
        }
    }

Interface Segregation Principle

Bir interface içerisinde tüm sorumlulukları toplamak yerine her interface’i özelleştirerek onunla bağdaşan sorumlulukları ve gereksiz bir sorumluluk kalmayacak şekilde sınıfa implement edilmesini isteyen prensiptir. Sınıf içerisine gereksiz özellik veya metot eklenmesi istenmeyen bir durumdur. Bu prensip için örnek göstermek gerekirse Liskov Substitution prensibinde verdiğimiz örnekte olduğu gibi kargo uçağına gereksiz yere yangınSondur() metodunun bulunmaması gerekir.

Dependency Inversion Principle

Son prensibimiz olan bu prensipte üst seviyeli sınıfların alt seviyeli sınıflara bağımlılığını ortadan kaldırarak her iki sınıfında soyut(abstract) kavramlara bağımlı olması hedeflenmektedir.

public class BackUp{            //Üst seviyeli sınıf
    public void write(){
        FileWriter fileWriter = new FileWriter();
        fileWriter.writeToFile();
    }
}

public class FileWriter{        //Alt seviyeli sınıf
    public void writeToFile(){
        System.out.println("Dosyaya yazıldı.");
    }
}

Öncelikli olarak yukarıda belirtilen kod parçası kötü kodu görmemiz içindir. Üst seviyeli sınıf olan yedekleme sınıfı dosyaya yazma sınıfına bağımlıdır. Zamanla yedekleme dosya yerine veritabanına olacak denildiğinde yüksek seviyeli sınıf üzerinde yapısal değişiklikler yapılması gerekecek. Bu prensibin amacı iki sınıf arasına soyut kavramlar eklemekti.

(üst sınıf + soyut nesne + alt sınıf)

Yapacağımız değişikliklerle bu prensibe uygun hale getirebiliriz.

public class BackUp{
    public void write(){
        IBackUp writer = new DatabaseWriter();      //Üst Seviye
        writer.writeTo();
    }
}

public interface IBackUp{                           //Soyut sınıf
    void writeTo();
}

public class FileWriter implements IBackUp{         //Alt seviye
    @Override
    public void writeTo() {
        System.out.println("Dosyaya yazıldı.");
    }
}

public class DatabaseWriter implements IBackUp{     //Alt seviye
    @Override
    public void writeTo() {
        System.out.println("Veritabanına yazıldı.");
    }
}

Sonuç olarak yüksek seviyeli sınıf ile alt seviye arasına interface koyarak prensibe bağlı kalmış olduk. İleride yedeklemeyi cloud bir sisteme yapılması istendiğinde yine aynı interface implement edilerek yüksek seviyeli sınıfta yapısal değişikliğe gerek kalmadan kullanabiliriz.

Solid prensiplerinin sonuncusunu da bitirmiş bulunmaktayız. Güzel kod yazmak sanattır. Bu prensiplerde onun bir parçasıdır.

Codest Blog

Codest Blog yazılım hakkında her konuda bilgi sağlayan bir blog sitesidir. Bilgi paylaştıkça güzeldir felsefesi ile hareket ederek, yazılımcıların en cok karşılaştığı konuları sizlere aktarmayı hedefliyoruz. Keyifli okumalar 🙂