Dependency Injection Nedir❓ Faydaları❓ Kodlama Yöntemleri❓| Kod örnekleri ile

Kaynak

Giriş

Selamlar 👋. Bu makalede Dependency Injection hakkında konuşacağız. Dependency Injection Nedir❓ Neden önemsemeliyiz❓ Faydaları nelerdir❓ Dependency Injection uygulama yöntemleri nelerdir gibi soruların cevaplarını arayacağız.

Dependency (Bağımlılık) Nedir?

Gerçek dünyada, bazı görevleri tamamlamak için bazı araçlara, nesnelere, eşyalara ihtiyacımız var. Bu nedenle, gerçek dünyayı nesneler olarak programlamada modellerken, bazı şeyleri yapmak için başka nesnelere ihtiyacımız var. Yani asıl işimizi tamamlamak için başka nesnelere, süreçlere, işlevlere vb. bağımlıyız. İşte dependency tam olarak budur. Bağımlılık.

Dependency, bir şeyleri tamamlamak için ihtiyaç duyduğumuz bazı gerekli nesnelerdir.

Kendi dependency’lerini kendisi ayağa kaldıran bir sınıf örneği

Örneğin; gerçek dünyada bir geliştiricinin (developer)☕, 💻, IDE (emojiyi bulamadım😊) ihtiyacı vardır. Bu geliştiriciyi programlamada modellerken, geliştirici sınıfının bunlara sahip olması gerekir. Yukarıdaki sınıfta, Developer sınıfının bir Coffee sınıfı nesnesine, bir Computer sınıfı nesnesine, bir IDE sınıfı nesnesine ihtiyacı vardır. Yukarıdaki sınıfta coffee, computer ve integratedDevEnv dependecy’lerdir.

Dependency’leri oluşturma yöntemleri

Self Construction

Bu yöntemde sınıf ihtiyaç duyduğu bağımlılıkları kendisi ayağa kaldırır. Örneğin yukarıdaki Developer sınıfı, coffee, computer, integratedDevEnv bağımlılıklarını kendi başına oluşturur ve ayağa kaldırır.

Injection

Bu yöntemde, dependencyler sınıfın dışından sınıfa gönderilir, inject edilir. Aşağıda gördüğünüz gibi sınıf, oluşturulurken ihtiyaç duyduğu dependencyleri talep ediyor.

Dependencylerinin dışarıdan inject edildiği sınıf

💉Dependency Injection Nedir❓

Dependency Injection, dependecylerin oluşturulmasını ve kullanımını ayırmamızı sağlayan bir tasarım kalıbıdır. Bu sayede gerekli dependencyleri ilgili sınıfta oluşturmak yerine runtimeda inject edebiliriz. Dependencyleri kolayca yönetmemizi sağlar. DI aynı zamanda SOLID’in 5. ilkesi olan Dependency Inversion’un uygulanması olarak da düşünebiliriz.

🤔💉Dependency Injection’u neden önemsemeli ve kullanmalıyız❓ Faydaları nelerdir❓

Self Construction gerçekten çok basittir. Büyük bir çaba, maliyet gerektirmez. Ve ilgili sınıf— örn. Developer— bağımlılıkların nasıl ve ne zaman oluşturulacağı konusunda tam kontrole sahiptir. Ancak aşağıdaki gibi bazı dezavantajları vardır;

  • Single Responsibility Prensibini ihlal etme; Developer sınıfının bağımlılıkları nasıl oluşturacağını ve bunları birlikte nasıl kullanacağını bilmesi gerekir. Bu çok fazla sorumluluk. Her sınıf sadece bir görevden sorumlu olmalıdır.
  • Test edilememe; A) Bir unit test patladığında bunun asıl sebebini bulmamız zorlaşır. Asıl sebep Developer sınıfı mı yoksa Developer sınıfının runtime’da oluşturduğu dependencylerin sınıfları mı olduğunu anlamak ekstra efor ister. B) Dependencyler runtime da değiştirilemezler (replacability). Dolayısıyla farklı test senaryoları için dependencylerin farklı varyasyonlarını kullanmak istediğimizde bunu yapamayız. Çünkü dependencyler hardcoded olarak ayağa kaldırıldıkları için ilgili sınıf test double’ları kullanmamıza izin vermez. Örneğin bir Developera Espresso yerine Latte, Matebook yerine Mac, Android Studio yerine VSCode verdiğimiz bir senaryo üzerinden Developerın verimliliğini ölçmek istersek bunu yapamayız. Çünkü bağımlılıklar replaceable değiller.
  • Tightly Coupling; Developer sınıfı, Coffee, Computer, IDE sınıflarına sıkı sıkıya bağlıdır. Dolayısıyla Developer sınıfını kullanmak istediğimizde Coffee, Computer, IDE sınıflarını kullanmamamız gerekiyor. Ayrıca bunları birinin diğer varyantlarıyla değiştiremeyiz. Ve dependency sınıflarında bir değişiklik meydana geldiğinde, tüm bağımlı sınıfların değişmesi gerekir. Bu yüzden bakımıda zordur.

