Composition over Inheritance İfadesini Doğru Anlamak

Yazılım mühendislerinin duyduğu önemli tavsiyelerden bir tanesi de “Composition Over Inheritance”. Ama ben bu ifadenin anlatılmak istenilen çözümü tam manasıyla ifade ettiğini düşünmüyorum. Yeniden tanımlamak gerekirse “Aggregation with Inheritance over Inheritance” olması lazım. Yok eğer composition over inheritance literal manada ele alınırsa karşımıza ciddi sorunlar çıkarıyor.

Aggregation vs. Composition

Önce bu ikisi arasında farkı doğru anlamak lazım. En kolay da kod ile anlatılacağından aşağıda ki kodları inceleyelim:

Hemen not düşeyim, yukarıda inheritance durumu yok. Neyse. Daha önce benzer kodları görmüşler arada ki farkı hızlı bir şekilde anlayacaklardır. Ama biz yine açıklayalım. Şöyle hızlıca baktığımızda Aggregation mantığında bir sınıfı oluşturan diğer structural tiplerin (sınıflar gibi) dışarıdan geldiğini ve dolayısıyla bunların hayat sürelerini (yani memory’de kalma sürelerini) belirleyecek olan sınıfın Account olmadığını görüyoruz. `Account` garbage collector tarafından yok edilse bile `ProfileInfo` ve `Login` tiplerinden oluşturulmuş instancelar yaşamaya devam edilebileceklerdir.

Ama composition yaklaşımına baktığımızda işlerin değiştiğini görüyoruz. `Profile` ve `Login` instancelarını oluşturan Account sınıfı olduğundan hayatları yine Account sınıfının yaşamasına bağlıdır.

Herhalde bu kısım anlaşılmıştır. Şimdi geçelim gerçek konuya.

Composition Over Inheritance

Bu ifade hem dar bir manaya geldiği gibi hem de anlatılmak istenilen çözümü tam ifade etmiyor. Composition başlı başına yukarıda ki açıklamaya baktığımızda bir sorun. Nedenlerinden bir tanesi ve belki de en bariz gözükeni unit testable kod yazmayı engellemesi yada cidden zorlaştırması. Çünkü ihtiyaç duyulan sınıfları dışarıdan alamadığınız için onları mock etmeniz de zorlaşmış hatta dillere göre imkansızlaşmış oluyor. Hatta mock edilememe gerçek sorunun symptomlarından sadece bir tanesi. Bu sorunun çözümünün ilk aşamasını ise aggregation oluşturuyor. Onun için bundan sonra ki kısımlarda composition kelimesini kullanmayacak ve artık Aggregation diyeceğim. Çünkü aslında Composition ifadesi yerine anlatılmak istenilen çözümde kullanılması gereken kelime daha baştan aggregation olmalıydı. Ama aggregation bile tek başına composition ile gelen sorunları çözmüyor. Sadece doğru çözüme bir adım yaklaşmayı sağlıyor. Oun için ileride sıraladığım bazı sorunlar composition tarafından da paylaşılıyor.

Diğer bir sorun ise sanki Aggregation yaparsanız hiç inheritance kullanmıyormuşsunuz havasının oluşması. Hayır, kullanıyorsunuz! Aksi halde fazlasıyla coupled yazmış oluyorsunuz. “Fazlasıyla” dedim çünkü hiç coupled olmayan kod yazmak hem neredeyse imkansız hem de saçma bir hedef olurdu. Aslında bu algının oluşmasında biraz da Composition kelimesinin seçilmesi var. Aggregation da depend olunan sınıfların dışarıdan alınması akıllara Dependency Injection getirdiğinden eğitilmiş zihinler de olayın içinde inheritance olduğu gerçeğini hızlıca gözler önüne seriyor.

Aksi halde?

