Çerçeveyi Tutun – Bağımlılık Enjeksiyon Modellerini Keşfedin
Yayınlanan: 2022-03-11Kontrolün tersine çevrilmesi (IoC) ile ilgili geleneksel görüşler, iki farklı yaklaşım arasında sert bir çizgi çiziyor gibi görünüyor: hizmet bulucu ve bağımlılık ekleme (DI) modelleri.
Neredeyse bildiğim her proje bir DI çerçevesi içeriyor. Müşteriler ve onların bağımlılıkları (genellikle yapıcı enjeksiyon yoluyla) arasında minimum veya hiç standart kod olmadan gevşek bağlantıyı teşvik ettikleri için insanlar onlara çekilir. Bu, hızlı geliştirme için harika olsa da, bazı insanlar kodun izini sürmeyi ve hata ayıklamayı zorlaştırabileceğini düşünüyor. "Perde arkasındaki sihir" genellikle bir dizi yeni sorunu beraberinde getirebilecek yansıma yoluyla elde edilir.
Bu makalede, Java 8+ ve Kotlin kod tabanları için çok uygun olan alternatif bir desen keşfedeceğiz. Bir DI çerçevesinin faydalarının çoğunu korurken, harici araçlar gerektirmeden bir hizmet bulucu kadar basit.
Motivasyon
- Dış bağımlılıklardan kaçının
- Yansımadan kaçının
- Yapıcı enjeksiyonunu teşvik edin
- Çalışma zamanı davranışını en aza indirin
Bir örnek
Aşağıdaki örnekte, içerik almak için farklı kaynakların kullanılabileceği bir TV uygulamasını modelleyeceğiz. Çeşitli kaynaklardan (örneğin karasal, kablo, uydu vb.) sinyalleri alabilen bir cihaz yapmamız gerekiyor. Aşağıdaki sınıf hiyerarşisini oluşturacağız:
Şimdi, Spring gibi bir çerçevenin bizim için her şeyi kabloladığı geleneksel bir DI uygulamasıyla başlayalım:
public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println("Turning on the TV"); this.source.tuneChannel(42); } } public interface TvSource { void tuneChannel(int channel); } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf("Adjusting dish frequency to channel %d\n", channel); } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf("Changing digital signal to channel %d\n", channel); } }
Bazı şeyleri fark ederiz:
- TV sınıfı, bir TvSource'a bağımlılığı ifade eder. Harici bir çerçeve bunu görecek ve somut bir uygulamanın bir örneğini (Karasal veya Kablo) enjekte edecektir.
- Yapıcı enjeksiyon modeli, alternatif uygulamalarla TV örneklerini kolayca oluşturabileceğiniz için kolay test sağlar.
İyi bir başlangıç yaptık, ancak bunun için bir DI çerçevesi getirmenin biraz abartı olabileceğinin farkındayız. Bazı geliştiriciler, inşaat problemlerinde hata ayıklama sorunları bildirmiştir (uzun yığın izleri, izlenemeyen bağımlılıklar). Müşterimiz ayrıca üretim sürelerinin beklenenden biraz daha uzun olduğunu ve profil oluşturucumuzun yansıtıcı çağrılarda yavaşlamalar gösterdiğini ifade etti.
Bir alternatif, Servis Bulucu desenini uygulamak olabilir. Basittir, yansıma kullanmaz ve küçük kod tabanımız için yeterli olabilir. Diğer bir alternatif ise sınıfları kendi haline bırakıp etraflarına bağımlılık konum kodunu yazmaktır.
Birçok alternatifi değerlendirdikten sonra, bunu bir sağlayıcı arayüzleri hiyerarşisi olarak uygulamayı seçiyoruz. Her bağımlılığın, yalnızca bir sınıfın bağımlılıklarını bulma ve enjekte edilmiş bir örnek oluşturma sorumluluğuna sahip olacak ilişkili bir sağlayıcısı olacaktır. Sağlayıcıyı ayrıca kullanım kolaylığı için bir iç arayüz yapacağız. Her sağlayıcı bağımlılıklarını bulmak için diğer sağlayıcılarla karıştırıldığından buna Mixin Injection diyeceğiz.
Bu yapıya neden karar verdiğimin ayrıntıları, Ayrıntılar ve Gerekçe bölümünde ayrıntılı olarak verilmiştir, ancak işte kısa versiyon:
- Bağımlılık konumu davranışını ayırır.
- Arabirimleri genişletmek elmas sorununa düşmez.
- Arayüzlerin varsayılan uygulamaları vardır.
- Eksik bağımlılıklar derlemeyi engeller (bonus puanlar!).
Aşağıdaki diyagram, bağımlılıkların ve sağlayıcıların nasıl etkileşime girdiğini gösterir ve uygulama aşağıda gösterilmektedir. Ayrıca bağımlılıklarımızı nasıl oluşturabileceğimizi ve bir TV nesnesi oluşturabileceğimizi göstermek için bir ana yöntem ekliyoruz. Bu örneğin daha uzun bir versiyonu da bu GitHub'da bulunabilir.
public interface TvSource { void tuneChannel(int channel); interface Provider { TvSource tvSource(); } } public class TV { private final TvSource source; public TV(TvSource source) { this.source = source; } public void turnOn() { System.out.println("Turning on the TV"); this.source.tuneChannel(42); } interface Provider extends TvSource.Provider { default TV tv() { return new TV(tvSource()); } } } public class Terrestrial implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf("Adjusting dish frequency to channel %d\n", channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Terrestrial(); } } } public class Cable implements TvSource { @Override public void tuneChannel(int channel) { System.out.printf("Changing digital signal to channel %d\n", channel); } interface Provider extends TvSource.Provider { @Override default TvSource tvSource() { return new Cable(); } } } // Here compose the code above to instantiate a TV with a Cable TvSource public class Main { public static void main(String[] args) { new MainContext().tv().turnOn(); } static class MainContext implements TV.Provider, Cable.Provider { } }
Bu örnekle ilgili birkaç not:
- TV sınıfı bir TvSource'a bağlıdır, ancak herhangi bir uygulama bilmez.
- TV.Provider, TvSource.Provider'ı genişletir çünkü bir TvSource oluşturmak için tvSource() yöntemine ihtiyaç duyar ve orada uygulanmamış olsa bile onu kullanabilir.
- Karasal ve Kablo kaynakları TV tarafından birbirinin yerine kullanılabilir.
- Terrestrial.Provider ve Cable.Provider arabirimleri, somut TvSource uygulamaları sağlar.
- Ana yöntem, bir TV örneği almak için kullanılan TV.Provider'ın MainContext'in somut bir uygulamasına sahiptir.
- Program, bir TV'yi başlatmak için derleme zamanında bir TvSource.Provider uygulaması gerektirir, bu nedenle örnek olarak Cable.Provider'ı ekledik.
Ayrıntılar ve Gerekçe
Modeli çalışırken ve arkasındaki mantığın bir kısmını gördük. Şimdiye kadar kullanmanız gerektiğine ikna olmamış olabilirsiniz ve haklısınız; tam olarak gümüş bir kurşun değil. Şahsen, çoğu yönden servis bulucu modelinden daha üstün olduğuna inanıyorum. Bununla birlikte, DI çerçeveleriyle karşılaştırıldığında, avantajların ortak kod ekleme ek yükünden daha ağır basıp basmadığını değerlendirmek gerekir.
Sağlayıcılar, Bağımlılıklarını Bulmak için Diğer Sağlayıcıları Genişletiyor
Bir sağlayıcı diğerini genişlettiğinde, bağımlılıklar birbirine bağlanır. Bu, geçersiz bağlamların oluşturulmasını önleyen statik doğrulama için temel temeli sağlar.
Hizmet bulma modelinin ana sorunlarından biri, bağımlılığınızı bir şekilde çözecek genel bir GetService<T>()
yöntemini çağırmanız gerekmesidir. Derleme zamanında, bağımlılığın konum belirleyiciye kaydedileceğine dair hiçbir garantiniz yoktur ve programınız çalışma zamanında başarısız olabilir.
DI modeli de bunu ele almıyor. Bağımlılık çözümlemesi genellikle, çoğunlukla kullanıcıdan gizlenen ve bağımlılıklar karşılanmazsa çalışma zamanında başarısız olan harici bir araç tarafından yansıtılarak yapılır. IntelliJ'in CDI'si (yalnızca ücretli sürümde mevcuttur) gibi araçlar bir miktar statik doğrulama sağlar, ancak yalnızca ek açıklama önişlemcisine sahip Dagger bu sorunu tasarım yoluyla çözebilir.
Sınıflar, DI Modelinin Tipik Yapıcı Enjeksiyonu'nu Sürdürür
Bu gerekli değildir, ancak geliştirici topluluğu tarafından kesinlikle istenir. Bir yandan, yapıcıya bakabilir ve sınıfın bağımlılıklarını hemen görebilirsiniz. Öte yandan, test edilen konuyu bağımlılıklarının alaylarıyla yapılandırarak, birçok kişinin bağlı olduğu birim testi türünü sağlar.

