SOLID, geliştirilen yazılımın kaliteli olmasını sağlamak için ortaya konulmuş 5 ana prensibin baş harflerini temsil etmektedir. Bu prensiplere dikkat edilerek geliştirilen yazılımlar esnek, sürdürülebilir, test edilebilir, kolay anlaşılır olurlar.

SOLID prensiplerini teker teker inceleyeceğiz. Örneklerimizi de MarsRover problemi üzerinden vereceğiz.

Single Responsibility Principle (Tek Sorumluluk Prensibi)

Bu prensibin özü bir sınıfın veya metodun yalnızca bir sorumluluk alanının olmasıdır. Bir diğer deyişle, sınıf veya metot üzerinde değişiklik yapılmasının yalnızca bir sebebi olmalıdır.

MarsRover projesinde yer alan birkaç sınıfın görevini açıklayarak nasıl Tek Sorumluluk Prensibine uymaya çalıştığımızı açıklayalım.

IVehicleBuilder arayüzü oluşturulmuştur. Bu arayüzün becerilerini üstlenen RoverBuilder adında bir sınıf mevcuttur. Bu sınıfın tek bir görevi, bildiği tek bir iş vardır. ‘Alınan argümana göre Rover nasıl üretilir?’ sorusunun cevabı bu sınıfın içerisine gizlenmiştir. Rover üretimiyle ilgili bir adımı değiştirmek istersek bu sınıfa müdahale etmemiz gerekecektir.

Tek sorumluluk prensibinin en büyük düşmanları arasında “ve” ve “veya” vardır. Değişiklik yapma gerekçemizi düşünürken “veya” kullanıyorsak sınıfımız Tek Sorumluluk Prensibine uymuyordur.

Bir diğer örnek VehicleFactory adıyla sistemde var olan sınıfımız olabilir. Bu sınıfın Generate metodunu aşağıda görebiliriz. Bu metoda bakarsak yaptığı tek şey üretilmek istenen aracın tipine göre aracı yapacak kişiyi bulup ona gereken bilgileri aktarmaktır.

VehicleFactory parametreleri işleterek, araçları kendisi üretecek olsaydı ve sistemde birden fazla araç (Rover, Drone vb.) yer alsaydı neler olacağını bir düşünelim. Rover üretim adımlarından biri veya Drone üretim adımlarından biri değişeceği zaman VehicleFactory üzerinde değişiklik yapacaktık. Bu durumda metot Tek Sorumluluk Prensibine uygun olmayacaktır.

Metot isimlerinde bu bağlaçları görmek de metodun Tek Sorumluluk Prensibine uymadığını gösterir. AddOrUpdate, IsNullOrEmpty, SetAndGet gibi metotlar tek sorumluluk prensibine uymazlar.

Open-Close Principle (Açık-Kapalı Prensibi)

Bu prensipte bahsi geçen açıklık ve kapalılık şu demektir: “gelişime açık, değişime kapalı”. Yani yeni bir özellik eklemek için kodlarımızı geliştirebiliriz ama değiştiremeyiz. Bu prensibi örneklemek için sorunun araçlar (Vehicle) kısmına odaklanabiliriz.

IMovable arayüzü ile hareket edebilme becerisine sahip olan varlıkları ortaya koyduk. Vehicle soyut sınıfımızın hareket edebilme becerisine sahip olacağı yönünde bir tasarıma gittik. Araçların somut örneği olarak da Rover sınıfını oluşturduk. Böylece araçlar üzerinde problemimizi çözecek kadar geliştirme yapmış olduk. Şimdi ise yeni bir görevimiz var: Mars’a uçabilen araçlar göndermek. Bu görevi yerine getirirken kodlarımızı değiştirmemeye çalışalım.

IFlyable (uçabilir) arayüzünü tanımlayarak ilk adımımızı atabiliriz.

Bu arayüze sahip olan sınıflardan türeyen araçların yukarı-aşağı hareket edebilme yeteneğine sahip olduğunu görebiliriz. Ardından FlyingVehicle (uçan araçlar) adında bir sınıf tasarlayabiliriz. “Her uçan araç bir araçtır” cümlesinin çok bariz bir gerçeği ortaya koymasından yola çıkarak ilişkiyi aşağıdaki gibi kurabiliriz.

Son olarak da Drone adında bir sınıfı FlyingVehicle sınıfından türetebiliriz.

Böylece sistemde kod değişikliği yapmadan, araçların eski davranışlarını değiştirmeden bu araçların yeni özellikler kazanmasını sağlamış olduk.

Sistemin geriye uyumluluk (backward compatibility) özelliğine sahip olmasının en önemli etmenlerinden biri Açık-Kapalı Prensibine ne kadar uyulduğudur.

Liskov Substitution Principle (Yerine Geçme Prensibi)

Bu prensip alt sınıfın (derived class) kalıtım aldığı üst sınıfın (base class) tüm özelliklerini alması gerektiğini söylemektedir. Bu özelliklerin tamamını taşımaması halinde tasarlanan sınıfımız üst sınıf yerine kullanılamaz.

