Photo by Marc-Olivier Jodoin on Unsplash

Teknik Muhabbetler #6 (Dependency Injection, Reflection & Abstraction)

Furkan Güngör
BilgeAdam Teknoloji
8 min readApr 5, 2021

--

“Bir grup Amerikalı yönetici otomobil üretim hattını görmek için Japonya’ya gitmişler. Hattın sonunda, otomobil kapılarının tıpkı Amerika’da olduğu gibi menteşelerle tutturulmuş olduğunu görmüşler. Fakat bir şey eksikmiş.
Amerika’da üretim hattında çalışan bir işçi, plastik bir çekiçle kapının kenarlarına vurarak kapının otomobile uyumlu hale gelmesini sağlıyormuş. Japonya’da ise, böyle bir işi yapan birisi yokmuş. Kafaları karışmış olan Amerikalı yöneticiler, hangi aşamada kapının araca uyup-uymadığının kontrol edildiğini sormuşlar. “Kapının yerine uyup-uymadığını, arabayı tasarladığımız sırada kontrol ediyoruz.” Japon firması, oluşan probleme göre bir çözüme gitmemiş, bunun yerine, sürecin başından beri istedikleri sonuca göre bir plan düzenleyip onu uygulamışlar ve sorunun doğmasını en baştan engellemişler.”

Bu yazıma yeni başladığım ve oldukça etkilendiğim Simon Sinek’in kaleme aldığı NEDEN İLE BAŞLA kitabından bir alıntı yaparak başlamak istedim. Yazının ilerleyen aşamalarında tekrar bu paragrafa atıflarda bulunarak daha iyi anlaşılmasını sağlayacağım. Yazının asıl konusuna geçmeden önce bu kitabı bana öneren Erol Kabadayı Hocama çok teşekkür ederim. Mesleğime ve olaylara karşı yeni bir bakış açısı kazanmamı sağladı. Eğer okumadıysanız şiddetle öneririm.📖

Uygulama geliştirirken sürekli olarak sorunlarla karşılaşırız. Karşılaştığımız sorunlar için ürettiğimiz çözümler sorunun temel kaynağına göre değişkenlik gösterir. Ancak ne olursa olsun ürettiğimiz çözümleri karşılaştığımız sorunun farklı türevlerinde tekrar kullanmak isteriz. Tam bu noktada aklımıza Abstraction kavramı geliyor. Şimdiye kadar karşılaştığım tüm projelerde farklı Abstraction yöntemlerinin kullanıldığını gördüm. Temelde hepsinin tek bir ortak noktası vardı, farklı senaryolar için tekrar kullanabilmek.

Ancak Abstraction uygulamayı rezil de eder vezir de. 🧐 Doğru Abstraction uygulamaya esneklik ve yeni gelen özelliklere karşı çeviklik sağlarken, yanlış Abstraction uygulama içerisinde gereksiz kod bloklarının oluşmasını sağlayacaktır. Bu sebeple doğru Abstraction yapmak biz geliştiricilerin önemli görevlerinden biridir.

Uygulamaları vezir veya rezil eden bir diğer konu ise Dependency yönetimidir. Geliştirme yaparken kullandığımız kütüphaneler, teknolojiler veya kurduğumuz yapılar günün sonunda bir dependency oluşturur. Oluşan bu dependency yönetimini iyi yapmak ve gereksiz bağımlılıklardan kurtarmak veya bağımlılıkları yönetmek yine biz geliştiricilerin önemli bir görevidir.

Örneğin; uygulamanızda bazı datalar için sürekli veri tabanına gitmek istemiyorsanız cache kullanarak bu sorunu çözebilirsiniz. Ancak hangi cache teknolojisini kullanacağınız önemli bir parametredir. (Memory or Distributed) Bu kararı aldıktan sonra karşımıza çıkan bir diğer problem ise cache provider seçimi olacaktır. (Redis, NCache etc.) Son olarak bu seçimi de yaptıktan sonra geriye kalan tek problem uygulama içerisinde bunun gerçekleştirilmesidir. Tam bu noktada dependency yönetiminin iyi yapılması gerektiğini düşünüyorum.