Dependency Injection ile yukarıdaki sorunları çözmeye çalışıyoruz. Bununla beraber daha fazla zamana ihtiyaç duyma gibi dezavantajları da vardır.

Dependency Injection kullanmanın test yazmayı kolaylaştırma, kodun tekrar kullanılabilirliğini arttırma, refactor’u kolaylaştırma, boilerplate kodu azaltmak, yapının daha loose coupled olmasını sağlamak gibi faydaları vardır.

Hadi bazılarına daha detaylı bakalım 🚀.

Testi Yazmayı Kolaylaştırma

DI’nin en önemli faydalarından biri, testi kolaylaştırmasıdır. Peki bu nasıl daha kolay hale geliyor? Sadece sihirli bir değnek mi? Testin aşama aşama nasıl daha kolay hale geldiğine gelin beraber bakalım ve DI’nin yan faydalarını da görelim.

Single Responsibility’e teşvik etmek; DI, sorumlulukları ayırmamız için bizi biraz zorlar. Her sınıf kendi işlevselliğinden sorumlu olmalı dependencyleri oluşturmak gibi bir durumu düşünmemelidir.

Sıkı bağları kırma; Sınıfların sorumluluklarını ayırarak birbirine sıkı bağlı olan sınıflar yerine daha bağımsız sınıflara sahip olacağız.

Sınıfları değiştirilebilir hale getirmeye teşvik etmek; DI, dependency inversion’ı uygulamamızı ve üst düzey abstractionlara bağlı olmamızı teşvik eder. Bu sayede sınıflarımızın farklı varyasyonlarını hazırlayabilir ve instancelarını ihtiyaç duyulan sınıflara paslayabiliriz. Böylece dependencyler farklı varyasyonlarla kolayca değiştirilebilir hale gelir. Daha iyi anlamak için bir örnek yapalım.

Dependencylerini kendisi oluşturan bir sınıf örneği

Yukarıdak sınıf dependencylerini kendisi yönetiyor. Bunun yanında kendi diğer sorumluluklarını yerine getiriyor. Hadi Coffee, Computer, IDE dependency sınıflarımızı soyutlayalım.

Dependency Inversion uygulanarak soyutlanmış sınıflar

— Aslında kahveden yudum almak Coffee sınıfının sorumluluğunda değildir. Ama basitlik için buraya ekleyelim —

Sınıflarımızı soyutladık. Coffee sınıfının Espresso, Americano, Latte gibi varyasyonlarına ihtiyacımız olduğunda, sadece bu ilgili interface’i uygulamamız yeterli olur. IDE ve Computer sınıfları içinde aynı şey geçerli. Artık farklı varyasyonları aşağıdaki gibi kolaylıkla hazırlayabiliriz.

Sınıfların farklı implementasyonları

Tamam, sınıflarımızı soyutladık peki replaceability nasıl çalışıyor?

Farklı dependency implementationlarını aynı sınıfa (MobileDeveloper) paslama

Bir MobileDeveloper — veya BackEndDeveloper — oluşturmak istediğimizde, ona herhangi bir Coffee nesnesini veya herhangi bir Computer nesnesini veya herhangi bir IDE nesnesini iletebiliriz. Dependencyler kolayca değiştirilebilir. Ayrıca dependencyleri farklı developerlar için aşağıdaki gibi yeniden kullanılabiliriz.

Farklı developer instanceları için aynı bağımlılıkları tekrar kullanabilme

