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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Kitap { String baslik; String yazar; String getBaslik() { return baslik; } void setBaslik(String baslik) { this.baslik = baslik; } String getYazar() { return yazar; } void setYazarString yazar) { this.yazar = yazar; } void kitapAra() {...} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Kitap { String baslik; String yazar; String getBaslik() { return baslik; } void setBaslik(String baslik) { this.baslik = baslik; } String getYazar() { return yazar; } void setYazar(String yazar) { this.yazar = yazar; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 |
public class Envanter{ Kitap kitap; Envanter(Kitap kitap) { this.kitap = kitap; } void KitapAra() {...} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class YemekkitapIndirim { String getYemekkitapIndirim() { String indirim = "%20 indirim uygula"; return indirim; } } class IndirimKontrol { void yemekkitapIndirimYap(YemekkitapIndirim indirim) {/*****/} } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
class RomanIndirim { String getRomanIndirim() { String indirim = "%50 indirim uygula"; return indirim; } } |
Yeni indirim türünü işlemek için, yeni işlevselliği IndirimKontrol
sınıfına da eklememiz gerekir:
1 2 3 4 5 6 |
class IndirimKontrol { void yemekkitapIndirimYap(YemekkitapIndirim indirim) {/*****/} void romanIndirimYap(RomanIndirim indirim) {/*****/} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public interface KitapIndirim { String getKitapIndirim(); } class YemekkitapIndirim implements KitapIndirim { @Override public String getKitapIndirim() { String indirim = "%20 indirim uygula"; return indirim; } } class RomanIndirim implements KitapIndirim { @Override public String getKitapIndirim() { String indirim = "%50 indirim uygula"; return indirim; } } |
Ş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.
1 2 3 4 5 6 |
class IndirimKontrol { void IndirimYap(KitapIndirim indirim) {/******/} } |
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:
1 2 3 4 5 6 7 8 |
class KitapSiparis { String kitaplar; int kullaniciID; void getSiparisKonum() {/***/} } |
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:
1 2 3 4 5 6 7 8 |
class CiltliKitapSiparis extends KitapSiparis { @Override void getSiparisKonum() {/***/} } |
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.
1 2 3 4 5 6 7 |
class eKitapSiparis extends KitapSiparis { @Override void getSiparisKonum() {/* fiziksel konum olmadığı için bunu kullanamayız*/} } |
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class KitapSiparis { String title; int userID; } class OfflineSiparis extends KitapSiparis { void getSiparisKonum() {...} } class OnlineSiparis extends KitapSiparis { void getYazilimSecenek() {...} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CiltliKitapSiparis extends OfflineSiparis { @Override void getYazilimSecenek() {...} } class eKitapSiparis extends OnlineSiparis { @Override void getYazilimSecenek() {...} } |
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().
1 2 3 4 5 6 7 |
public interface KitapHareket { void yorumOku(); void ikinciElAra(); void ornekOku(); } |
Daha sonra iki sınıf oluşturuyoruz: CiltliKitapUI ve KitapHareket arayüzünü kendi işlevleriyle uygulayan bir eKitapUI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class CiltliKitapUI implements KitapHareket { @Override public void yorumOku(){/***/}; @Override public void ikinciElAra() {/***/} @Override public void ornekOku() {/**/} } class eKitapUI implements KitapHareket { @Override public void yorumOku(){/***/}; @Override public void ikinciElAra() {/***/} @Override public void ornekOku() {/**/} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public interface KitapHareket { void yorumOku(); } public interface CiltliKitapHareket extends KitapHareket{ void ikinciElAra(); } public interface eKitapHareket extends KitapHareket { void ornekOku(); } |
Ş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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class CiltliUI implements CiltliKitapHareket { @Override public void yorumOku() {/*...*/} @Override public void ikinciElAra() {/*...*/} } class eKitapUI implements eKitapHareket { @Override public void yorumOku() {/*...*/} @Override public void ornekOku() {/*...*/} } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Kitap { void yorumOku() {/*...*/} void ornekOku() {/*...*/} } class Raf { Kitap book; void kitapEkle(Kitap kitap) {/*...*/} void RafDuzenle() {/*...*/} } |
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:
1 2 3 4 5 6 7 8 |
class DVD { void YorumOku() {/*...*/} void ornekIzle() {/*...*/} } |
Ş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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public interface Urun { void yorumOku(); void ornekBak(); } class Kitap implements Urun{ @Override public void yorumOku() {/*...*/} @Override public void ornekBak() {/*...*/} } class DVD implements Urun{ @Override public void yorumOku() {/*...*/} @Override public void ornekBak() {/*...*/} } |
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.
1 2 3 4 5 6 7 8 9 10 |
class Raf { Urun urun; void urunEkle(Urun urun) {/*...*/} void RafDuzenle() {/*...*/} } |
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 Yap