SOLID Prensipleri
Nesne tabanlı tasarımın ilk 5 ilkesi
SOLID, Robert C. Martin’in (Bob Amca olarak da bilinir) Design Principles and Design Patterns kitabındaki ilk beş nesne yönelimli tasarım (OOD) ilkesinin kısaltmasıdır. SOLID kısaltması daha sonra Michael Feathers tarafından tanımlandı.
Bu ilkelerin amacı, yazılım tasarımlarını daha anlaşılır, okunabilir kılmak, bakımı ve geliştirmeyi daha kolay hale getirmektir. Bir yazılım mühendisi olarak bu 5 ilkeyi bilmek çok önemlidir.
SOLID’in açılımı:
- S — Single-responsiblity Principle
- O — Open-closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
Bu yazıda her bir prensibi örneklerle açıklamaya çalışacağım. Örnekleri PHP ile vereceğim. Ancak herhangi bir OOP dili için uygulanabilir.
Single-Responsibility Principle
Bir sınıfın değişmesi için bir ve yalnızca bir nedeni olmalıdır. Yani bir sınıfın yalnızca bir işi olmalıdır.
Şu alıntıyı duymuş olabilirsiniz: “Sadece bir şey yapın ve onu iyi yapın”. Bu, single-responsibility principle’ı ifade eder.
Bir örnekle başlayalım. Daireler ve kareler gibi şekillerden oluşan bir koleksiyon alan ve koleksiyondaki tüm şekillerin alanlarının toplamını hesaplayan bir uygulama düşünelim.
İlk olarak, şekil sınıflarını oluşturalım ve constructor’ların gerekli parametreleri ayarlamasını sağlayalım.
Kare için bir sınıf oluşturalım:
Daire için bir sınıf oluşturalım:
Ardından, AreaCalculator sınıfını oluşturalım ve sağlanan tüm şekillerin alanlarını hesaplayalım.
Şimdi bir şekil dizisi oluşturalım ve bu diziyi oluşturduğumuz nesneye parametre olarak ekleyelim.
Burada sorun, AreaCalculator sınıfında çıktı almak için output metodunu kullanıyor olmamızdır.
Çıktının JSON gibi başka bir biçime dönüştürülmesi gereken bir senaryo düşünün. Bu durumda output ile ilgili bütün mantıksal işlemlerin AreaCalculator sınıfında yapılması gerekir. Bu da single-responsibility prensibini ihlal eder. AreaCalculator sınıfı, yalnızca sağlanan şekillerin alanlarının toplamıyla ilgilenmelidir. Kullanıcının JSON veya HTML istemesi ile ilgili işlemler olmamalıdır.
Bunu çözmek için ayrı bir SumCalculatorOutputter sınıfı oluşturabilir ve bu yeni sınıfı kullanıcının istediği biçimdeki çıktıları oluşturmak için kullanabiliriz.
SumCalculatorOutputter sınıfı şöyle çalışmalıdır:
Şu anda kullanıcıya verilecek çıktıların mantıksal işlemlerini SumCalculatorOutputter’da gerçekleştirmekteyiz. Böylece single-responsibility prensibine uygun geliştirme yapmış oluyoruz.
Open-Closed Principle
Yazılım varlıkları (sınıflar, modüller, fonksiyonlar vb.) geliştirmeye açık ancak değişikliğe kapalı olmalıdır.
Örneğin bir sınıf, kendisini değiştirmeden genişletilebilir olmalıdır.
AreaCalculator sınıfına yeniden bakalım ve buradaki sum metoduna odaklanalım:
Kullanıcının üçgenler, beşgenler, altıgenler gibi diğer şekillerin toplamını isteyeceği bir senaryo düşünün. Bu dosyayı sürekli olarak düzenlemeniz ve daha fazla if / else bloğu eklemeniz gerekir. Bu, open-closed prensibini ihlal eder.
Bu alan hesaplama yöntemini daha iyi hale getirmenin yolu AreaCalculator sınıfından hesaplama işlemini alıp her bir şeklin alanını kendi sınıfı içinde hesaplamaktır.
Bu durumda Square sınıfımız şu şekilde olacaktır:
Circle sınıfımız:
Şimdi AreaCalculator sınıfındaki sum metodunu yeniden yazalım.
Şimdi, başka bir şekil sınıfı oluşturabilir ve alanı hesaplarken kodu bozmadan çalıştırabiliriz.
Ancak bu noktada da başka bir sorunla karşılaşabiliriz. AreaCalculator’a iletilen nesnenin aslında bir şekil olduğunu veya şeklin area adında bir metodu olup olmadığını nasıl anlarız?
Bir interface, işimizi çözecektir ve bu SOLID’in ayrılmaz bir parçasıdır.
ShapeInterface adında bir interface oluşturalım:
Bu interface’i şekil sınıflarımızın her birine implement edelim:
Şimdi AreaCalculator’daki sum metodunda sağlanan şekillerin gerçekten ShapeInterface örnekleri olup olmadığını kontrol edebiliriz. Değilse exception atabiliriz.
Yeni sum metodumuz şu şekilde olacaktır:
Böylece open-closed prensibine uygun geliştirme yapmış oluyoruz.
Liskov Substitution Principle
Bu muhtemelen ilk anlamaya çalıştığımızda mantığını oturtması en zor olanıdır.
Liskov substitution prensibinde; S, T’nin bir alt tipiyse, T tipi nesneler S tipi nesnelerle değiştirilebilir.
Bu matematiksel olarak şu şekilde formüle edebiliriz:
q(x), T türündeki x nesneler için kanıtlanabilir bir özellik olsun. O zaman q(y), S türündeki y nesneleri için kanıtlanabilir olmalıdır. Burada S, T’nin bir alt türüdür.
Yani daha genel olarak, bir programdaki nesnelerin, o programın doğruluğunu değiştirmeden alt türlerinin örnekleriyle değiştirilebilmesi gerektiğini belirtir.
Örneğin AreaCalculator sınıfından yola çıkarak, AreaCalculator sınıfını extend eden yeni bir VolumeCalculator sınıfı düşünelim:
Şimdi SumCalculatorOutputter sınıfına yeniden bakalım:
Eğer şöyle bir örnek çalıştırmayı denersek:
$output2 nesnesinde toText metodunu çağırdığımızda, diziden string’e dönüştürme konusunda bir E_NOTICE hatası alırız.
Bunu düzeltmek için, VolumeCalculator sınıfında sum metodundan bir dizi döndürmek yerine direkt $summedData isimli değişken döndürebiliriz.
$summedData float, double ya da int olabilir.
Böylece liskov substitution prensibine uygun geliştirme yapmış oluyoruz.
Interface Segregation Principle
Bu anlaşılması kolay bir prensip.
Bir client asla kullanmadığı bir interface’i uygulamaya zorlanmamalı veya client’lar kullanmadıkları metotlara bağımlı olmaya zorlanmamalıdır.
Bir diğer değişle, yeni metotlar ekleyerek mevcut bir interface’e ek işlevler eklememeliyiz. Bunun yerine yeni bir interface oluşturalım ve sınıfımızın gerekirse birden çok interface uygulamasına izin verelim.
Yeni üç boyutlu kare prizma ve küre şekilleri olduğunu düşünelim. Şimdi bu şekillerin de hacimlerinin hesaplanması gerekecektir. ShapeInterface’te bu özelliği sağlamamız gerekecektir.
Başka bir yöntem eklemek için ShapeInterface’i değiştirirsek ne olacağını düşünelim:
Şimdi, oluşturduğunuz herhangi bir şeklin volume metodunu uygulaması gerekir. Ancak karelerin düz şekiller olduğunu ve hacimlerinin olmadığını biliyoruz. Bu nedenle bu interface, Square sınıfını hiç kullanmadığı bir yöntemi uygulamaya zorlar.
Böyle yaparsak interface segregation prensibini ihlal etmiş oluruz. Bunun yerine, “volume” özelliğine sahip ThreeDimensionalShapeInterface adında başka bir interface oluşturabiliriz. Üç boyutlu şekiller de bu arabirim ile uygulanabilir:
Bu çok daha iyi bir yaklaşımdır. Ancak isimlendirme için yeni bir interface oluşturmamız daha iyi olur. Bir ShapeInterface veya ThreeDimensionalShapeInterface kullanmak yerine başka bir interface, belki ManageShapeInterface oluşturabilir ve bunu hem düz hem de üç boyutlu şekillere uygulayabiliriz.
Böylece şekilleri yönetmek için tek bir API’ye sahip olabiliriz:
Şimdi AreaCalculator sınıfında, area metodu yerine calculate metodunu çağırabilir ve ayrıca nesnenin ShapeInterface değil ManageShapeInterface instance’ı olup olmadığını kontrol edebiliriz.
Böylece interface segregation prensibine uygun geliştirme yapmış oluyoruz.
Dependency Inversion Principle
Üst seviye modüller alt seviye modüllere bağlı olmamalıdır. Bağımlılıklar sadece abstract (soyut) kavramlara bağlı olmalıdır.
Bu prensibe uymak için, bağımlılık ters çevirme modeli(dependency inversion pattern) olarak bilinen ve çoğunlukla bağımlılık enjeksiyonu(dependency injection) kullanılarak çözülen bir tasarım modeli(design pattern) kullanmamız gerekir.
Dependency injection başlı başına ele alınabilecek bir konudur. Ancak kısaca, bir sınıfın herhangi bir bağımlılığının bir parametre olarak constructor aracılığıyla “enjekte edilmesiyle(injecting)” kullanılır.
PasswordReminder adında MySQL’e bağlanan bir sınıfımız olsun:
Burada MySQLConnection alt seviye bir modüldür, PasswordReminder yüksek seviyeli bir modüldür. Ancak SOLID’deki D’nin tanımına göre, somut kavramlara değil soyut kavramlara bağlı olmalıydı. PasswordReminder sınıfı MySQLConnection sınıfına bağımlı olmaya zorlandığından bu prensibi ihlal etmektedir.
Ayrıca sonradan kullandığımız veritabanını değiştrimek istersek PasswordReminder sınıfını da düzenlemeniz gerekir. Bu open-closed prensibini de ihlal etmiş olur.
PasswordReminder sınıfı bizim hangi veritabanını kullandığımızla ilgilenmemelidir. Üst seviye ve alt seviye modüllerimizin soyutlamaya (abstraction) bağlı olması için bir DBConnectionInterface oluşturabiliriz:
Interface bir connect metoduna sahip. MySQLConnection sınıfına bu interface’i implement edelim. Ayrıca, PasswordReminder constructor’ına doğrudan MySQLConnection sınıfını parametre olarak yazmak yerine DBConnectionInterface’i yazarsak, uygulamamızın kullandığı veritabanı türü ne olursa olsun PasswordReminder sınıfı veritabanına sorunsuz bir şekilde bağlanabilir ve open-closed prensibini ihlal etmez.
Şimdi bunu uygulayalım:
Şimdi kodumuzu hem üst seviyeli hem de alt seviyeli modüllerin soyut kavramlara bağlı olduğu hale getirdik.
Böylece dependency inversion prensibine uygun geliştirme yapmış oluyoruz.
Sonuç
Bu yazıda SOLID prensiplerini anlatmaya çalıştım. Spagetti kodlar bizden ırak olsun. :) Umarım faydalı olmuştur. Eğer bir hatam varsa ya da ekleyeceğiniz bir şey olursa belirtirseniz sevinirim, ben de öğrenmiş olurum. Güzel, kaliteli, faydalı bilgilerde buluşmak dileğiyle.
Kaynaklar