Java SOLID Tasarım İlkesi – Tasarım Kodlama
Java

Java SOLID Tasarım İlkesi

Sınıflar, herhangi bir java uygulamasının yapı taşlarıdır. Bu bloklar sağlam değilse, bina (yani uygulama) gelecekte zor zamanlarla karşılaşacaktır. Bu, esasen çok iyi yazılmış olmayan sınıfların, uygulama kapsamı arttığında veya uygulama üretim veya bakımda belirli tasarım sorunlarıyla karşılaştığında çok zor durumlara yol açabileceği anlamına gelir.

Öte yandan, iyi tasarlanmış ve yazılan sınıflardan oluşan bir dizi, kodlama sürecini sıçramalar ve sınırlamalar ile hızlandırabilirken, kıyaslandığında hata sayısını azaltır.

Bu yazıda, Java’daki SOLID ilkelerini( en çok tavsiye edilen 5 tasarım) ilkesini örneklerle tartışacağız, sınıflarımızı yazarken akılda tutmamız gereken şu konuya dikkatinizi çekmek isterim.

SOLID adı, her harfin bir yazılım tasarım ilkesini temsil ettiği, aşağıdaki gibi hatırlatıcı bir kısaltmadır:

1. Single Responsibility Principle
2. Open Closed Principle
3. Liskov’s Substitution Principle
4. Interface Segregation Principle
5. Dependency Inversion Principle

Hepsini birer birer inceleyelim.

1. Single Responsibility Principle (Tek Sorumluluk İlkesi)

“Bir sınıfın yalnızca bir sorumluluğu olmalı”

Başka bir deyişle, bir sınıfı yalnızca bir amaç için yazmalı, değiştirmeli ve korumalıyız. Eğer bu model sınıfı ise yalnızca bir oyuncuyu yada varlığı temsil etmelidir. Bu, bize başka bir varlığa ait değişikliklerinin etkilerinden endişe etmeden gelecekte değişiklik yapma esnekliği sağlayacaktır.

Benzer şekilde, eğer servis / yönetici sınıfı yazıyorsak, yöntem çağrıları sadece bu kısmı içermeli ve hiçbir şey içermemelidir. Modül ile ilgili global fonksiyonlar bile kullanılmaz. Onları başka bir global olarak erişilebilir sınıf dosyasında ayırmak daha iyi olur. Bu, belirli bir amaç için sınıfın korunmasına yardımcı olacaktır ve sınıfın sadece belirli bir modüle görünürlüğüne karar vermiş oluruz.

Tek Sorumluluk İlke Örneği

Bir örneğe bakalım. Java’yı kullanacağım, ancak SOLID tasarım ilkelerini diğer OOP dillerine de uygulayabilirsiniz.

Diyelim ki bir kitapçı için bir Java uygulaması yazıyoruz. Kullanıcıların her bir kitabın adını ve yazarını alıp ayarlamasına ve envanterde kitabı aramasına izin veren bir Kitap sınıfı oluşturuyoruz.

Yukarıdaki Kitap sınıfında Tek Sorumluluk İlkesini ihlal eder şekilde iki sorumluluk mevcuttur.  İlk olarak, kitaplarla ilgili verileri (başlık ve yazar) belirler. İkincisi, envanterdeki kitabı arar. Ayarlayıcı yöntemleri, aynı kitabı envanterde aramak istediğimizde sorunlara yol açabilecek olan Kitap nesnesini değiştirebilir.

Bununla birlikte, Kitap sınıfı iki sorumluluğa sahip olduğundan yukarıdaki kod Tek Sorumluluk İlkesini ihlal eder. İlk olarak, kitaplarla ilgili verileri (unvan ve yazar) belirler. İkincisi, envanterdeki kitabı arar. Ayarlayıcı yöntemleri, aynı kitabı envanterde aramak istediğimizde sorunlara yol açabilecek olan Kitap nesnesini değiştirir.

Tek Sorumluluk İlkesini uygulamak için iki sorumluluğu çözmemiz gerekir. Refactor kodunda, Kitap sınıfı yalnızca Kitap  nesnesinin verilerini almaktan ve ayarlamaktan sorumlu olmalıdır.

Ardından, envanteri kontrol etmekten sorumlu olacak Envanter adlı başka bir sınıf oluşturuyoruz. KitapAra() yöntemini buraya taşır ve kurucudaki Kitap sınıfına başvururuz.

 

2. Open Closed Principle (Açık Kapalı İlkesi)

Bu, uygulamamızı tasarlarken akılda tutmamız gereken ikinci önemli kuraldır. Açık kapalı prensibi: “Yazılım bileşenleri geliştirme  için açık, ancak değişiklik için kapalı olmalıdır”