Bu, diğer kalıpların desteklenmediği anlamına gelmez. Aslında, Mixin Injection'ın test için karmaşık bağımlılık grafikleri oluşturmayı basitleştirdiği bile görülebilir, çünkü yalnızca konunuzun sağlayıcısını genişleten bir bağlam sınıfı uygulamanız gerekir. Yukarıdaki MainContext
, tüm arayüzlerin varsayılan uygulamalara sahip olduğu mükemmel bir örnektir, bu nedenle boş bir uygulamaya sahip olabilir. Bir bağımlılığı değiştirmek, yalnızca sağlayıcı yöntemini geçersiz kılmayı gerektirir.
TV sınıfı için aşağıdaki teste bakalım. Bir TV örneğini başlatması gerekiyor, ancak sınıf yapıcısını çağırmak yerine TV.Provider arabirimini kullanıyor. TvSource.Provider'ın varsayılan uygulaması yoktur, bu yüzden onu kendimiz yazmamız gerekiyor.
public class TVTest { @Test public void testWithProvider() { TvSource source = Mockito.mock(TvSource.class); TV.Provider provider = () -> source; // lambdas FTW provider.tv().turnOn(); Mockito.verify(source, times(1)).tuneChannel(42); } }
Şimdi TV sınıfına bir bağımlılık daha ekleyelim. CathodeRayTube bağımlılığı, bir görüntünün TV ekranında görünmesini sağlamak için sihri çalıştırır. Gelecekte LCD veya LED'e geçmek isteyebileceğimiz için TV uygulamasından ayrılmıştır.
public class TV { public TV(TvSource source, CathodeRayTube cathodeRayTube) { ... } public interface Provider extends TvSource.Provider, CathodeRayTube.Provider { default TV tv() { return new TV(tvSource(), cathodeRayTube()); } } } public class CathodeRayTube { public void beam() { System.out.println("Beaming electrons to produce the TV image"); } public interface Provider { default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }
Bunu yaparsanız, az önce yazdığımız testin hala derlendiğini ve beklendiği gibi geçtiğini fark edeceksiniz. TV'ye yeni bir bağımlılık ekledik, ancak varsayılan bir uygulama da sağladık. Bu, yalnızca gerçek uygulamayı kullanmak istiyorsak onunla alay etmemiz gerekmediği anlamına gelir ve testlerimiz, istediğimiz herhangi bir düzeyde sahte ayrıntı düzeyine sahip karmaşık nesneler oluşturabilir.
Bu, karmaşık bir sınıf hiyerarşisinde (örneğin, yalnızca veritabanı erişim katmanı) belirli bir şeyle alay etmek istediğinizde kullanışlıdır. Model, bazen tek başına yapılan testlere tercih edilen türden sosyal testlerin kolayca kurulmasını sağlar.
Tercihiniz ne olursa olsun, her durumda ihtiyaçlarınıza daha iyi uyan herhangi bir test biçimine başvurabileceğinizden emin olabilirsiniz.
Dış Bağımlılıklardan Kaçının
Gördüğünüz gibi, harici bileşenlere referans veya söz yok. Bu, boyut ve hatta güvenlik kısıtlamaları olan birçok proje için anahtardır. Çerçevelerin belirli bir DI çerçevesine bağlı kalması gerekmediğinden birlikte çalışabilirliğe de yardımcı olur. Java'da, uyumluluk sorunlarını azaltan Java Standardı için JSR-330 Dependency Injection gibi çabalar olmuştur.
Yansımadan Kaçının
Servis bulucu uygulamaları genellikle yansımaya dayanmaz, ancak DI uygulamaları yapar (Hançer 2'nin dikkate değer istisnası dışında). Bu, uygulamanın başlatılmasını yavaşlatmanın ana dezavantajlarına sahiptir, çünkü çerçevenin modüllerinizi taraması, bağımlılık grafiğini çözmesi, nesnelerinizi yansıtıcı bir şekilde oluşturması vb.
Mixin Injection, hizmet bulucu desenindeki kayıt adımına benzer şekilde, hizmetlerinizi somutlaştırmak için kod yazmanızı gerektirir. Bu küçük ekstra çalışma, yansıtıcı çağrıları tamamen ortadan kaldırarak kodunuzu daha hızlı ve anlaşılır hale getirir.
Son zamanlarda dikkatimi çeken ve yansımadan kaçınmanın faydasını gören iki proje Graal's Substrate VM ve Kotlin/Native. Her ikisi de yerel bayt koduyla derlenir ve bu, derleyicinin yapacağınız herhangi bir yansıtıcı çağrıyı önceden bilmesini gerektirir. Graal durumunda, yazılması zor, statik olarak kontrol edilemeyen, favori araçlarınız kullanılarak kolayca yeniden düzenlenemeyen bir JSON dosyasında belirtilir. İlk etapta yansımayı önlemek için Mixin Injection'ı kullanmak, yerel derlemenin avantajlarından yararlanmanın harika bir yoludur.
Çalışma Zamanı Davranışını En Aza İndirin
Gerekli arayüzleri uygulayarak ve genişleterek, bağımlılık grafiğini her seferinde tek parça oluşturursunuz. Her sağlayıcı, programınıza düzen ve mantık getiren somut uygulamanın yanında yer alır. Daha önce Mixin modelini veya Cake modelini kullandıysanız, bu tür katmanlama tanıdık gelecektir.
Bu noktada MainContext sınıfından bahsetmek faydalı olabilir. Bağımlılık grafiğinin köküdür ve büyük resmi bilir. Bu sınıf, tüm sağlayıcı arayüzlerini içerir ve statik kontrolleri etkinleştirmenin anahtarıdır. Örneğe geri dönersek ve Cable.Provider'ı uygulama listesinden çıkarırsak, şunu açıkça göreceğiz:
static class MainContext implements TV.Provider { } // ^^^ // MainContext is not abstract and does not override abstract method tvSource() in TvSource.Provider
Burada olan şu ki, uygulama kullanılacak somut TvSource'u belirtmedi ve derleyici hatayı yakaladı. Servis bulucu ve yansıma tabanlı DI ile, tüm birim testleri geçse bile, program çalışma zamanında çökene kadar bu hata fark edilmeyebilirdi! Bunların ve gösterdiğimiz diğer faydaların, kalıbın çalışması için gerekli olan ortak levhayı yazmanın dezavantajından daha ağır bastığına inanıyorum.
Dairesel Bağımlılıkları Yakala
CathodeRayTube örneğine geri dönelim ve döngüsel bir bağımlılık ekleyelim. Diyelim ki bir TV örneğine enjekte edilmesini istiyoruz, bu yüzden TV'yi genişletiyoruz. Sağlayıcı:
public class CathodeRayTube { public interface Provider extends TV.Provider { // ^^^ // cyclic inheritance involving CathodeRayTube.Provider default CathodeRayTube cathodeRayTube() { return new CathodeRayTube(); } } }
Derleyici döngüsel kalıtıma izin vermiyor ve bu tür bir ilişkiyi tanımlayamıyoruz. Çoğu çerçeve, bu olduğunda çalışma zamanında başarısız olur ve geliştiriciler, yalnızca programı çalıştırmak için bu sorunu çözme eğilimindedir. Bu anti-desen gerçek dünyada bulunabilse de, genellikle kötü tasarımın bir işaretidir. Kod derlenemediğinde, değiştirmek için çok geç olmadan daha iyi çözümler aramaya teşvik edilmelidir.
Nesne İnşasında Sadeliği Koruyun
DI yerine SL lehine olan argümanlardan biri, hata ayıklamanın basit ve daha kolay olmasıdır. Örneklerden, bir bağımlılığı başlatmanın yalnızca bir sağlayıcı yöntemi çağrıları zinciri olacağı açıktır. Bir bağımlılığın kaynağının izini sürmek, yöntem çağrısına girmek ve nereye vardığınızı görmek kadar basittir. Hata ayıklama, her iki alternatiften daha basittir çünkü doğrudan sağlayıcıdan bağımlılıkların tam olarak oluşturulduğu yere gidebilirsiniz.
Hizmet Ömrü
Dikkatli bir okuyucu, bu uygulamanın hizmet ömrü sorununu çözmediğini fark etmiş olabilir. Sağlayıcı yöntemlerine yapılan tüm çağrılar, yeni nesneleri başlatacak ve bunu Spring'in Prototip kapsamına benzer hale getirecektir.
Bu ve diğer hususlar, bu makalenin kapsamı dışındadır, çünkü sadece desenin özünü, dikkat dağıtıcı ayrıntılar olmadan sunmak istedim. Bununla birlikte, bir üründe tam kullanım ve uygulama, ömür boyu destekle birlikte tam çözümü hesaba katmalıdır.
Çözüm
Bağımlılık ekleme çerçevelerine veya kendi hizmet bulucularınızı yazmaya alışkın olun, bu alternatifi keşfetmek isteyebilirsiniz. Az önce gördüğümüz mixin modelini kullanmayı düşünün ve kodunuzu daha güvenli ve akıl yürütmeyi daha kolay hale getirip getiremeyeceğinize bakın.