Modern programlama tekniklerinde olmazsa olmaz olan OOP (object oriented programming) mimarisi , programcıların sadık kalacağı kural ve teknikler ile daha yönetilebilir, genişletilebilir ve stabil kodlar ortaya çıkartilabilir. Uzun yıllar boyunca program geliştiricileri tarafından bir çok deneme yanılma yollar veya planlı tasarımlar sonucunda ortaya çok ciddi bir bilgi ve tecrübe birikimi ortaya çıkmıştır. Bugün bu tecrübeler farklı isimlerle yazılım geliştirme dünyasında kullanılmaktadır. Bunlardan en temel bilineni olan Solid prensibi bir çoğumuz farkında olmadan bile kullanmışızdır aslında. Şayet bilmeden kullandığımız yada bilmeyi isteyeceğimiz bu Solid denen prensibe bir göz atalım.
Solid Nedir ?
Solid ismini alt bünyesinde barındırdığı 5 alt prensibten alır. Her prensibin baş harflerden ortaya SOLID çıkmaktadır.
1. (S)ingle Responsibility Principle
2. (O)pen/Closed Principle
3. (L)iskov ‘s Substitution Principle
4. (I)nterface Segregation Principle
5. (D)ependency Inversion Principle
Bu prensibi kısaca bir örnekle açıklamak gerekirse , Örneğin bir dosya kopyalama işi yapıyorsunuz ağ üzerinden ve dosyanın adındaki boşlukları silip _ haline getirmek istediniz.
Yani
Kaynak : “\\192.168.1.2\paylasim\19 05 2017 143000.mpg
Hedef :”E:/arsiv/2017/05/19/19_05_2017_143000
Kodunuzu sadece bir void method ile ; Ağ üzerinde dosya var mı ? Evet , varsa boşlukları _ ile değiştir yaptınız, Yıl/Ay/Gun arşiv yapısı içerisinde kopyalama yapılacak arşiv haline getirdiniz. Kopyalama başarılı ise dosyanın ağ üzerindeki kaynak silinmesi işinide yapıyor. Buraya kadar herşey yolunda , peki yöneticiniz size şunları derse ne yapacaksınız ??
Artık ağ üzerindeki hedef silinmeyecek ?
Artık oranın kullandığı arşiv formatı Gun/Ay/Yıl , ama önceden kullandığım formatada destek verilecek !! (eski kodları silmeye izin yok)
Artık Dosya kopyalama yarıda kalmış mı tespit edilecek ?
Eğer kodu yine tek void methodda düzenlerdim diyorsanız bu kesinlikle single responsibility prensibine uygun yaklaşım değildir. Ortaya karmakarışık bir business kuralları olan bir kod çıkacak ve if satırları ile dolan , sürekli yukarıda satırlara bağımlı olan yeniden kullanılamaz kodlar oluşur. Bir şekilde çözersiniz evet fakat siz o yazılım şirketinden yada projeden ayrıldığınızda sizi hayırla anmayacak üstad gibi nitelemeler yerine daha sevimsiz nitelemelere maruz kalabilirisiniz kulağınız duymasa bile.
Çözüm :
1- Arşiv yöntemini string olarak üretecek fonksiyon yazılır, böylece kaç farklı yöntem olursa olsun ortaya bir arşiv format kütüphanesi çıkar. Eski bir formata destek vermek için 50 tane referans alan çağrının içinde kodlar 50 kere kopyalanmaz. (Ölümcül hata)
1 2 3 4 5 |
public string YilAyGun(string root, DateTime tarih,string filename) { string lastFileName= filename.Replace(" ","_"); return $"{root}/{tarih.Year}/{tarih.Month}/{tarih.Day}/"; } |
2- Ağ üzerinde silme işlemi yapacak kod dışarıya void olarak çıkarılır.
1 2 3 4 5 |
public string TargetFileDelete(bool delete,string path) { if (delete) File.Delete(path); } |
Böylelikle değişiklik yapılacak noktaları tespit etmek, kod okunurluğunu arttırmak, yazdığınız kodu tekrar kullanılabilir hale getirmek , geriye destek vermek son derece yönetilebilir ve kolay olmaktadır.
Bu prensip proje geliştirirken sınıf ve metodların değişime açık , bu sınıfları tüketen kodların ise değişime kapalı olmasıdır . Nasıl olduğunu gelin inceleyelim
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public interface IBuyer { void Buy(); } public class CarBuyer : IBuyer { public void Buy() { Console.WriteLine("Araba Sattım"); } } public class BusBuyer : IBuyer { public void Buy() { Console.WriteLine("Otobüs Sattım"); } } |
Sınıfı ve methodu Tüketen Kod , örneğimizde Program.cs içinde düşünelim
Yukarıdakı kodu gelin bir senaryoya dökelim.
1 2 3 4 5 6 7 8 9 10 |
[STAThread] static void Main() { IList<IBuyer> sepet = new List<IBuyer>(); IBuyer buycar = new CarBuyer(); IBuyer buyBus = new BusBuyer(); sepet.Add(buycar); sepet.Add(buyBus); } |
Yukarıdaki kodda araba ve otobüs satın almak için sepetinize eklediniz, şimdi 2 senaryo daha düşünelim.
1- Araba almaktan vazgeçtiniz ve yerine otobüs alacaksınız?
2- Minibus almak istiyorsunuz , sepete bunu atabilmek istiyorsunuz
Çözümü aşağıdaki kodlarda gerçekleştirip açıkladı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 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class CarBuyer : IBuyer { public void Buy() { Console.WriteLine("Araba satın aldınız, hediyeniz 4 lastik"); } } public class BusBuyer : IBuyer { public void Buy() { Console.WriteLine("Otobüs satın aldınız, hediyeniz 6 lastik "); } } public class MinibusBuyer : IBuyer { public void Buy() { Console.WriteLine("Minibüs satın aldınız, hediyeniz 5 lastik "); } } [STAThread] static void Main() { IList<IBuyer> sepet = new List<IBuyer>(); IBuyer buyBus1 = new BusBuyer(); // IBuyer buycar = new CarBuyer(); vazgeçtik IBuyer buyBus2 = new BusBuyer(); IBuyer minibus = new MinibusBuyer(); sepet.Add(buyBus1); sepet.Add(buyBus2); sepet.Add(minibus); foreach (IBuyer sepetUrun in sepet) { sepetUrun.Buy(); } } |
Output :
Otobüs Satın aldınız, hediyeniz 6 lastik
Otobüs Satın aldınız, hediyeniz 6 lastik
Minibüs Satın aldınız, hediyeniz 5 lastik
Sonuç: Yukarıda gördüğünüz üzere sepetteki değişiklerden ana kod ciddi denilebilecek şekilde etkilenmedi. Yeni bir ürün yeni bir sınıf ile sisteme tanıtıldı ve doğrudan sepetin formatına uygun ve bozmayacak ve doğrudan desteklenebilecek şekilde IBuyer sınıfından implemente edildi. Tüketme kısmında kolaylıkla vazgeçilen ürünün yeni tipi, sepetin dinamik haline uygun şekilde türetildi. Sepet kısmımızın bozulmaya değişikliğe uğramadan kapalı kaldığını farkettirebildiysem ne mutlu bana 🙂 , Yani Sepetimiz şöyle oluşturulmadı ;
List<BusBuyer> buycarList = new List<BusBuyer>();
her biri için bu yapılmış olsaydı , her biri için ayrı bir sepet , ayrı bir satış gerçekleştirme döngüsü , uzayıp gidecekti . Sepet toplamlarını toplayıp , en son toplam fiyat diyecektik. İtiraf edelim bu iş hamallık 😉
Bu prensibin genel amacı Implement edilmesi zorunlu olan metodların türetilen sınıfta kullanılmasının mecburi olması prensibidir. Misal vermek gerekirse ;
Örneğin yönetmelik gereği mecbur olan bir asansörlü bir bina tasarlamanız gerekiyor , asansörün en boy bilgisini throw new exception() olarak döndüren bir metodun ne demek istediğini anlayabilmek , anlatabilmek için çok çaba gerek 🙂 Bir sınıf, bir sınıftan veya interfaceden türetilmişse ! exception dönen veya boş dönen metod implementasyonları yada override işlemleri olmamalıdır.
Özetle mecbur olmayan şeyleri mecbur gibi dayatmamak , yeteneğimiz olmayan şeyleri de yeteneğimiz gibi göstermemek lazım. Eski bir arabada ABS özelliği yokken var göstermeniz gülünç olur 🙂 Var ama çalışmıyor demek sizi gülünçlükten kurtarmaz, Yada eski araçlara saatte 300 km gitme zorunluluğu dayatmak ! Yada villalara otopark dayatmak , ne kadar da benzer yanlışlar.
Aşağıdaki gibi tasarımda exception aldığınızda program kırılır. Gider exception fırlatan satırı silersiniz boş sonuç dönersiniz eyvallah fakat çirkin bir iş yaparsınız bu prensibe göre.
ÇİRKİN TASARIM
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
public abstract class Asansor { public abstract void Kur(); public abstract string EnBoy(); } public class VillaAsansor : Asansor { public override string EnBoy() { throw new NotImplementedException(); return ""; // 2 satırda yanlış } public override void Kur() { Console.WriteLine("keyfim isterse kurarım."); } } public class BinaAsansor : Asansor { public override void Kur() { Console.WriteLine("zorundayım ondan kurdum."); } public override string EnBoy() { return "1x2"; } } [STAThread] static void Main() { IList<Asansor> asansorlu = new List<Asansor>(); asansorlu.Add(new BinaAsansor()); asansorlu.Add(new VillaAsansor()); foreach (Asansor asansorItem in asansorlu) { var enboy = asansorItem.EnBoy(); // bu satır villa Asansörü için hata verecek } } |
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public abstract class Asansor { void Kur(); } public abstract MecburiAsansor { string EnBoy(); } public class VillaAsansor : Asansor { public void Kur() { Console.WriteLine("keyfim isterse kurarım."); } } public class BinaAsansor : Asansor,MecburiAsansor { public void Kur() { Console.WriteLine("zorundayım ondan kurdum.") } public string EnBoy() { return "1x2"; } } [STAThread] static void Main() { IList<MecburiAsansor> asansorMecburYapilar = new List<MecburiAsansor>(); asansorMecburYapilar.Add( new BinaAsansor()); asansorMecburYapilar.Add(new VillaAsansor()); // bu satır hata verir,ekleyemezsiniz bile, mecbur değil, GÜZEL TASARIM foreach (MecburiAsansor asansorItem in asansorMecburYapilar) { var enboy= asansorItem.EnBoy(); asansorItem.Kur(); // kaç metre boşluk bırakılacaksa o kadar malzeme al } } |
GÜZEL VE DOĞRU TASARIM
1 |
Bu prensip Interface arayüzlerinize gerektiğinden fazla method eklenmesinin Liskov ‘s Substitution Principle olduğunda gibi gereksiz ve çirkin bir iş olduğunu söyler. Liskov da abstract sınıf için kural konulurken , burada ise Interface için bu kural koyuluyor. Aşağıdaki tasarım doğru bir tasarımdır. Yanlış tasarım nedir derseniz bana göre ; EnBoy methodunu IAsansorde tanımlayıp, IMecburiAsansor interfacesini silmek denebilir. Özetle mecbur olmayan şeyleri mecbur gibi dayatmamak , yeteneğimiz olmayan şeyleri de yeteneğimiz gibi göstermemek lazım. Eski bir arabada ABS özelliği yokken var göstermeniz gülünç olur 🙂 Var ama çalışmıyor demek sizi gülünçlükten kurtarmaz, Yada eski araçlara saatte 300 km gitme zorunluluğu dayatmak ! Yada villalara otopark dayatmak , ne kadar da benzer yanlışlar.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public interface IAsansor { void Kur(); } public interface IMecburiAsansor { string EnBoy(); } public class VillaAsansor : IAsansor { public void Kur() { Console.WriteLine("keyfim isterse kurarım.") } } public class BinaAsansor : IAsansor,IMecburi { public void Kur() { Console.WriteLine("zorundayım ondan kurdum.") } public string EnBoy() { return "1x2"; } } [STAThread] static void Main() { IList<IMecburiAsansor> asansorMecburYapilar = new List<IMecburiAsansor>(); asansorMecburYapilar.Add( new BinaAsansor()); asansorMecburYapilar.Add(new VillaAsansor()); // bu satır hata verir,ekleyemezsiniz bile, mecbur değil, GÜZEL TASARIM foreach (IMecburiAsansor asansorItem in asansorMecburYapilar) { var enboy= asansorItem.EnBoy(); asansorItem.Kur(); // kaç metre boşluk bırakılacaksa o kadar malzeme al } } |
Bağımlılıkların terse çevrilmesi olarak çevrilen bu prensipte üst sınıf alt sınıftaki değişikliklerden etkilenmemesi asıl amaçtır. Kısaca bir lokantanız var ve işe çorbacı aldınız. Usta domates verilse çorba yapacak, kara lahana verilse yine çorba yapacak, hatta turp verseniz , hamsi verseniz bile 🙂 onun işi çorba yapmak sorun çıkarmak değil. Bundan çorba olmaz demek onun işi değil , bunu söylemek iş kurallarını tanımlayan patrona kalmış birşeydir. Programlama dünyasında da bu kuralları yazılım mimarları , analizciler, programcılar tasarlar.. Kod örneğimiz ile inşaAllah daha anlaşılır hale getirebilmişimdir.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public interface ICorba { void Pisir(IMalzeme malzeme); } public interface IMalzeme { void MalzemeGetir(); } public class Domates : IMalzeme { public void MalzemeGetir() { Console.WriteLine("Al sana Domates"); } public override string ToString() { return "Domates"; } } public class KaraLahana : IMalzeme { public void MalzemeGetir() { Console.WriteLine("Al sana Kara Lahana"); } public override string ToString() { return "KaraLahana"; } } public class Corbaci : ICorba { public void Pisir(IMalzeme malzeme) { malzeme.MalzemeGetir(); Console.WriteLine(malzeme.ToString() + " Pişirildi."); } } static void test() { Corbaci corbaci = new Corbaci(); corbaci.Pisir(new KaraLahana()); corbaci.Pisir(new Domates()); } |
output:
Al sana Kara Lahana
KaraLahana Pişirildi.
Al sana Domates
Domates Pişirildi.