Dependency Inversion ve Dependency Injection kullandığımız için artık sınıflarımız çok esnek. High-level abstractionlara bağlıyız. İhtiyacımıza göre runtimeda spesifik bir implementationu inject edebiliyoruz.

Test yazmayı kolaylaştırma; Dependencylerin kolayca değiştirilebilmesi sayesinde farklı senaryoların taklit edilmesi için sınıflarımıza kolayca fake instancelar enjekte edebiliriz.

Developerlara farklı instancelar paslayarak test yapma

Kodun Tekrar Kullanılabilirliğini Arttırma

  • Dependencyleri defalarca kullanılabiliriz.
  • Sınıfların sadece 1 fonksiyonelliğe sahip olması sayesinde sınıfları farklı alanlarda tekrar kullanabiliriz.

Refactor Yapmayı Kolaylaştırma

  • Sınıflarımız artık ayrılmış ve izole durumdalar. Bu sayede bir yerde yapılan değişikliğin minimum yeri etkilemesi sağlanmış oluyor.
  • Firebase’den HMS Core’a geçiş gibi bir implementation detayını değiştirmek istersek, kolayca yeni bir sınıf oluşturabilir ve dependency providing centerımızdan (ör; Hilt’te Module classları) yeni sınıfın bir instanceını inject edebiliriz.

DI’nin yukarıdaki gibi bazı faydaları vardır.

💉🛣️ Dependecy Injection Uygulama Yöntemleri❓

Field Injection / Setter Injection

Sistem/library/framework tarafında ayağa kaldırılan sınıfların dependencilerini setter fonksiyonlarıyla veya builder pattern aracılığıyla pasladığımız yöntemdir.

Field / Setter Injection Örneği

Constructor Injection

Bu yöntemde bir sınıfın nesnesini oluştururken ihtiyaç duyduğu gerekli dependencyleri paslarız. Bunu aşağıdaki gibi 2 şekilde yapabiliriz.

Manuel Constructor Injection

Hedef sınıf örneğimizi oluşturmadan önce onun dependencylerini hazırlarız. Ardından, hedef sınıfın nesnesini oluştururken bağımlılıkları paslarız.

Manuel Constructor Injection

MobileDeveloper sınıfından bir nesne üretebilmek için daha öncesinde ihtiyacı olan dependencyleri oluşturmamız gerekir.

Dezavantajlar

  • Çok fazla boilerplate kod var. Bir sınıfa her ihtiyaç duyduğumuzda dependencylerini manuel olarak oluşturmamız gerekiyor.
  • Single Responsibility prensibini ihlal eder. Bir sınıf MobileDeveloper sınıfını kullanmak istediğinde hem dependencyleri yönetmekten hem de kendi işini yapmaktan sorumlu olması gerekecektir.
  • MobileDeveloper sınıfındaki bir değişiklik birçok alanı etkileyecektir. Bu yüzden bakımı zordur. Yönetim zordur.
  • Hangi sınıfın hangi dependecylere ihtiyacı olduğunu bilmemiz gerekir veya gerekli bağımlılıklar için sınıfa kontrol etmemiz gerekir.

Sizde içten içe

Bu ne ya böyle 💩. Altı üstü bir instance oluşturup, onu kullanarak asıl işime odaklanmak istiyorum. Ne kadar çok şey çıkıyor.

diyor musunuz?

Evet, tamamen haklısınız. Katılıyorum🤗 Ayrıca, bunu çözmek için size bir çözüm önerim var. Bir sonraki başlığa geçelim.

Automated Dependency Injection via Libraries

Bir önceki başlıkta bağımlılıkları kendimiz oluşturduk, pasladık ve yönettik. Ama bu işleri bizim adımıza yapacak çeşitli kütüphaneler var. Bunların pek çok özelliği vardır ama hemen hemen hepsinde 2 temel özellik vardır.

1.Her bağımlılığın nasıl provide edileceğini tanımlamamıza izin verirler. Syntax vb. kullandığınız kütüphaneye bağlı değişebilir.

Android Geliştirmede Hilt ile Dependencyleri Sağlama