Biraz daha developer friendly bir şekilde konuyu özetlemek gerekirse uygulama içerisinde Memory Cache tercihini yaptıysak sonsuza kadar bu tercihi kullanacağız diye bir kural olmamakla birlikte farklı senaryolarda hızlıca provider değişikliği yapabilmeliyiz. Yani Abstraction yapmalıyız. Cache işlemleri tüm providerlarda aynıdır sadece uygulama şekli yani implementasyonları farklıdır. Eğer doğru bir Abstraction yapabilirsek Dependency Injection ile farklı senaryolarda hızlıca provider değişikliği yapabiliriz. Hadi gelin cache için doğru ve yanlış Abstraction yapalım. 🧐

Yukarıda gösterildiği gibi tüm cache işlemleri aslında ortak ama yanlış bir Abstraction yöntemi tercih edip her provider için yeni bir Interface açarak implementasyonları ayrı ayrı yaptık. Bu Abstractionyöntemi ileride yeni bir provider eklemek istediğimizde yeni bir Interface ve implementasyon yapmamız için bizi zorlayacaktır. Peki doğru Abstraction yöntemini nasıl uygularız?
Tüm cache işlemleri ortak olduğu için tek bir Interface tanımlayıp Remove Set ve Get işlemlerini ortaklaştırabiliriz. Yeni bir provider eklemek istediğimizde yapmamız gereken tek şey ICacheService isimli ortak Interface ‘i kullanarak ilgili implementasyonu tanımlamak olacaktır.

Madem bu kadar cache işlemlerinden bahsettik o zaman reklam yapmadan bu konuyu kapatmayalım. 😀 Cache işlemlerini ortaklaştırmak ve hiçbir cache provider’a göbekten bağlı kalmamak için EasyCache isimli bir kütüphane geliştirmiştim. Merak ettiyseniz kaynak kodlarını buradan ve konuyu daha detaylı özetlediğim yazıya buradan ulaşabilirsiniz.

2020 yılının Aralık ayında Bilge Adam’a transfer oldum. Bilindiği üzere Bilge Adam genellikle eğitim ve outsource yönleriyle tanınan bir teknoloji şirketi. Ancak görev aldığım ekip Bilge Adam’ın daha önce yapmadığı bir iş üzerinde çalışmakta. Bu ekip, FOW Apps (Focus On Work) isimli bir platformla farklı sektörler için birden fazla ürünü tek bir çatı altında toplayarak Bilge Adam’ı bir ürün şirketine dönüştürmeyi hedefliyor. Bu dijital dönüşümü gerçekleştirirken dikkat ettiğimiz konuların başında, doğru Abstraction ve kullandığımız providerların oluşturduğu bağımlılıkları doğru bir şekilde yönetmek geliyor.

Konuyu teorik örneklerle özetlediğime göre artık asıl soruna odaklanabiliriz. 🧐

Problemimiz şu; kullanıcılar sistem üzerinden notification göndermek istiyor ancak sistemin tek bir provider üzerinden çalışmaması gerekiyor. Yani kullanıcılar sistemin desteklediği notification sağlayıcılarından birini seçip kullanmak istiyorlar. Aslında konu çok basit, ilgili providerlar için implementasyonları yapıp kullanıcıdan configuration bilgilerini aldıktan sonra o meşhur metodu ben de yazacaktım. Evet doğru tahmin, hemen hemen tüm geliştiricilerin hayatında en az bir kere yazdığı o SendNotification metodu beni bekliyordu. 😀

Yazının ilk paragrafında NEDEN İLE BAŞLA kitabından yaptığım alıntı aklıma geldi. Kapıyı ürettikten sonra plastik bir çekiçle menteşeleri yerine oturtmak bir yöntem ancak planlama aşamasında arabaya uygun kapıyı üretmek daha cazip bir yöntem. Hangisini tercih etmeliyim diye kara kara düşünürken Japon abilerimizin taktiği daha çok hoşuma gitti. Planlama aşamasında bu konuyu ele almalıydım ve plastik çekiçle uğraşmak yerine ileride yeni gelecek providerlar için bir yapı hazırlamak daha cazip geldi. İşte yazının geri kalan aşamasında, Amerikan otomobil üretim planı ile başlayıp Japon üretim planı ile bitirmeyi hedefliyorum. 🧐(Amerika Japonya’yı kıskanıyor. 😀)

Öncelikle istenilen özellik için temel yapılarımızı oluşturalım. Kullanıcının hangi sağlayıcıyı seçtiğini veri tabanı üzerinde NotificationProvider isimli bir tabloda saklayalım.

Akla ilk gelen ve bence yanlış Abstraction olan yöntemi uygulayalım.

Her notification provider için farklı bir Abstraction uyguladım ve sonrasında if-else blokları ile kullanıcı hangi provider’ı kullanıyorsa o provider’ı ServiceProvider üzerinden çekip ilgili metodunu çalıştırdım.

