Evet arkadaşlar nihayet SOLID prensiplerinin son halkasına erişmiş bulunmaktayız. SOLID serisi ile ilgili diğer yazılarıma, SOLID prensipleri genel anlamda neye hizmet ediyor için de ilk yazıya aşağıdaki bağlantılardan başvurabilirsiniz:
- SOLID 1 – Tek Sorumluluk Prensibi
- SOLID 2 – Açık/Kapalı Prensibi
- SOLID 3 – “Liskov Substitution” Prensibi
- SOLID 4 – Arayüz Ayrıştırma Prensibi
- SOLID 5 – Bağımlılıkların Ters Çevrilmesi Prensibi
Bağımlılıkların Ters Çevrilmesi Prensibi
Bu yazımda, sonuncu prensip olan Bağımlılıkların Ters Çevrilmesine (“Dependency Inversion Principle“) göz atacağız.
Prensibin detaylarına geçmeden önce, aslında bu prensiple de yakından ilgili olan ve karşımıza kötü tasarım olarak çıkan bir takım hususlardan bahsetmek iyi olacak sanırım. Daha önceki SOLID yazılarımda da ifade ettiğim üzere, bu tarz prensiplerin en önemli amacı, yazılımlarımızda ortaya çıkabilecek ve bize daha sonra sıkıntı çıkarabilecek problemlerin önüne geçmektir. Ne yazık ki bunların bir çoğu genelde projede, ilerleme sağlandıktan sonra farkedilir ve bunlara ilişkin alınacak tedbirlerin de maliyeti fazla olabilir. Peki nedir bu sıkıntılar ve bunları nasıl anlayabiliriz.
Bir uygulamada geliştirilen kabiliyeti, bileşeni ya da ilgili bir parçayı, mevcut bağımlılıklarında ötürü kullanamama, ya da en ufak bir değişiklikte bile bir çok yazılım parçasının bundan etkilenmesi ve değişikliğin izole edilememesi. Gerçekleştirilen değişiklik sonucu, beklenmedik şekilde baş gösteren problemler/hatalar. İşte bütün bunlar bize aslında, tasarımsal bir sıkıntının olduğuna ilişkin uyarılar olarak niteleyebiliriz. Bunlar gibi elbette bir çok daha etmen sayabiliriz ama en önemlilerinin bunlar olduğunu ifade edebiliriz.
Şimdi gelelim prensibimize. Aslında bu prensibin ismine bakarak da kendisine dair bir fikriniz oluşmuş olabilir. Bir kere, bağımlılıkların karşısında olduğu sanırım açık 🙂
Diğer prensiplerde olduğu gibi, bu prensip için de, Bob Martin’in ünlü yazısında ifade ettiği açıklamalara bakalım:
High level modules should not depend upon low level modules. Both should depend upon abstractions.
ve
Abstractions should not depend upon details. Details should depend upon abstractions.
Yani kısaca:
- Üst seviye modüller, kabiliyetler, sınıflar, alt seviye birimlere bağımlı olmamalı. Her iki seviye birimler de soyutlamalara bağımlı olmalıdırlar,
- Soyut arayüzler, sınıflar, kabiliyetler, detaylara kabiliyet, sınıflara bağımlı olmamalıdır. Tam tersi, detay sınıf ve kabiliyetler soyut sınıflara bağımlı olmalıdır.
Burada geçen alt seviye ve üst seviye modül ve kabiliyetlerden neyi kast ettiğimizden kısaca bahsedelim:
- Alt seviye modül: Spesifik ve somut işlevleri gerçekleştiren, temel kod parçaları olarak ifade edebiliriz. Özel olarak, alt seviye API çağrıları, grafiksel kulanıcı bileşenleri ya da protokol metotları (ör. soket API’leri, OpenGL çağrıları vb.)
- Üst seviye modül: Alt seviye işlevleri kullanarak, asıl uygulama işlevlerini gerçekleştiren kod parçaları olarak ifade edebiliriz. Ör. Alt seviye soket kodlarını kullanan çok oyunculu sunucu kabiliyeti ya da alt seviye OpenGL çağrılarını kullanan arazi görselleştirmesi kodları.
İsim olarak, neden ters çevirme denildiğine gelecek olursak, bunun kökleri aslında eskilere, yapısal programlama zamanlarına dayanmakta. Genel olarak yapısal programlama ve tasarımda, yazılım parçaları (üst seviye), daha küçük olan bileşenleri düşünerek, onlara bağımlı olarak oluşturulmakta ve de soyutlamalar da, bunları gerçekleyen detaylara bağımlı geliştirilmekteydi. İşte bu ters çevirme ibaresi de tam da bunu tersine çevirmek için öne sürülmüş bir prensiptir.
Şimdi yukarıda bahsettiğimiz yönde (üst seviyeden alt seviyeye doğru) bir bağımlılık durumunda neler olabileceğine de kısaca bir göz atalım. Direk bağımlılık olması durumunda, alt seviyede gerçekleşecek olan herhangi bir değişiklik, üst seviye modülleri de direk etkileyecek, onları da değişmeye zorlayacak ki bunlar aslında iş mantıklarını ve işleyişlerini içerdikleri için değişmelerini çok istemiyoruz. Uzun vadede de, aslında bu tarz üst seviye kabiliyetlerin tekrar kullanılması istenilen bir özelliktir.
İnternette ve kaynaklar kısmında verdiğim sitelerde, farklı programlama dilleri için bu prensibin nasıl uygulanabileceğine dair örnek kodlara erişebilirsiniz. Ben burada, Bob amcanın makalesinde verdiği örneği sizler ile paylaşacağım. Siz de muhakkak bu makaleye göz atmayı unutmayın.
Şimdi bunları, yinen Bob amcanın makelesinde verdiği örnek üzerinden inceleyelim. Örnek, basitçe klavyeden girilen verileri, yazıcıya gönderen bir programı içeriyor. Aşağıda bu uygulamaya ilişkin genel yapıyı görebilirsiniz:
Çok da karmaşık olmayan bu uygulama kapsamında, Copy kabiliyeti, diğer iki kabiliyeti kontrol ediyor, ilgili metotlarını kullanıyor. Basitçe aşağıdaki gibi bir kodun çalıştığını düşünebilirsiniz:
1 2 3 4 5 6 |
void copy() { int c; while ((c = readKeyboard()) != EOF) writePrinter(c); } |
Burada her ne kadar, klavyeden veri okuma ve yazıcıya veri gönderme kabiliyetleri, diğer işlevler için kullanılabilecek olsa da, kopyalama işlevi, tekrar kullanılabilir değil. Peki neden? Çünkü şu anda alt seviye iki kabiliyete de göbekten bağlı. Örneğin, kopyalama kabiliyetinin, klavyeden okunacak olan veriyi diske yazacağını düşünelim. Mevcut durumda yapılabilecek, kopyalama kabiliyetinin içeriğini değiştirip, diske yazma için de ek kontroller ekleyerek yapılabilir ama bu da pek matah bir çözüm olmayacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Hedefleri coklayalım enum class OutputDevice { ePrinter, eDisk }; void copy(OutputDevice dev) { int c; while ((c = readKeyboard()) != EOF) { if (dev == OutputDevice::ePrinter) writePrinter(c); else writeDisk(c); } } |
Bu durumda yapabileceğimiz aslında, üst seviye kopyalama kabiliyetini direk klavye ve yazıcıya bağımlı olan durumdan kurtarmak olacaktır ki, bunu da aşağıdaki gibi soyut sınıfları tanımlayarak yapabiliriz.
Ne yapmış olduk? Burada artık kopyalama kabiliyeti direk somut sınıflara bağımlı olmaktan çıkarıp arayüzlere bağımlı hale getirdik. Ve benzer şekilde de, klavye ve yazıcı sınıflarını da aynı arayüzlere bağımlı yaptık. Artık kopyalama kabiliyetini bu arayüzleri sağlayan farklı veri sağlayıcısı ve veri yazıcıları ile herhangi bir modifikasyon yapmadan kullanabileceğiz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Reader { public: virtual int read() = 0; }; class Writer { public: virtual void write(char) = 0; }; void Copy(Reader& r, Writer& w) { int c; while((c = r.read()) != EOF) w.write(c); } |
Örneğimize de baktığımıza göre şimdi diğer yazılarımda olduğu gibi bu prensip ve ilişkili SOLID prensiplerine dair de bir kaç kelam etmek istiyorum.
Bu noktada biraz durup hangi prensiplerin, bu prensipler ile ilgili olabileceğini biraz düşünmenizi isteyeceğim. Evet, yeterince düşündüyseniz, bunların, Açık/Kapalı ve “Liskov Substitution” prensipleri olduğunu hatırlayacaksınız. Aslına bakarsanız, bu iki prensibi kodunuzda uyguladığımız zaman aslında kodumuzun bağımlılıkların ters çevrilmesi prensibini de sağlayacaktır. Peki nasıl?
Hemen kısaca açıklayalım. Öncelikle Açık/Kapalı prensibini hatırlayalım. Bu prensibi sağlamak için sınıflarımıza arayüzler tanımlayıp, bu arayüzleri farklı şekilde gerçekleyerek, bileşenlerimizi genişleyebilir hale getiriyorduk. “Liskov Substitution” prensibinde ise, somut sınıflarınız, arayüzleri doğru bir şekilde gerçekliyor ve bu arayüzlerin kullanıldığı yerlerde, uygulamanız açısından herhangi bir probleme yol açmamalıdır.
Peki kodlarken bu prensibi nasıl göz önünde bulundurabiliriz diye düşünecek olursak. Kısaca aşağıdaki kalemlere dikkat etmek, bu anlamda yardımcı olacaktır:
- Olabildiğince sınıflardan ziyade, arayüzlere, soyut sınıflara (“abstract class”) bağımlı olmak,
- Referans ve işaretçilerin türünü olabildiğince soyut sınıf ya da arayüz olarak belirlemek,
- Sınıflar, soyut sınıf ya da arayüzlerden türetilmeli,
- Türetilen sınıflarda bulunan fonksiyonlar, üst sınıfların ilgili fonksiyonlarını geçersiz kılmamalı/yeniden tanımlamamalı (“override”).
Yazımın sonuna gelirken, emin sizlerin de bir kısmınızın da, benim gibi benzer bir takım kavram ile karşılamışsınızdır. Bunlardan en önemlisi de sanırım, “Dependency Injection (DI)”. Bir diğeri ise “Inversion of Control (IoC)”. Bu kavramların detaylarına burada girmeyeceğim ama bunlara ilişkin güzel bir makaleye sizlerin yönlendirmek istiyorum. Bu makaleye şu adresten ulaşabilirsiniz:
https://martinfowler.com/articles/dipInTheWild.html#YouMeanDependencyInversionRight.
Bu makalede kısaca ifade edildiği üzere, DIP genel olarak bağımlılık ilişkisinin şekli, DI bağlama yöntemi ve IoC ise bu ilişkin yönü ile alakalı bir kavramdır. Bu arada makale DIP’in kullanımına ilişkin de çok güzel örnekler barındırıyor.
Sonuç olarak bakacak olursak, DIP, nesne yönelimli teknoloji için vaat edilen bir çok faydadan yararlanabilmek için önemli bir prensip olarak karşımıza çıkmakta. Özellikle yazılım kabiliyetlerin tekrar kullanılmasının yanında, gerçekleşecek olan değişikliklerin etkisinin de izole edilerek azaltılmasına yardımcı olacak çok önemli bir prensiptir.
Kaynaklar:
- https://www.youtube.com/watch?v=kMFQ3Gh4NHI
- https://martinfowler.com/articles/dipInTheWild.html#YouMeanDependencyInversionRight
- https://www.wikiwand.com/en/Dependency_inversion_principle
- https://drive.google.com/file/d/0BwhCYaYDn8EgMjdlMWIzNGUtZTQ0NC00ZjQ5LTkwYzQtZjRhMDRlNTQ3ZGMz/view
- https://www.youtube.com/watch?v=5WHKNOTqwsA
- https://www.tomdalling.com/blog/software-design/solid-class-design-the-dependency-inversion-principle/
- https://www.labri.fr/perso/clement/enseignements/ao/DIP.pdf
- https://d3s.mff.cuni.cz/f/teaching/nprg043/05-principles.pdf