Bunu örneklemek için Vehicle sınıfını düşünebiliriz. Vehicle sınıfının örnekleri, ileri gitme ve sağa-sola dönmek gibi özelliklere sahiptir. Bunu yaparken Mars’a sağa sola dönemeyen bir araç gönderilmeyeceğini düşünmüştük. Örneğimizi devam ettirebilmek için Mars’a tren hattı kurmaya başladığımızı ve artık sağa sola dönme özelliği olmayan bir araç tasarladığımızı düşünelim. Bu durumda araç (Vehicle) tiplerimizden biri olan Tren (Train) sınıfımızın diğer araçlarımız gibi davranmadığını görürüz. Tren sınıfımız bizim soyutlamamıza göre aslında bir araç değildir ya da araç tanımımızı değiştirmemiz gerekecektir.

Yerine Geçme Prensibine göre alt sınıflar (derived type), üst sınıfların (base type) tüm özelliklerine sahipse varlıklar birbirinin yerine kullanılabilir. Açık-Kapalı ilkesinde anlatılan örneği ele alacak olursak, Rover sınıfının bir örneği ile etrafı keşfedebiliriz. Drone sınıfı yerine geçme prensibini ihlal etmediği için, bu sınıfın bir örneği ile de etrafı keşfedebiliriz.

Interface Segregation Principle (Arayüz Ayrım Prensibi)

Bir arayüzü üstlenen sınıfın, arayüzün tüm gerekliliklerini yerine getirmesi gerektiğini anlatan prensiptir. Bu prensip arayüzlere fazla özellik verilmemesi gerektiğini belirtir.

Bu örnek için Rover sınıfından önce Drone sınıfını tasarladığımızı düşünelim. IMovable arayüzü, Fly (uç) metoduna da sahip olsun. Bu durumda Rover sınıfını tasarlamaya başladığımız zaman Fly metodunun içerisinde throw new NotImplementedException(); kod satırının yerine yazacak bir şey bulamamaktayız.

Tek Sorumluluk Prensibini ihlal eden arayüz tasarımları, Arayüz Ayrım Prensibini uygulayamamamızın sebebi olurlar. Arayüz Ayrım Prensibine uyamadığımız durumda da Yerine Geçme Prensibini ihlal etmiş oluruz.

Dependency Inversion Principle (Bağımlılıkların Tersine Çevrilmesi Prensibi)

Bağımlıkların Tersine Çevrilmesi, sınıflar arasındaki bağımlılık seviyesini düşürmeyi hedeflemektedir. Bağımlılıkların azalması aracılığı ile bu prensip, sürdürülebilirlik ve esneklik konusunda yazılımın kalitesine büyük katkı sağlamaktadır. Bunlarla birlikte en büyük artıyı test edilebilirlik noktasında görmekteyiz. Eğer birim testi yazma aşamasında sorun yaşıyorsanız, bağımlılıkların yeterince soyutlanıp soyutlanmadığını kontrol etmek iyi bir başlangıç noktası olabilir.

VehicleFactory sınıfına bakacak olursak, IVehicleBuilder tipinden nesneleri kendi yapıcı metodu aracılığı ile tanımaktadır.

Generate metoduna baktığımızda ise bir araç tipi ve bu aracı üretmek için gerekli parametre verisi yer almaktadır. Metot çalıştığı zaman uygun tipteki IVehicleBuilder nesnesini bulmaktadır ve ardından nesnenin Build metodunu çalıştırmaktadır.

Bağımlılığı tersine çevirmemiş olsaydık, IVehicleBuilder sınıfları (RoverBuilder, DroneBuilder, TrainBuilder etc) VehicleFactory sınıfının içinde üretilecekti. Bu durumda VehicleFactory sınıfı Builder sınıflarına sıkı sıkıya bağımlı olacaktı. Factory sınıfının testleri Builder sınıfını da test ediyor olacaktı.

Bağımlılıklar dışarıdan yönetildiği zaman, durumu istediğimiz gibi modelleyebilir ve testini yapabiliriz. Ayrıca Factory sınıfını test ederken dışarıdan verdiğimiz sahte (mock) objeler sayesinde de Factory sınıfının testleri, Builder sınıfının gerçek objelerini çağırmayacaktır ve onları testin dışında tutacaktır.

İsteğe uygun IVehicleBuilder bulunamazsa oluşacak sonucun testi:

Uygun IVehicleBuilder objesi bulunursa, bu objenin Build metodunun bir kere çağırıldığının testi:

Nesne Yönelimli Programla (Object Oriented Programming) yaparken önemli olan ve yazılımcıların dikkat etmesi gereken 5 prensipten bahsettik. Nesne yönelimli programlama hakkında yazdığım yazıya da bu link aracılığı ile ulaşabilirsiniz.

var software = ConvertFrom(caffeine)

var software = ConvertFrom(caffeine)