Ne anlama geliyor? Bu, sınıflarımız öyle bir şekilde tasarlanmalıdır ki uygulamadaki diğer geliştiriciler uygulamada belirli koşullarda kontrol akışını değiştirmek istediğinde, sınıfımızı genişletmek ve bazı işlevleri geçersiz kılmak için ihtiyaç duydukları her şeyi yapsınlar.

Diğer geliştiriciler, sınıfımızın getirdiği kısıtlamalar nedeniyle istenen davranışı tasarlayamıyorsa, sınıfımızı değiştirmeyi yeniden düşüneceklerdir. Burada kimsenin sınıfımızın bütün mantığını değiştirebileceğini kastetmiyorum, ancak yazılım tarafından sağlanan seçeneklerin yazılımcılar tarafından geliştirmeye açık, değiştirmeye kapalı olmaları gerektiği anlamına gelmesi gerekir.

Açık / Kapalı Prensip Örneği

Kitapçı örneğimize bakalım. Şimdi, yemek kitaplarını indirimli bir fiyatla vermek istiyoruz. Tek Sorumluluk İlkesini takip ederek,  iki ayrı sınıf oluşturuyoruz: iskontoların detaylarını saklamak için YemekKitapIndirim ve iskontoyu fiyata uygulamak için IndirimKontrol sınıflarını olacak.

Bu kod, mağaza yönetimi bize yemek kitaplarının indirimli satışlarının o kadar başarılı olduğunu söyleyene kadar devam eder. Şimdi, roman türü kitapları da % 50 indirim ile dağıtmak istiyorlar. Yeni bir özellik eklemek için yeni bir RomanIndirim sınıfı oluşturduk:

Yeni indirim türünü işlemek için, yeni işlevselliği IndirimKontrol sınıfına da eklememiz gerekir:

Ancak, mevcut işlevselliği değiştirirken Açık / Kapalı Prensibi ihlal ettik. Yukarıdaki kod düzgün çalışsa da, uygulamaya yeni güvenlik açıkları getirebilir. Yeni eklemenin, IndirimKontrol sınıfına bağlı olarak kodun diğer bölümleriyle nasıl etkileşimde bulunacağını bilmiyoruz. Gerçek dünyadaki bir uygulamada bu, tüm uygulamamızı tekrar test etmemiz ve dağıtmamız gerektiği anlamına gelir.

Ancak, tüm indirim türlerini temsil eden ekstra bir soyutlama katmanı ekleyerek kodumuzu yeniden düzenlemeyi de seçebiliriz. Öyleyse, YemekkitapIndirim ve RomanIndirim sınıflarının uygulayacağı KitapIndirim adlı yeni bir arayüz oluşturalım.

Şimdi, IndirimKontrol somut sınıflar yerine KitapIndirim arayüzüne başvurabilir. kitapIndirimYap() yöntemi çağrıldığında, hem YemekkitapIndirim hem de RomanIndirim‘e argüman olarak iletebiliriz, çünkü her ikisi de KitapIndirim  arabiriminin uygulamasıdır.

Yenilenmiş kod, mevcut kod tabanını değiştirmeden yeni YemekkitapIndirim sınıfını ekleyebileceğimiz için Açık / Kapalı prensibini izler. Bu, gelecekte uygulamamızı diğer indirim türleriyle de (örneğin, BilimkurguIndirim ile) genişletebileceğimiz anlamına gelir.

 

3. Liskov’s Substitution Principle(Liskov Değiştirme Prensibi)

Bu ilke daha önce tartışılan açık kapalı ilkenin bir çeşididir.

“Türetilmiş tipler, temel tipleri için tamamen değişken olmalıdır”

Bu, sınıfımızı genişleten yazılımcılar ek bir efor sarfetmeden sınıflarımızı uygulayabilmeleri gerekir. Bu, alt sınıflarınızın nesnelerinin, üst sınıfınızın nesneleriyle aynı şekilde davranmasını gerektirir. Bir üst sınıfın nesnesinin uygulamada sorunlara neden olmadan alt sınıflarının nesneleri tarafından değiştirilebilmesi gerektiğini belirtir. Özellikte bu metotlarda instanceof gibi nesnelerin tipleri arasında kıyaslama yapılmak zorunda kalındığı zaman, LSP prensibi çiğnenmiş olur ki, bu alt sınıfların varlığından haberdar olunduğu anlamına gelir. İdeal olan ise kullanıcı sınıfları alt sınıfların varlığını bilmesidir.

Liskov Değiştirme Prensibi Örneği

Şimdi, kitapçı, uygulamaya yeni bir teslimat işlevi eklememizi istiyor. Bu nedenle, müşterileri siparişlerini alabilecekleri konum sayısı hakkında bilgilendiren bir KitapSiparis sınıfı oluşturuyoruz:

Bununla birlikte, mağaza aynı zamanda sadece kendi cadde mağazalarına teslim etmek istedikleri lüks deri kılıflı kitaplar da satmaktadır. Bu yüzden, KitapSiparis öğesini genişleten ve getSiparisKonum() yöntemini kendi işlevselliği ile geçersiz kılan yeni bir CiltliKitapSiparis alt sınıfı oluşturuyoruz:

Daha sonra, mağaza bizden ekitaplar için teslimat işlevleri oluşturmamızı istedi. Şimdi, mevcut KitapSiparis sınıfını bir eKitapSiparis alt sınıfı ile genişletiyoruz. Ancak, getSiparisKonum() yöntemini geçersiz kılmak istediğimizde, ekitapların fiziksel konumlara teslim edilemeyeceğini biliyoruz.

Bununla birlikte, getSiparisKonum() yönteminin bazı özelliklerini değiştirebiliriz, ancak Liskov Değiştirme İlkesini ihlal edecek. Değişiklikten sonra, uygulamayı bozmadan KitapSiparis üst sınıfını eKitapSiparis alt sınıfıyla değiştiremedik.

Sorunu çözmek için, kalıtım hiyerarşisini düzeltmemiz gerekir. Kitap teslim türlerini daha iyi ayırt eden ekstra bir katman sunalım. Yeni OfflineSiparis ve OnlineSiparis sınıfları, KitapSiparis üst sınıfını ayıralım. Ayrıca, getSiparisKonum() yöntemini OfflineSiparis öğesine taşır ve OnlineSiparis sınıfı için yeni bir getYazilimSecenek() yöntemi oluştururuz (bu, çevrimiçi teslimatlar için daha uygundur).

Refactor yapılan kodda CiltliKitapSiparis, OfflineSiparis öğesinin alt sınıfı olacak ve kendi işlevleriyle getSiparisKonum() yöntemini geçersiz kılacaktır.

eKitapSiparis, şu anda getSiparisKonum() yöntemiyle uğraşması gerekmediği için iyi bir haber olan OnlineSiparis’nin alt sınıfı olacaktır. Bunun yerine, ebeveyinin getYazilimSecenek() yöntemini kendi uygulamasıyla geçersiz kılabilir.

Yenileme işleminden sonra, herhangi bir alt sınıfı, üst sınıfı yerine uygulamayı kırmadan kullanabiliriz.

 

4. Interface Segregation Principle(Arayüz Ayrıştırma İlkesi)

Bu prensip benim favorim. Arayüzlere sınıflara ait tek sorumluluk ilkesi olduğu için uygulanabilir.

“Kullanıcılar, kullanmayacakları gereksiz yöntemleri uygulamaya zorlanmamalıdır”

Örneğin bir geliştirici raporlama için iki yöntem sunmaktadır. Bir tanesi excel’e diğeri de pdf formatına raporlamaktadır. Şimdi müşteri “A” bu arayüzü kullanmak istiyor ancak raporları Excel formatında değil, sadece PDF formatında kullanmak istiyor. İşlevselliği kolayca kullanabilecek mi?

Hayır! Her ikisini de uygulamak zorunda kalacak, bunlardan bir tanesi yazılım tasarımcısı tarafından kendisine fazladan yük getirilecek. Ya başka bir yöntem uygulayacak ya da boş bırakacaktır. Bu iyi bir tasarım değil.

Öyleyse çözüm nedir? Çözüm, mevcut olanı kırarak iki arayüz oluşturmaktır. Bu, kullanıcıya yalnızca yalnızca gerekli işlevleri kullanma esnekliği verecektir.

Ayayüz Ayrıştırma İlkesi Örneği

Müşterilerimizin bir satın alma işlemi yapmadan önce içerikle etkileşimde bulunabilmesi için çevrimiçi kitapçığımıza bazı kullanıcı eylemleri ekleyelim. Bunu yapmak için, üç yöntemle KitapHareket adlı bir arayüz yapıp: yorumOku(), ikinciElAra() ve ornekOku().

Daha sonra iki sınıf oluşturuyoruz: CiltliKitapUI ve KitapHareket arayüzünü kendi işlevleriyle uygulayan bir eKitapUI:

Her iki sınıf da kullanmadıkları yöntemlere bağlı olduğundan Arayüz Ayrıştırma İlkesini ihlal ettik. Ciltli kitaplar okunamaz(ekitap gibi), bu yüzden CiltliKitapUI  sınıfının ornekOku() yöntemine ihtiyacı yoktur. Benzer şekilde, ekitaplarda ikinci el kopyalar bulunmaz, bu yüzden eKitapUI sınıfı da buna ihtiyaç duymaz.