Her fonksiyon bir dependencyden sorumludur. MobileDeveloperın bir nesnesine ihtiyaç duyduğumuzda DI kütüphanesi provideCoffee() fonksiyonunu çağırarak Coffeenin bir nesnesini oluşturup provideMobileDeveloper() fonksiyonuna paslayacak. Benzer şeyler computer ve ide parametreleri için de gerçekleştirilir. Gerekli tüm parametreler (dependencyler) oluşturulduğunda provideMobileDeveloper() fonksiyonu MobileDeveloper sınıfının bir nesnesini oluşturup return eder. Her ihtiyaç duyduğumuzda DI kütüphanesi provideMobileDeveloper() tarafından sağlanan nesneyi bize verecektir.

2.İhtiyacımız olan, inject edilmesini istediğimiz nesneleri/dependencyleri belirtebilmemize imkan tanırlar. Biz sadece ihtiyacımız olan nesneleri belirtiriz. Sonrasında library boilerplate kodları kendisi oluşturup bize dependencyi sağlar. MobileDeveloperın bir nesnesine ihtiyacımız olduğunda libraryi haberdar etmemiz yeterlidir. Dependecylerin nasıl ayağa kaldırılacağını hiç düşünmeden direkt kullanmaya başlayabiliriz. Çok iyi, dimi😎?

(Android Dev) Hilt ile değişkenleri inject etme

Avantajlar

  • Dependecyleri oluşturma ve kullanma ayrılmış oldu. Dependecyleri sağlama katmanı ayrı, net ve anlaşılması kolay oldu. Uygulamanın diğer bölümleri dependecyleri düşünmez. Sadece işlerine odaklanırlar.
  • Kolay bakım yapılabilirlik, çünkü bağımlılıkların provide edilmesi bazı dosyalarda merkezileştirilmiştir. Ve uygulamanın geri kalanı burayı kullanıyor. Bu nedenle, provider fonksiyonunu değiştirdiğimizde, onu kullanan tüm kodu otomatikmen etkileyecektir. Yani manuel olarak yeniden düzenlemeye gerek olmayacaktır.
  • (Loosely Coupling) Sınıflar arasındaki bağlılık azalmış oldu. Sınıflar kolayca replace edilebilirler. Haliyle test yazmada kolaylaşmış oldu. Eğer mobileDeveloperları Latte yerine Americano ile test etmek istersek fun provideCoffee() = Latte() olarak değiştirmemiz yeterli olacaktır. — Bunu yapabilmek için high-level abstractionlara sahip olmamız gerekir. —
  • Dependencylerin oluşturulmasını library üzerine yüklediğimiz için codebasemizde boilerplate kod azalmış oldu.

Dezavantajlar

  • Daha fazla zaman maliyeti
  • Birazcık öğrenme zorluğu

🔚 💚 👏 Son Sözler

Dependency Injection sihirli bir değnek değildir. Dependency Injection dependencyleri ihtiyacı olduğu sınıflarda oluşturmak yerine başka bir yerde oluşturma ve ilgili sınıfa verme fikridir. SOLID, Temiz Kodlama İlkeleri, Modüler Mimari vb. konularını önemsediğimizde çok önemli, kullanışlı ve uygulanabilir olacaktır. Uygulamamızda belli bir kısmı DI için ayırmak iyi olacaktır. Bunu yapabilmek için birçok kütüphane, framework bize yardımcı olmak için hazırlanmış durumda. Tabii ki, bunu herhangi bir 3rd part kütüphane veya framework olmadan da yapabiliriz. Ancak kütüphaneler, frameworkler boilerplate kodu baya azaltır ve dependencylerin yönetimini kolaylaştırır.

Öyleyse, kütüphaneler, frameworkler aracılığıyla DI uygulayarak development yaşamımıza devam edelim👨‍💻.

Okuduğunuz için teşekkürler. Umarım faydalı olmuştur. Sonraki makalelerde görüşmek üzere sağlıklı günler dilerim👋. Keyifli Kodlamalar 👩‍💻.

REFERANSLAR

Dependency Injection — Developers Android

--

--

Cengiz Toru (🇵🇸 #FreePalestineFromGenocide)
Huawei Developers - Türkiye

(🇵🇸 #FreePalestineFromGenocide 🍉) | Muslim, Computer Engineer & Android Developer @ Hepsiburada (NASDAQ: HEPS ) , ex; Huawei, T-Soft, Arneca