Peki inheritance kullanmadığınız ve aggregation yapmak istediğiniz durum da ne gibi sorunlar ile karşılaşacaksınız, bunları kısaca aklıma geldiği ölçüde özetleyelim:

  1. OCP Violation: OCP Open Closed Principle demek. Ne olduğunu bilmiyorsanız burada ki videomu izlemenizi tavsiye ederim. Sınıfınızları extend etmek istediğiniz bunu var olan sınıflarınızı modify etmeden yapamayacaksınız. Bunun şu an aklıma gelen iki major sorunu oluşturma durumu var. Birincisi çalışan bug-free kodlarınızı bozma durumunuz. Yıllarca kullanılmış ve tabiri caizse bullet-proof yani kurşun geçirmez hale getirdiğiniz ve farklı testler ile her açıdan test ettiğiniz sınıfılarınızı modify etmek ile onları bozma ihtimaliniz. Bu aynı zamanda var olan bu sınıfları yeniden test etmeye ve aynı seviyede güven kazanmaları için belkide yıllarca farklı use case’ler ile kullanmanızı gerektirecek bir süreç olacak. OCP aslında bu sorunun oluşmaması için bir tavsiye niteliğinde bir prensip. İkinci ciddi sorun ise binary compatibility. Yani modify ettiğiniz sınıflarınızı içeren assembly’ler ve bu assemby’leri kullanan herşeyin yeniden derlenmesi ve tüm application server’lara deploy edilmesi. Bir de bunu manual yapıyorsanız Allah yardımcınız olsun. Manual deployment en kısa tabiri ile yazılım mühendislerini gece kan ter içinde uykularından uyandırması gereken karanlık kabusları olmalı. Değilse bile bu şekilde bakılmalı. Neyse daha fazla uzatmayalım. Durum bu.
  2. Code Duplication: Biraz düşününce bunun çok bariz bir sorun olarak karşımıza çıkacağını görmüşsünüzdür. Inheritance kullanmadığınzdan dolayı, code reusability ciddi oranda düşecektir. Çünkü aynı kodları başka sınıflar ile çalışabilir halde yazmanız gerekecektir. Bu aslında generics ile farklı bir açıdan çözüme kavuşmuş bir durumdur. Ama ortak bir contract üzerinden kod yazılması paha biçilmez bir kolaylaktır mühendisler için. Bunu daha fazla açıklama gereği duymuyorum. Ama şunun bilinmesi lazım ki code reusability düştüğünde daha farklı sorunların çorap söküğü olarak geleceğinin bilinmesi. Örneğin code maintainability düşüceğinden daha fazla zaman alan deadline süreleri, düşen moral, giden para ve zaman, kaybedilen müşteriler… Daha ne olsun!
  3. Almost impossible unit testing: Aslında cevap çok basit: Nasıl mock/stub edeceksiniz? :)

Bazı diller mesela Go gibi, C# yada Java gibi dillerdekine benzer explicit inheritance sunmasalar da, benzer bir şey olan duck typing sunuyorlar. Ama nedeni herhalde yukarıda ki 2. maddeden rahatlıkla çıkarılabilinir.

Peki doğru tabir?

Aggregation with inheritance over inheritance. Evet daha uzun oldu. Ama anlaşılır oldu. Peki nasıl gözükecekti bu ifade? Hepiniz buna çok aşinasınız ama ben yine göstereyim:

Bu kadar basit çözümlerin arka planda nice sorunları engellemesi çok güzel bir şey. Yukarıda ki çözüm işte bunlardan bir tanesi. Bu arada C# ta ki interface isimlendirme de idiomatic yaklaşımı bilmeyenler için diyeyim: Interface isimlerinin genelde başlarına `I` harfi konulur. Bu onların interface olduğunu gösteriyor. Zorunlu değilsiniz ama bence güzel bir practice.

Yukarıda ki yaklaşımın farklı design pattern’ları enable ettiğini de unutmadan söyleyeyim: Bunlardan bazıları decorator ve strategy. Aslında yukarıda ki yaklaşım OOP içinde ki patternların çoğuyla yakından veya uzaktan ilgili. Dependency Injection da yukarıda ki yaklaşım sayesinde varlık sahasında kendisine doğru bir yer bulabiliyor.

Bir sonra ki yazımda görüşmek üzere…