Ancak, KitapHareket arayüzü bu yöntemleri içerdiğinden, bağımlı sınıflarının tümü bunları uygulamak zorundadır. Başka bir deyişle, KitapHareket ayrıştırmamız gereken kirli bir arayüzdür. İki özel alt arayüzle daha genişletelim: CiltliKitapHareket ve  eKitapHareket.

Şimdi, CiltliKitapUI sınıfı CiltliKitapUI arabirimini uygulayabilir ve ekitapUI sınıfı eKitapHareket arabirimini uygulayabilir.

Bu şekilde, her iki sınıf da kendileri için gereksiz olan metotları kullanmamış olurlar.

Refactor yapılan kod, her iki sınıf da kullanmadıkları yöntemlere bağlı olmadığından, Arayüz Ayrıştırma İlkesini izler.

5. Dependency Inversion Principle(Bağımlılıkları Ters Çevirme)

Çoğumuz, prensipte adı geçen kelimeleri zaten biliyoruz.

Yazılımımızı, birbirlerini bağlamak için soyut bir katman kullanarak çeşitli modüllerin birbirinden ayrılabileceği şekilde tasarlamalıyız.

Başka bir deyişle, üst ve alt seviye sınıflarını ayırmanız gerekir. Yüksek seviye sınıfları genellikle karmaşık mantığı içine alırken, düşük seviye sınıfları veri veya yardımcı programları içerir. Tipik olarak, çoğu insan yüksek seviye dersleri yapmak ister, düşük seviye derslerine bağlıdır. Ancak, Bağımlılık İnversiyon Prensibi’ne göre, bağımlılığı tersine çevirmeniz gerekir. Aksi halde, düşük seviye sınıfı değiştirildiğinde, yüksek seviye sınıfı da etkilenecektir.

Çözüm olarak, düşük seviyeli sınıflar için soyut bir katman oluşturmanız gerekir, böylece yüksek seviyeli sınıflar somut uygulamalardan ziyade soyutlamaya bağlı olabilir.

Bağımlılık İnversiyon Prensibi Örneği

Şimdi, kitapçı bizden en sevdikleri kitapları rafa koymalarını sağlayan yeni bir özellik geliştirmemizi istedi.

Yeni işlevselliği uygulamak için, daha düşük bir Kitap sınıfı ve daha yüksek bir Raf sınıfı oluşturduk. Kitap sınıfı, kullanıcıların raflarında depoladıkları her kitabın incelemelerini görmelerini ve okumalarını sağlayacaktır. Raf sınıfı, raflarına bir kitap ekleme ve rafı kişiselleştirme olanağı sağlar.

Her şey yolunda görünüyor, ancak yüksek seviye Raf sınıfı düşük seviye Kitaba bağlı olduğundan, yukarıdaki kod Bağımlılık

İnversiyon Prensibi’ni ihlal ediyor. Mağaza, müşterilerimizden raflarına DVD eklememizi istediklerinde de bu açıkça ortaya çıkıyor. Talebi karşılamak için yeni bir DVD sınıfı yaratıyoruz:

Şimdi, Raf sınıfını da DVD’leri kabul edebilecek şekilde değiştirmeliyiz. Ancak, bu açıkça Açık / Kapalı Prensibi bozacaktır. Çözüm, alt seviye sınıfları için bir soyutlama katmanı oluşturmaktır (Kitap ve DVD). Bunu, her iki sınıfın da uygulayacağı Ürün arayüzünü tanıtarak yapacağız.

Artık Raf, uygulamaları yerine Ürün arayüzüne başvurabilir (Kitap ve DVD). Yenilenmiş kod, daha sonra, müşterilerin raflarına koyabilecekleri yeni ürün türlerini (örneğin Dergi) de tanıtmamıza izin verir.

Yukarıdaki kod aynı zamanda Liskov Değiştirme Prensibi’ni izler, çünkü Ürün tipi programı bozmadan her iki alt tipiyle (Kitap ve DVD) ikame edilebilir. Aynı zamanda, yenilenmiş kodunda olduğu gibi, bağımlılık Inversion Principle (Bağımlılık İnversiyonu Prensibi) ‘ni de uyguladık, yüksek seviyeli sınıflar da düşük seviyeli sınıflara bağlı değildir.

SOLID tasarım ilkelerini nasıl uygulamalısınız?

SOLID tasarım ilkelerini uygulamak, bir kod tabanının genel karmaşıklığını arttırır, ancak daha esnek bir tasarıma yol açar. Yekpare uygulamaların yanı sıra, SOLID tasarım ilkelerini, her bir microservise bağımsız bir kod modülü (yukarıdaki örneklerde verilen bir sınıf gibi) gibi davranabileceğiniz microservislere de uygulayabilirsiniz.

 

Yorum bırak