Bu ilkel yöntem her zaman çalışır. Yanlış bir Abstraction olması çalışmayacağı anlamına gelmez. Ancak bu yapıda kötü kokan yerler var. Ekibe yeni katılan bir geliştiriciyi düşünün, görevi yeni bir provider eklemek. Bu arkadaşa nasıl yapması gerektiğini anlatalım. “Önce IxSender isimli bir interface oluşturmalısın. Ardından bu interface için bir implementasyon yazmalısın. Bitti mi? Hayır. Startup.cs içerisinde bunu Dependency Injectioniçin eklemelisin. Bitti mi? Hayır. Controller içerisinde bu provider için yeni bir if-else bloğu eklemelisin. İşte bu kadar böylece yeni bir provider ekleyebilirsin.” Bu konuşmayı yaptığınızda karşınızda size şaşkın gözlerle bakan geliştiriciler göreceğinize eminim. 😲 Yapacağımız iş bu kadar kompleks değilken neden bir provider eklemek için bir sürü sınıfa dokunuyorum. Yetmezmiş gibi yeni if-else blokları ekliyorum. Dışarıdan bakacak olursak Abstraction yaptık, Dependency Injection kullandık hepsi çok havalı kelimeler değil mi? 😀 Ancak yanlış Abstraction bizi rezil etti. Nerede Single Responsibility Principle , nerede Open-Closed Principle , nerede bu meşhur SOLID diye haykırmak istiyorum. 😀

If-Else blokları dışarıdan oldukça masum gözükür ancak çok sinsi yapılardır. Bu kodu debug ettiğinizi düşünün en az dört farklı senaryo var. Okunabilirliği yeni providerlar eklendikçe düşecek. Bu hipotezimi rakamlarla desteklemek için gelin Cyclomatic Complexity değerlerine bakalım.

Cyclomatic Complexity for NotificationController.cs

Bu gördüğünüz değerleri detaylı bir şekilde açıklamayacağım çünkü Offensive vs Defensive Programmingisimli bir yazı planlıyorum. O yazının içinde çok daha detaylı bilgi vermeyi hedefliyorum. Bu sebeple sadece Maintainability Index parametresine dikkat etmenizi istiyorum. Bu parametre ilgili kodun yapısına, içeride kullanılan if-else bloklarına ve kaç farklı case içerdiğine bakarak bir puan çıkarıyor. Şu an puanımız 63. Bakalım refactor ettiğimizde bu puan ne kadar değişecek.

Bu kodu beğenmediğimiz ortada, refactor çalışmalarına başlayalım. Bu sefer doğru Abstraction yapalım. Yani tek bir ISender isimli Interface ile tüm providerlar için implementasyon yapmaya çalışacağım. Ek olarak Controller içinde if-else blokları yazmak yerine IServiceCollection için yazacağım bir extension ile DI Container içerisine objeyi yerleştirirken karar verip hangi provider üzerinden çalışacağını belirleyeceğim.

Yukarıda belirtilen kod blokları ile en azından doğru bir Abstraction yaptığımızı söyleyebilirim. Şimdi Cyclomatic Complexity değerine tekrar bakalım.

Cyclomatic Complexity for NotificationController.cs and ServiceCollectionExtension.cs

NotificationController.cs için değerimiz arttı çünkü if-else bloklarını kaldırdık ve hangi implementasyonu kullanacağımızı IServiceCollection üzerine yazdığımız bir extension ile belirledik. Uygulama genelinde bir tane ISender olacak şekilde güncelledik. Artık tüm geliştiriciler bu arayüzü kullanabilirler. Ancak bu sefer IServiceCollection üzerine yazdığımız extension Maintainability Index değerimizi düşürdü. Yani if-else kaldırdık yerine switch-case koyduk ve kötü kodun yerini değiştirdik. Sonuç hemen hemen aynı. Hala kötü kokular geliyor burnuma. 😲 Yine ekibe yeni katılan bir geliştiriciye yeni bir provider nasıl ekleniyor anlatalım.
“Öncelikle ISender arayüzünden türeyen bir impelentasyon yapmalısın. Sonrasında IServiceCollectioniçin yazılan extensiona gidip yeni bir case eklemelisin. Sonrasında ISenderarayüzünü çağır ve kullan.” Bir önceki senaryoya göre daha az yere dokunan bir yapı oldu ancak yine provider eklemek için ilgili classtan çıkıp farklı classlara dokunuyoruz. Open-Closed Principle ilkesini hatırlayalım, Bir varlık kaynak kodunu değiştirmeden davranışını genişletebilmeli. Genişletilmeye açık ancak değişiklik için kapalı olmalı. Burada IServiceCollection için yazdığımız kaynak kod her seferinde değişikliğe uğrayacak ve bu ilkeyi yerine getirememiş olacağız.

Daha farklı nasıl bir implementasyon yapabilirim diye düşünürken Enis Necipoğlu ile bir beyin fırtınası yaparken ortaya bir hipotez atıldı. “Bir attribute ile ilgili yapıları işaretleyelim. Sonrasında bu işaretlemeyi kullanan yapıları bulur devam ederiz.” Attribute eklersek nasıl bir avantaj sağlarız sorusuna hızlıca cevap bulabildik. Yine ISender arayüzünden türeyen yapılarımız olur sonrasında bir attribute ile işaretleriz ve IServiceCollection üzerine if-else, swith-case yazmadan bu attribute’ü kullanan yapıyı DI Container içerisine verebilirdik. Böylece yazdığımız source code değişikliğe uğramaz. Artık yapmak gereken tek şey ISender arayüzünden türeyen bir implementasyon yazıp sonra tepesine bir attribute yerleştirmektir. Bu yapıyı düşünürken Volosoft şirketinin geliştirdiği AbpFramework içerisinde bulunan Blob Storage özelliğinden esinlendiğimizi belirtmekte fayda var. Bu özelliğin detayı dökümantasyonuna buradan ulaşabilirsiniz.

Kodu yazmak için sabırsızlanıyorum. 🧐

Yazının başlığında bulunan Reflection kelimesi şimdi karşımızda. Ne zaman esnek ve genişleyebilen bir yapı kurmak istesek yanı başımızda beliren Reflection yine iş başında. 😀
Artık ekibe yeni katılan arkadaşa yeni bir cümle kurabiliriz.
“Yeni bir provider eklemek için öncelikle ISender arayüzünden türeyen yeni bir implementasyon yazmalısın sonrasında SenderFlag attribute ile sınıfı işaretleyip veri tabanında name olarak nasıl kayıt edeceksen attribute içerisine parametre olarak verebilirsin.” Kaynak kod değişmeden genişleyebilen ve her sınıfın tüm gereksinimlerini kendi içerisinde bulundurduğu bir yapı oluşturduk. Bakalım Cyclomatic Complexity bize ne söyleyecek.

Cyclomatic Complexity for NotificationController.cs and ServiceCollectionExtension.cs

Biraz şaşırmış olabilirsiniz çünkü üst satırlarda biraz fazla övmüş olabilirim. Resimde görüldüğü gibi Maintainability Index değerini 62'den 55'e düşürmüş olduk. Sebebi Reflection kullandığımız için System.Reflection namespace altında bulunan classlara coupling yaptık ve satır başına çalışacak code sayısını yani maliyeti artırdık.😌 Ancak Cyclomatic Complexity değerini düşürebildik. Sebebi if-else, switch-case bloklarının olmaması tabii. Ne yazık ki her zaman hem cam kenarı hem de en ön sırada oturamıyoruz. Bazı yerleri daha okunaklı yapmaya çalışırken bazı yerlerden taviz verebiliyoruz. Eminim daha iyi bir çözümü vardır ancak ben bu yazı itibariyle aklıma gelen yöntemleri yanlıştan doğruya sıralamaya çalıştım. Amerikan olarak başlayıp Japon olarak bitirdim. 😀 Aklınıza gelen farklı bir yöntem varsa yorumlara yazmayı unutmayın. “Bilgi paylaştıkça çoğalır.” felsefesini bilgisayarımızın son flip flop devresine kadar yayalım. ⛏😀

Son olarak yine NEDEN İLE BAŞLA kitabından bir alıntı yaparak yazıyı bitirmek istiyorum. Kısa vadeli çözümler üzerine odaklanan şirketler için söylenmiş bir söz ama biz kısa vadeli çözümler üzerine odaklanan geliştiriciler odağında düşünelim ve dağılalım. 😀👋

“Bir şeyi ilk seferde doğru yapmak için zamanları ve paraları yok. Fakat her zaman aynısını yeniden yapmak için zamanları ve paraları var.”

--

--

Furkan Güngör
BilgeAdam Teknoloji

Solution Developer — I want to change the world, give me the source code.