21 Kasım 2023 Salı

Dependency Injection in Delphi TÜRKÇE - Nick Hodges

Bağımlılık Enjeksiyonu Nedir?

Giriş

Üç çocuğum var. Artık yaşlandılar ama küçükken onlarla yemek pişirmek eğlenceliydi. Daha sonra yemekten keyif alacakları bir pasta, kurabiye veya başka şekerlemeler pişirirdik. Ancak bir sorun vardı; pişirirken büyük bir karışıklık yarattık. Her yerde ve her tarafımızda un, şeker ve her türlü şey vardı. Pişirme süreci bize büyük bir temizlik işi bıraktı. Pek çok işe yaradı. Elbette eğlendik ama karmaşa yine de oradaydı.

Karışıklıktan nasıl kaçınılır? Sanırım daha dikkatli olabiliriz ama çocuklar çocuk olacaktır. Çoğu zaman doğum günü pastası pişirirdik. Elbette, pasta pişirmenin tüm zorluğuna ve karmaşasına bir alternatif, mağazaya gidip önceden hazırlanmış bir pasta satın almak olacaktır. Daha da iyi bir çözüm, sipariş üzerine doğum günü pastası teslim edecek ve doğum günü pastanız için tam olarak istediğiniz şeyi kapınıza bırakacak bir fırın bulmak olacaktır.

Şimdi burada dikkate alınması gereken başka bir şey var. Diyelim ki teslimatçı geldi ve çamurda yürüyordu, çizmeleri kirliydi ve şiddetli bir öksürüğü vardı. Onu evine mi alacaksın? Hayır tabii değil. Muhtemelen kapı aralığından ona bir kez bakar ve ona pasta kutusunu verandaya bırakmasını ve çimenlikten defolup gitmesini söylersiniz. Sanırım pasta konusunda da son derece dikkatli olursunuz, ancak tartışma adına pastanın iyi sarıldığını ve teslimatçının yaydığı balgamdan korunduğunu varsayacağız. Başka bir deyişle, teslimatçıyla etkileşiminizin mümkün olduğunca az olmasını istiyorsunuz ama yine de pastayı istiyorsunuz. Çocuğunuz doğum günü pastasının mumlarını üflemezse oldukça üzülecektir. Pastayı karışıklık olmadan ve teslimatçıyla en ince etkileşimlerle istiyorsunuz.

Hatta doğum günü pastasına bağımlılığınız olduğu ve fırının pastayı size teslim ederek bu bağımlılığı enjekte ettiği bile söylenebilir. Hmm.

Veya diyelim ki süpermarkettesiniz ve yiyecek dolu bir alışveriş sepetiniz var. Onları kasa görevlisine götürürsün, o da hepsini arar. “Bu 123,45 dolar olacak” diyor. Ee ne yapıyorsun? Ona cüzdanınızı verip nakit ya da kredi kartı bulmasını mı istiyorsunuz? Tabii ki hayır - cüzdanınızla etkileşimi mümkün olduğunca minimumda tutmak istiyorsunuz - onun etrafta dolaşmasını istemiyorsunuz. Ne olacağını kim bilir. Bunun yerine adama nakit verirsiniz ya da kredi kartınızı çıkarıp ona verirsiniz. Ya da daha iyisi, kartı kendiniz kaydırırsınız, böylece kasa görevlisi kartınıza asla dokunmaz. Yine, bu etkileşimin mümkün olduğunca az olmasını istiyorsunuz. Görevliyle aranızdaki iletişimi minimumda tutmak istediğinizi söyleyebilirsiniz.

Bir tane daha. Bir ev almayı düşündüğünüzü söyleyin. Şehrin her yerini ararsınız ve sonunda gerçekten beğendiğiniz bir yer bulursunuz. Konumu iyi, okul bölgesi iyi, mahalle iyi. Tek bir sorun var; tüm elektrikli cihazlar evin içine kabloyla bağlı. Tüm lambalar, ekmek kızartma makinesi, saç kurutma makinesi, her şey kablolu. Evde priz yok. Her şey doğrudan elektrik sistemine bağlanmıştır, dolayısıyla hiçbir şeyi kolayca değiştiremezsiniz. Elektrikli cihazlar ile elektrik sistemi arasındaki normal arayüzler (elektrik fişleri) mevcut değildir. Ekmek kızartma makinenizi elektrikçi çağırmadan değiştiremezsiniz. Arayüz eksikliğinin işleri çok zorlaştırdığı söylenebilir ve bu nedenle evi satın almamaya karar veriyorsunuz.

Burada bir model (şablon) görüyor musun?

Tamam, bu kadar hikaye yeterli. Sadece söyleyeceğim. Kodunuzda nesneler arasındaki etkileşimin mümkün olduğunca ince ve temiz olması gerekir. Tam olarak ihtiyacınız olanı istemelisiniz, daha fazlasını değil. Bir şeye ihtiyacınız varsa, onu kendiniz yaratmaya çalışmak yerine, onu istemelisiniz, size “teslim edilmesini” sağlamalısınız. Bu şekilde tıpkı pasta, kasiyer ve evde olduğu gibi her şey düzenli, temiz ve esnek kalır. Düzgün, temiz ve esnek bir kodun kulağa oldukça hoş geldiğini düşünüyorum.

Bağımlılık Enjeksiyonunun özü budur. Gerçekten bundan başka bir şey değil. Bağımlılık Enjeksiyonu, on sentlik bir konsept için yirmi beş dolarlık bir terimdir: Bir şeye ihtiyacınız varsa, onu isteyin. Bir şeyleri kendiniz yaratmayın; bunu başkasına ittirin. Herhangi bir maddenin her sınıfı muhtemelen diğer sınıfların yardımına ihtiyaç duyacaktır. Sınıfınızın yardıma ihtiyacı varsa isteyin; "kendisi yapsın" diye uğraşmayın. Unutmayın, her "kendi başına yapsın" diye çalıştığınızda, sabit, katı şekilde kodlanmış bir bağımlılık yaratırsınız. Ve yine küresel (global) değişkenler gibi bunlardan kaçınılmalıdır.

Göreceğimiz gibi aslında bu kadar basit.

Dikkat edilmesi gereken bir nokta -ki bu kitabın ilk bölümünde üzerinde duracağım bir noktadır- henüz "Konteyner" kelimesini kullanmamış olmamızdır. Aslında işte benim bir tweetim:

Bu biraz tuhaf gelebilir ama önemli bir noktaya işaret ediyor: "Bağımlılık Enjeksiyonu Yapmak" ve "Bağımlılık Enjeksiyonu konteyneri kullanmak" gerçekten iki farklı şeydir. Bunlar birbiriyle ilişkilidir (ikincisi birincinin ölçeklendirilmesini kolaylaştırır), ancak aynı şey değildirler. Aslında DI Konteyneri hakkında tekrar konuşmadan önce çok fazla alana değineceğim.

Peki Bağımlılık Enjeksiyonu Tam Olarak Nedir?

Şu ana kadar muhtemelen DI'nin tam olarak ne olduğunu merak ediyorsunuz. Harika “.Net'te Dependency Injection” kitabının yazarı Mark Seemann'a göre Dependency Injection, “gevşek bağlı kod geliştirmemize olanak tanıyan bir dizi yazılım tasarım ilkesi ve modelidir.” Bu iyi bir tanım ama biraz antiseptik. Biraz daha derinlemesine inceleyelim.

Eğer bir bağımlılık enjekte edecekseniz, bağımlılığın ne olduğunu bilmeniz gerekir. Bağımlılık, belirli bir sınıfın işini yapmak için ihtiyaç duyduğu herhangi bir şeydir (ör. alanlar, diğer sınıflar vb.). TClassA'yı derlemek için TClassB'nin mevcut olması gerekiyorsa, o zaman TClassA, TClassB'ye bağımlıdır. Veya başka bir deyişle TClassB, TClassA'nın bağımlılığıdır. Bağımlılıklar, siz bir tane oluşturduğunuzda yaratılır. İşte bir örnek:

type 
  TClassB = class 
  end; 

  TClassA = class 
  private 
    FClassB: TClassB; 
  public 
    constructor Create
  end; 

constructor TClassA.Create
begin 
  FClassB := TClassB.Create; 
end; 

Yukarıdaki kodda, TClassA'da TClassB'ye sabit kodlanmış bir bağımlılık oluşturduk. Tıpkı daha önce tartıştığımız saç kurutma makinesi veya ekmek kızartma makinesi gibi; sınıfa kablolarla bağlı. TClassA tamamen TClassB'ye bağımlıdır. TClassB, TClassA'ya "bağlanmıştır". Sıkıca birleştirilmiş. Gerçekten alabildiğiniz kadar sıkı bir şekilde birleşmişsiniz. Ve sıkı bağlaşım kötüdür.

Bağlaşım (coupling) Hakkında Birkaç Kelime

Yukarıda gördüğümüz gibi bağlaşım, bir şeyin diğerine bağımlı olması kavramıdır. Sıkı bağlaşım, her şeyin gerçekten birbirine bağlı olduğu ve sıkıca bağlanmış oldukları zamandır. B sınıfının tam tanımı olmadan A sınıfını derleyemezsiniz. B kalıcı olarak A'ya yapışmıştır. Sıkı bağlaşım kötüdür; esnek olmayan kod oluşturur. Başka birine kelepçelenseydiniz hareket etmenin ne kadar zor olacağını bir düşünün. Sabit kodlanmış bağımlılıklar oluşturduğunuzda bir sınıf böyle hisseder.

İstediğiniz şey, sınıflarınız ve modülleriniz arasındaki bağlaşımı mümkün olduğunca "gevşek" tutmaktır. Yani, bağımlılıklarınızın mümkün olduğu kadar zayıf olmasını istiyorsunuz. Müşteri adına ihtiyaç duyan bir sınıfınız varsa yalnızca müşteri adını girin. Müşterinin tamamını aktarmayın. Ve göreceğimiz gibi, sınıfınızın başka bir sınıfa ihtiyacı varsa, o sınıfın bir soyutlamasını (genellikle bir arayüzü) aktarın. Arayüz bir duman bulutu gibidir; orada bir şey vardır ama onu gerçekten kavrayamazsınız.

Bu kavram o kadar önemlidir ki onu bir sonraki bölümde daha ayrıntılı olarak tartışacağız.

Bu sabit kodlanmış, sıkı bir şekilde birleştirilmiş bağımlılıkları nasıl yaratırsınız? Bunları diğer sınıfların içinde bir şeyler yaratarak yaratırsınız. Yukarıdaki örneğe bakın. TClassA'nın yapıcısı, TClassB'nin bir örneğini oluşturur ve onu dahili olarak saklar. Bu Create çağrısı bağımlılığı yaratır. TClassA'yı derlemek için TClassB'ye ihtiyacınız var.

Ne yapmalı?

Yapılacak ilk şey TClassB'yi TClassA'nın içinde yaratmamak. Bunun yerine, bağımlılığı yapılandırıcı (constructor) aracılığıyla "enjekte etmek":

type 
  TClassB = class 
  end; 
  TClassA = class 
  private 
    FClassB: TClassB; 
  public 
    constructor Create(aClassB: TClassB); 
  end; 
  
constructor TClassA.Create(aClassB: TClassB); 
begin 
  FClassB := aClassB; 
end; 

Bunu yaparak bağlaşımı biraz gevşetmiş olursunuz. Öncelikle artık istediğiniz TClassB örneğini aktarabileceğinizi unutmayın. Eğer mantıklıysa, soyundan bile geçebilirsiniz. Hala TClassB'ye bağlısınız ve TClassA hala TClassB olmadan derlenemiyor, ancak bağlaşımı bir dokunuşla gevşeterek biraz esneklik eklediniz. TClassA ve TClassB hala bağlaşıktır ancak daha gevşek bir şekilde bağlıdırlar.

Yani bu noktada iki tür bağlaşımımız var: birinci sınıfın aslında ikinci sınıfın bir örneğini oluşturduğu yer ve birinci sınıfın ikinci sınıfa ihtiyaç duyduğu, yani enjekte edildiği yer. İkincisi, daha az (daha gevşek) bağlaşım içermesi nedeniyle birinciye tercih edilir. Bir sonraki bölümde eşleşme hiyerarşisine, yani "uyum-connassence" adı verilen bir kavrama bakacağız.

Ve bu, arkadaşlar, DI'nin özüdür. Bağımlılıklarınızı oluşturmak yerine enjekte edin. Bu kadar. Bu Bağımlılık Enjeksiyonu. Tam burada durabilirim ve eğer size öğrettiğim tek şey bu olsaydı, kodunuz bugün olduğundan daha iyi durumda olurdu (çünkü kabul edin, kod tabanınız sabit kodlanmış bağımlılıklarla dolu, değil mi?). Kitabı burada bitirebilirim ve alet çantanızda işleri gerçekten geliştirecek yeni bir araç olur.

Ama elbette, aslında bu kadar basit değil. Bundan daha fazlası var ve projeler büyüdükçe işler karmaşıklaşıyor, ancak ilk örnek olarak bu, DI'nın ne olduğunu ve nasıl çalıştığını açıklama konusunda uzun bir yol kat ediyor. Önümüzdeki bölümlerde bunu daha derinlemesine inceleyeceğiz ancak bu basit tekniğin gücünü anladıysanız ve gördüyseniz, daha iyi, daha temiz, bakımı daha kolay ve daha esnek kod yazma yolundasınız demektir.

Uyulması Gereken Temel İlkeler

Bu kitaba hakim olacak ve Bağımlılık Enjeksiyonu konusuyla ilgili olacak birkaç temel prensip vardır. Bunlar aşağıdaki gibidir:

Uygulamalara(implementations) Değil, Soyutlamalara(abstractions) Karşı Kod

"Dörtlü Çete"den ("Tasarım Desenleri" kitabının yazarları) Erich Gamma'nın bu ifadeyi icat ettiği düşünülmektedir ve bu güçlü ve önemli bir fikirdir. Yeni geliştiricilere yalnız tek bir şey öğretecek olsanız, o da bu aforizma olmalıdır. Soyutlamalar - genellikle arayüzlerdir (interfaces), ancak her zaman değil (aşağıya bakın) - esnektir. Arayüzler (veya soyut sınıflar) birçok yolla uygulanabilir. Arayüzler, uygulama tamamlanmadan önce bile kodlanabilir. Bir uygulamaya kod yazarsanız sıkı sıkıya bağlı ve esnek olmayan bir sistem yaratırsınız. Kendinizi tek bir uygulamaya (implementation'a) kilitlemeyin. Bunun yerine soyutlamalar kullanın ve kodunuzun esnek, yeniden kullanılabilir ve esnek olmasına izin verin.

Asla Yaratılmaması Gereken Şeyleri Yaratmayın

Sınıflarınız, bir sınıfın yalnızca tek bir şey yapması gerektiği fikri olan Tek Sorumluluk İlkesine uymalıdır. Eğer bunu yaparlarsa, o zaman bir şeyler yaratmamaları gerekir çünkü bunu yaptıklarında iki şey yapmış olurlar. Bunun yerine, ihtiyaç duydukları işlevselliği istemeli ve başka bir şeyin bu işlevselliği yaratmasına ve sağlamasına izin vermelidirler.

Yaratılabilirler ve Enjekte Edilebilirler 

Peki ne yaratılmalı? Aslında ilgilenmemiz gereken iki farklı nesne türü var: "Yaratılabilirler" ve "Enjekte Edilebilirler."

Yaratılabilirler, devam edilmesi ve yaratması gereken sınıflardır. Bunlar yaygın ve iyi bilinen RTL veya yardımcı program sınıflarıdır. Delphi geliştiricileri için bunlar TStringList ve TList<T> gibi şeylerdir. Genel olarak Delphi çalışma zamanındaki sınıflar Yaratılabilirler olarak değerlendirilmelidir. Bunun gibi sınıflar enjekte edilmemeli, sınıflarınız tarafından oluşturulmalıdır. Çoğunlukla kısa ömürleri vardır ve sıklıkla tek bir yöntemin süresinden daha uzun yaşamazlar. Eğer sınıfın tamamı için gerekliyse yapılandırıcıda oluşturulabilirler. Bir Yaratılabilir'in yapılandırıcısına yalnızca diğer Yaratılabilirler aktarılmalıdır.

Enjekte edilebilirler ise asla doğrudan oluşturmak istemediğimiz sınıflardır. Bunlar asla bir bağımlılığı sabit kodlamak istemediğimiz ve her zaman Bağımlılık Enjeksiyonu yoluyla aktarılması gereken sınıf türleridir. Normalde bir yapılandırıcı içerisine bağımlılıklar olarak istenecektir. Yukarıdaki kurala uygun olarak, enjekte edilebilir öğelere, bir örneğe doğrudan referanslar değil, arayüzler aracılığıyla referans verilmelidir. Enjekte edilebilirler çoğunlukla iş mantığınızın (business logic) bir parçası olarak yazdığınız sınıflar olacaktır. Bunların daima bir soyutlamanın, genellikle bir arayüzün arkasına gizlenmeleri gerekir. Enjekte edilebilirlerin yapıcılarında başka enjekte edilebilirler isteyebileceğini de unutmayın.

Yapıcıları Basit Tutun

Yapıcılar basit tutulmalıdır. Bir sınıfın yapılandırıcısı herhangi bir "iş" yapmamalıdır; yani sıfır olup olmadığını kontrol etmek, Yaratılabilir Öğeler oluşturmak ve bağımlılıkları daha sonra kullanmak üzere depolamak dışında hiçbir şey yapmamalıdır. Herhangi bir kodlama mantığı içermemelidirler. Bir sınıfın yapıcısında sıfır(nil) olup olmadığını kontrol etmeyen bir 'if' cümlesi, o sınıfın iki sınıfa bölünmesi için bir çığlıktır. (if ifadesini içermeyen, sıfır(nil) değerli parametreleri kontrol etmenin yolları vardır. Daha sonraki bir bölümde "Asla sıfırı kabul etme" kavramını ele alacağız). Karmaşık bir yapılandırıcı, sınıfınızın çok fazla şey yaptığını gösteren açık bir işarettir. Yapıcıları kısa, basit ve her türlü mantıktan (logic) uzak tutun.

Uygulama(implementation) Hakkında Hiçbir Şey Varsaymayın

Arayüzler elbette bir uygulama olmadan işe yaramaz. Ancak bir geliştirici olarak siz, bu uygulamanın ne olduğu konusunda hiçbir zaman varsayımda bulunmamalısınız. Yalnızca arayüzün yaptığı sözleşmeye göre kodlama yapmalısınız. Uygulamayı yazmış olabilirsiniz, ancak bu uygulamayı göz önünde bulundurarak arayüze karşı kodlama yapmamalısınız. Başka bir deyişle, sanki arayüzün tamamen yeni ve daha iyi bir uygulaması çok yakındaymış gibi arayüzünüze göre kodlayın. İyi tasarlanmış bir arayüz size ne yapmanız gerektiğini ve nasıl kullanmanız gerektiğini anlatacaktır. Bu arayüzün uygulanması, arayüzü kullanımınız açısından önemsiz olmalıdır.


Bir Arayüzün Soyutlama Olduğunu Düşünmeyin

Arayüzler güzel ve kesinlikle her zaman onları övüyorum. Ancak her arayüzün bir soyutlama olmadığının farkına varmak önemlidir. Örneğin, arayüzünüz sınıfınızın genel kısmının tam bir temsiliyse, gerçekten hiçbir şeyi "soyutlamıyorsunuz", değil mi? (Bu tür arayüzlere C++ başlık dosyalarına benzedikleri için “başlık arayüzleri” adı verilir). Sınıflardan çıkarılan arayüzler yalnızca o sınıfa kolayca sıkı bir şekilde bağlanabilir, bu da arayüzü bir soyutlama olarak işe yaramaz hale getirir. Son olarak, soyutlamalar "sızdıran" olabilir, yani uygulamalarına ilişkin belirli uygulama ayrıntılarını ortaya çıkarabilirler. Sızdıran soyutlamalar da normalde belirli bir uygulamaya bağlıdır. (Bu kavram hakkında daha fazla bilgiyi Mark Seemann'ın [http://bit.ly/2awOhmn]http://bit.ly/2awOhmn] adresindeki mükemmel blog yazısında okuyabilirsiniz.)

Sonuç

Tamam, bu Bağımlılık Enjeksiyonu fikrine temel bir giriş görevi görmelidir. Bağımlılık Enjeksiyonu belirli bir amaca yönelik bir araçtır ve bu amaç gevşek bir şekilde bağlanmış koddur.

Açıkçası bundan daha fazlası var, dolayısıyla bu kitabın geri kalanı. Ancak soyutlamalara karşı kodlama yapmanız ve ihtiyacınız olan işlevselliği istemeniz gerektiği kavramlarını anlarsanız, Dependency Injection'ı anlama ve daha iyi kod yazma yolunda iyi bir yoldasınız demektir.

Bağımlılık Enjeksiyonunun Faydaları

Bütün bunları neden yapmalıyız? Kodumuzu Dependency Injection ilkelerinin gerektirdiği şekilde düzenlemek için neden bu kadar zahmete girelim ki? Çünkü faydaları var. Bağımlılık Enjeksiyonunun faydalarından biraz bahsedelim çünkü bunlar çok sayıda ve ilgi çekicidir.

Sürdürülebilirlik – Bağımlılık Enjeksiyonunun muhtemelen temel faydası sürdürülebilirliktir. Sınıflarınız gevşek bir şekilde bağlıysa ve tek sorumluluk ilkesini (DI kullanmanın doğal sonucu) izliyorsa kodunuzun bakımı daha kolay olacaktır. Basit, bağımsız sınıfların düzeltilmesi, karmaşık, sıkı bir şekilde birleştirilmiş sınıflardan daha kolaydır. Bakımı yapılabilen kodun toplam sahip olma maliyeti daha düşüktür. Bakım maliyetleri genellikle ilk etapta kodu oluşturma maliyetini aşar; dolayısıyla kodunuzun sürdürülebilirliğini artıran her şey iyi bir şeydir. Hepimiz zamandan ve paradan tasarruf etmek istiyoruz, değil mi?

Test Edilebilirlik – Sürdürülebilirlik ile aynı doğrultuda test edilebilirlik de vardır. Test edilmesi kolay olan kod daha sık test edilir. Daha fazla test, daha yüksek kalite anlamına gelir. Yalnızca tek bir şey yapan (yine DI kullanmanın doğal sonucu olan) gevşek bağlı sınıfların birim testini yapmak çok kolaydır. Dependency Injection'ı kullanarak test çiftleri (genellikle "sahte" olarak adlandırılır) oluşturmayı çok daha kolay hale getirirsiniz. Bağımlılıklar sınıflara aktarılırsa, test çift uygulamasını geçmek oldukça basittir. Bağımlılıklar sabit kodlanmışsa bu bağımlılıklar için test çiftleri oluşturmak imkansızdır. Gerçekte test edilen test edilebilir kod, kalite kodudur. Veya en azından test edilmemiş koddan daha kalitelidir. Birim testlerinin zaman kaybı olduğu iddiasını kabul etmekte zorlanıyorum; onlar benim için her zaman zaman ayırmaya değer. (Elbette bunun tartışmalı olmasını garip bulan tek kişi ben değilim?)

Okunabilirlik – DI kullanan kod daha basittir. Tek Sorumluluk İlkesini takip eder ve böylece daha küçük, daha kompakt ve noktasal sınıflarla sonuçlanır. Yapıcılar o kadar karmaşık ve mantıkla dolu değiller. Sınıflar daha net bir şekilde tanımlanmış olup neye ihtiyaçları olduğu açıkça bildirilmektedir. Tüm bunlardan dolayı DI tabanlı kod daha okunabilirdir. Ve daha okunabilir olan kod daha kolay korunur-sürdürülür.

Esneklik – Gevşek bağlı kod – yine DI kullanmanın sonucu – daha esnektir ve farklı şekillerde kullanılabilir. Tek bir şey yapan küçük sınıflar daha kolay bir şekilde yeniden bir araya getirilebilir ve farklı durumlarda yeniden kullanılabilir. Küçük sınıflar Legolar(tm) gibidir; daha hacimli ve daha az esnek olan Duplo(tm) bloklarının aksine, çok sayıda şey oluşturmak için kolaylıkla bir araya getirilebilirler. Kodu yeniden kullanabilmek zamandan ve paradan tasarruf sağlar. Tüm yazılımların değişebilmesi ve yeni gereksinimlere uyum sağlayabilmesi gerekiyor. Bağımlılık Enjeksiyonu kullanan gevşek bağlı kod esnektir ve bu değişikliklere uyum sağlayabilir.

Genişletilebilirlik – Bağımlılık Enjeksiyonunu kullanan kod, daha genişletilebilir bir sınıf yapısıyla sonuçlanır. Kod, uygulamalar (implementation) yerine soyutlamalara güvenerek belirli bir uygulamayı kolayca değiştirebilir. Soyutlamalara karşı kod yazdığınızda, yaptığınız işin çok daha iyi bir uygulamasının çok yakında olduğu fikriyle kodlayabilirsiniz. Küçük, esnek sınıflar miras veya bileşim yoluyla kolayca genişletilebilir. Bir uygulamanın kod tabanı hiçbir zaman statik kalmaz ve kod tabanınız büyüdükçe ve yeni gereksinimler ortaya çıktıkça büyük olasılıkla yeni özellikler eklemeniz gerekecektir. Genişletilebilir kod bu zorluğun üstesinden gelebilir.

Ekip Geliştirme – Bir ekipteyseniz ve bu ekibin bir proje üzerinde birlikte çalışması gerekiyorsa (bu ne zaman doğru değildir?), Dependency Injection ekip gelişimini kolaylaştıracaktır. (Yalnız çalışıyor olsanız bile, ileride işinizin birilerine devredilmesi ihtimali çok yüksektir). Dependency Injection sizi uygulamalara(impementation'lara) değil soyutlamalara göre kodlamaya çağırır. Birlikte çalışan ve her biri diğerinin çalışmasına ihtiyaç duyan iki ekibiniz varsa, uygulamaları yapmadan önce soyutlamaları tanımlayabilirsiniz ve ardından her takım, uygulamalar yazılmadan önce bile soyutlamaları kullanarak kendi kodunu yazabilir. Ayrıca kod gevşek bir şekilde bağlı olduğundan bu uygulamalar birbirine bağlı olmayacak ve bu nedenle ekipler arasında kolaylıkla bölünebilir.

İşte işte buradasın. Bağımlılık Enjeksiyonu, ekip üyeleri arasında kolayca dağıtılabilen, bakımı yapılabilir (sürdürülebilir), test edilebilir, okunabilir, esnek ve genişletilebilir kodla sonuçlanır. Herhangi bir geliştiricinin tüm bunları istemeyeceğini hayal etmek zor görünüyor.

Bağlaşıma Daha Yakından Bir Bakış: Uyum (connassence)

Giriş

Dikkat ettiyseniz bağlılığı sevmediğimi fark etmişsinizdir. Sıkıca bağlanmış kod beni rahatsız ediyor. Blog yazılarımda, kitaplarımda, her yerde bundan bahsediyorum. Sıkı bağlantı kötüdür. Gevşek bağlantı istediğimizi ve sıkı bağlantıdan kaçınmamız gerektiğini biliyoruz. Bu bir bakıma verilmiş bir şey ama tüm bunların tam olarak ne anlama geldiğine dair çok az değerli tartışma oldu. Bunu tarif etmenin oldukça zor olduğu biliniyor. “Gördüğümde anlarım” türünden bir şeydi bu. Bağımlılık Enjeksiyonu tamamen bağlamayı azaltmakla ilgili olduğundan, bağlamanın ne olduğu hakkında biraz daha fazla bilgi içeren bu bölümü eklemenin iyi bir fikir olacağını düşündüm.

Bağlılık, iki modül arasındaki ilişkilerin ve bağlantıların bir ölçüsüdür. Kodun bir şekilde bağlanması gerekiyor, yoksa hiçbir şey yapamaz. Peki bu ölçümler nasıl yapılıyor? Tam olarak ne ölçülüyor? Bunlar zor sorular. Sıkı bağlantının kötü olduğu göz önüne alındığında, bunu mümkün olduğunca sınırlamak istiyoruz. Peki bunu tam olarak nasıl yapacağız? Bağlantı tam olarak ne halt ediyor?

Uyum (Connascence)

Neyse ki bağlılığı ölçmenin bir yolu var. Buna "uyum" (connascence) denir. ("Cuh-NAY-since" diye telaffuz edildiğini duydum) Yazılım geliştirme alanında, sistemin genel doğruluğunu korumak için iki modülden birini değiştirmek diğerinde de değişiklik gerektiriyorsa iki modülün "connascent" (uyumlu) olduğu söylenir. . Birleşmeyi düşündüğümüzde aklımıza gelen hemen hemen budur. Terim ilk kez Meilir Page-Jones tarafından eşleşmenin tam olarak ne olduğunu ölçebilmek ve nitelendirebilmek amacıyla kullanıldı. Bunu ilk olarak “Nesneye Yönelik Tasarım Hakkında Her Programcının Bilmesi Gerekenler” adlı kitabında tartıştı. Kitap 1995 yılında yayınlandı, dolayısıyla bu fikir yeni bir şey değil.

Ancak çoğu zaman olduğu gibi yirmi yıllık bu fikir ancak şimdilerde gündeme geliyor. Connascence, kodunuzdaki bağımlılığı ölçmenin bir yoludur.

Connascence iki boyutta ele alınmaktadır. Birincisi, dokuz farklı uyum düzeyi vardır. İkincisi, bu seviyelerin hepsinin belirli nitelikleri vardır. Önce bu niteliklerden bahsedeceğim, sonra da uyum düzeyleri hakkında konuşmaya geçeceğiz. Uyum düzeyleri, bir eşleşme taksonomisi oluşturmamıza ve bunun hakkında konuşmak için bize bir kelime dağarcığı vermemize olanak tanır. Bu gerçekten faydalıdır, çünkü daha önceki eşleşme tartışmaları genellikle "sıkı mı gevşek mi" konusunda çok şekilsiz bir tartışmaya dönüşüyordu; bu da olaylara pek bilimsel bir bakış açısı getirmiyordu.

Connascence'nin Nitelikleri

Connascence'ın üç niteliği vardır: Güç, Derece ve Yerellik.

Connascence'nin Gücü

Eğer onu düzeltmek daha derinlemesine ve daha zor değişiklikler gerektiriyorsa, bir Connascence (uyumluluk-uzlaşma) düzeyinin diğerinden daha güçlü olduğu söylenir. Örneğin, iki varlık arasındaki bağlaşımın azaltılması kolay ve basit bir değişiklik gerektiriyorsa, bu durumda connascence'nin, karmaşık bir değişiklik gerektiren connascence'den daha az güçlü olduğu söylenir. Bir düzeyde connascence'nin yeniden düzenlenmesi (refactoring) zorsa, bunun güçlü düzeyde bir uyum olduğu söylenir. Aşağıda açıklanan Uyum Düzeyleri artan güce göre listelenmiştir. Bu sıralama, yeniden düzenleme (refactoring) işleminize nasıl öncelik vereceğiniz konusunda size bir fikir verir. Yani, en güçlü bağlantıları daha zayıf bağlantı seviyelerine kadar yeniden düzenlemeye (refactor etmeye) çalışmalısınız.

Connascence Derecesi

Connascence derecesi, uyumluluğun meydana geldiği seviyenin bir ölçüsüdür. Connassence küçük derecede veya büyük derecede meydana gelebilir. Örneğin, iki modül tek bir referans yerine birden fazla referansla bağlanabilir. Çoklu referansların bağlantısının yüksek derecede yakınlığa sahip olduğu söylenir. Belirli bir yöntem, onu birçok dış sınıfa bağlayan birçok parametreye sahip olabilir. Böyle bir metodun-yöntemin yüksek derecede connascence'si (uyumu) vardır.

Connascence'nin Bölgesi 

Bazen bağlaşım(coupling) birbirine yakın gerçekleşir; aynı ünitede (Unit) birbirine bağlı iki sınıfınız vardır. Connascence tek bir metotla ortaya çıkabilir. Ancak bazen bu bağlaşım birbirinden çok uzakta olan iki birimde (Unit'te) meydana gelir. Bunu hepimiz gördük; uygulamanızın “sol alt” kısmında bir değişiklik yapıyorsunuz ve bu, programın “sağ üst” kısmında çok uzakta bir etki yaratıyor. Birbirine yakın olan connascence, uzak olandan daha iyidir-hayırlıdır.

Connascence'nin Düzeyleri

Bağlaşımın bazısı gereklidir. Bir uygulamada o olmadan hiçbir şey olamaz. Bununla birlikte, bağlaşımın gücünü mümkün olduğu kadar zayıf, derecesini mümkün olduğu kadar küçük ve lokalitesini mümkün olduğu kadar yakın tutmak istiyoruz. Eğer bağlaşımı ölçebilseydik, bunu yaptığımızı bilebilirdik, değil mi? Page-Jones, her biri bir öncekinden daha güçlü, daha yüksek derecede ve/veya daha uzak bir bölgede olan dokuz connascence düzeyini tanımladı. Bu bağlantı seviyelerini fark ettiğimizde, bunları daha düşük bir seviyeye indirecek şeyler yapabiliriz. Bir göz atalım ve her şeyin nasıl çalıştığını görelim.

Statik-Durağan Connascence'ler

Connascense'ın ilk beş seviyesi statiktir denilir, çünkü kodunuzu görsel olarak inceleyerek bulunabilirler.

İsim Connascence'si

İsim connascense'i, iki şeyin bir şeyin adı üzerinde anlaşmaya varması gerektiğinde ortaya çıkar. Bu, connascence'nin en zayıf biçimidir ve kendimizi sınırlamaya çalışmamız gereken biçimdir. Bu neredeyse apaçıktır ve kaçınılamaz. Bir prosedür bildirirseniz:

procedure TMyClass.DoSomething;

onu DoSomething adını kullanarak çağırmanız gerekir. Bir şeyin adını değiştirmek başka yerde değişiklik yapılmasını gerektirir. Prosedürün adını değiştirmek istiyorsanız, onu çağırdığınız her yerde değiştirmeniz gerekir. Bu açık görünüyor ve elbette bu connascence düzeyi kaçınılmazdır. Aslında bu arzu edilen bir şeydir. Bu, sahip olabileceğimiz en düşük düzeydeki bağlaşmadır ve bu yüzden onu aramalı ve en çok kullanmalıyız. Eğer birleşmemizi İsmin Uzlaşması ile sınırlandırabilirsek, çok iyi durumda oluruz.

Type (tip) Connascence'si

Tip connascence'si, iki varlığın bir şeyin türü üzerinde anlaşmaya varması gerektiğinde ortaya çıkar. En bariz örnek bir metodun parametreleridir. Bir işlevi(fonksiyonu) aşağıdaki gibi bildirirseniz:

function TSomeClass.ProcessWidget(aWidget: TWidget; aAction: TWidgetActionType): Boolean; 

bu durumda herhangi bir çağıran kod, ProcessWidget işlevinin parametreleri olarak bir TWidget ve bir TWidgetActionType iletmeli ve sonuç türü olarak bir Boolean kabul etmelidir. Delphi güçlü bir şekilde typed olarak yazılmıştır, dolayısıyla bu tür bir connascence neredeyse her zaman derleyici tarafından yakalanır.(MÖ'nün notu : typed'ı tanımlamak için untyped'ı tanımlamak gerekir. Pointer kullanıyorsanız programınız untyped'dır ve çalışma zamanında hata vermesi çok muhtemeldir. Örneğin eski TList sınıfına bir Type'ı değil yalnızca bir pointeri eklersiniz. Tamsayı, reel sayı, çeşitli bilgiler içeren bir record ve bir sınıf olabilir bunlar ve siz bunu ancak runtime'de kontrol edebilirsiniz. Oysa yeni generic'ler-soysallar kullanılarak bu tür hatalardan da kaçınılabilir duruma geldi Delphi.). Type connascence'si, İsim Connscence'si kadar zayıf değildir, ancak yine de zayıf ve kabul edilebilir bir connscence düzeyi olarak kabul edilir. Gerçekten de, o olmadan gerçekten idare edemezsiniz, değil mi? Herhangi bir şeyi yapmak için bir sınıfın yöntemini çağırabilmeniz gerekir ve türlerinizin eşleştiğinden emin olmak çok da külfetli değildir. Aslında Delphi'de kodunuzu derlemek için bile Connascence of Type aracılığıyla kodu bağlaştırmanız gerekir.

Anlam Connascence'si

Anlam Connascence'si, bileşenlerin belirli değerlerin anlamı üzerinde anlaşmaya varması gerektiğinde ortaya çıkar. Anlam Connascence'siçoğunlukla “sihirli sayıları”, yani anlamı olan ve birden fazla yerde kullanılan belirli bir değeri kullandığımızda ortaya çıkar. Aşağıdaki kodu ele alalım, inceleyelim:

function GetWidgetType(aWidget: TWidget): integer; 
begin 
  if aWidget.Status = 'Working' then 
  begin 
    Result := 1; 
  end 
  else 
  begin 
    if aWidget.Status = 'Broken' then 
    begin 
      Result := 2; 
    end 
    else 
    begin 
      if aWidget.State = 'Missing' then 
      begin 
        Result := 3; 
      end 
      else 
      begin 
        Result := 0; 
      end; 
    end; 
  end; 
end; 

Yukarıdaki kodu kullanmak istiyorsanız GetWidgetType işlevine ait sonuç kodunun anlamını bilmeniz gerekir. Sonuç türlerinden birini değiştirirseniz veya yenisini eklerseniz, bu işlevi kullanan kodu, kullanıldığı her yerde değiştirmeniz gerekir. Bu değişikliği yapmak için her sonuç kodunun anlamını bilmeniz gerekir. Buradaki bariz çözüm, sonuç kodu için sabit adlar veya daha iyisi, sonuç kodlarını tanımlayan numaralandırılmış bir tür kullanacak şekilde kodu yeniden düzenlemektir. Bu, sizin connascence'nizi, arzu edilen bir sonuç olarak, Anlam Connascence'sinden İsim Connascence'sine doğru azaltır. Unutmayın, daha yüksek bir düzeyden daha düşük bir connascence düzeyine doğru yeniden düzenleme (refactoring) yaptığınızda, bağlaşmayı azaltmış ve dolayısıyla kodunuzu geliştirmiş olursunuz. Anlam Uzlaşmasının bir başka örneği de sıfırın bir sinyal olarak kullanılmasıdır. Geliştiriciler genellikle nil kelimesini "değer yok" veya "Üzgünüm, bunu yapamadım/bulamadım/tamamlayamadım" anlamında kullanırlar ve kodunuzun bunu halletmesi gerekir. Daha sonraki bölümlerde göreceğimiz gibi, Anlam Connascence'si yoluyla bağlaşma yarattığı için nil'in bu kullanımından kaçınılmalıdır.

Konum Connascence'si 

Konum Connascence'si, iki farklı yerdeki kodun nesnelerin konumu üzerinde anlaşmaya varması gerektiğinde ortaya çıkar. Bu en yaygın olarak, bir metodun parametre listesindeki parametrelerin sırasının bu sırayı korumak için gerekli olduğu parametre listelerinde meydana gelir. Mevcut bir parametre listesinin ortasına bir parametre eklerseniz, bu yöntemin tüm kullanımlarında yeni parametrenin doğru konuma eklenmesi gerekir.

Bazı diller, parametrelerinizi herhangi bir sıraya dahil edilebilecek şekilde adlandırmanıza izin verir, ancak Delphi buna izin vermez. Bu nedenle Delphi kodu yazarken Konum Connascence'si kullanarak kodu bağlaştırmak gerekir.

Artık, herhangi bir rutindeki parametre sayısını sınırlayarak Konum Connascence derecesi azaltılabilir. Konum Connascence'sini sınırlamak için parametre listesini tek bir türe indirgeyebilir, böylece Konum Connascence'sinden Tür Connascence'sine geçiş yapmış olursunuz. Tip Connascence'si daha zayıf bir bağlaşımdır ve bu, yapmaya çalışmanız gereken bir şeydir.

İşte bir örnek. Bu rutini ele alalım:

procedure TUserManager.AddUser(aFirstName: string; 
       aLastName: string; aAge: integer; aBirthdate: TDateTime;           aAddress: TAddress; aPrivileges: TPrivileges); 

AddUser'ı kullanmak için tüm parametreleri tam olarak doğru konuma aldığınızdan emin olmalısınız. Ortaya bir parametre eklerseniz AddUser kullanımının bu yeni değeri tam olarak doğru yere koyduğundan emin olmanız gerekir.

Bu Konum Connascence örneğini, bunun yerine Tip Connascence'si kullanacak şekilde yeniden düzenleyerek (refactoring) azaltabiliriz. Örneğin:

type 
  TUserRecord = record 
    FirstName: string; 
    LastName: string; 
    Age: integer; 
    Birthday: TDateTime; 
    Address: TAddress; 
    Privileges: TPrivileges; 
  end; 

procedure TUserManager.AddUser(aUser: TUserRecord);

Artık bir tür oluşturarak ve AddUser prosedürünün uzun bir parametre listesinin konumu yerine parametrenin türüne bağlı olmasını sağlayarak bağlaşımı azalttık. Bağlaşımın gücünü azaltarak kodu geliştirdik. Bu tekniğe aynı zamanda “Parametre Nesnesi” de denir. (Bkz.Parametre Nesnesi)

Algoritmanın Connascence'si

Algoritma Connascence'si, iki modülün birlikte çalışabilmesi için belirli bir algoritma üzerinde anlaşması gerektiğinde ortaya çıkar.

Delphi istemcisi tarafından kullanılacak C# tabanlı API'ye sahip bir sisteminiz olduğunu hayal edin. Bu iki modül arasında gönderilen bilgiler hassastır ve şifrelenmesi gerekir. Böylece, bu iki modül Algoritma Connascence'si ile birleştirilir çünkü her ikisinin de kullanılacak şifreleme algoritması üzerinde anlaşması gerekir. Gönderici şifreleme algoritmasını değiştirirse, alıcının da aynı algoritmaya geçmesi gerekir.

Algoritmanın Connascence'sini (Uzlaşısını) azaltmak zordur, çünkü çoğu zaman yüksek derecede bir yerelliğe sahiptir (yani, bağlaşım çok uzakta gerçekleşir). Bir çözüm, algoritmanın bulunduğu tek yer haline gelen tek bir modül oluşturmak ve ardından her iki tüketen modülün de bu tek modülü kullanmasını sağlamak olabilir.

Dinamik Connascence'ler

Sonraki dört Connascence düzeyinin "dinamik" olduğu söylenir çünkü bunlar yalnızca kodunuzu çalıştırarak keşfedilebilir. Bu seviyeler statik olanlardan daha güçlüdür çünkü kendilerini yalnızca çalışma zamanında ortaya çıkarırlar, bu da onları tespit etmeyi ve çoğu zaman düzeltmeyi zorlaştırır.

Yürütme(execution) Connascence'si

Yürütme Uyumu, sistemin doğru olması için kodun yürütme sırasının gerekli olduğu durumlarda ortaya çıkar. Genellikle "Geçici Bağlaşım" olarak anılır. 

Yukarıdaki kodu kullanan bir örnek:

UserRecord.FirstName := 'Alicia'; 
UserRecord.LastName := 'Florrick'; 
UserRecord.Age = 47; 
UserManager.AddUser(UserRecord); 
UserRecord.Birthday := EncodeDate(1968, 12, 3);

Bu kod, kullanıcı eklendikten sonra Doğum Günü değerini ekler. Bu açıkça işe yaramayacak. Açıkçası, kodu incelediğinizde bu fark edilebilir, ancak fark edilmesi daha zor olan daha karmaşık bir senaryo hayal edebilirsiniz. Bu koda göz atalım:

SprocketProcesser.AddSprocket(SomeSprocket); 
SprocketProcessor.ValidateSprocket; 

Bu iki ifadenin sırası önemli mi? Dişlinin eklenmesi ve ardından doğrulanması mı gerekiyor, yoksa eklenmeden önce doğrulanması mı gerekiyor? Bunu söylemek zor ve sistemde iyi bilgi sahibi olmayan biri bunları yanlış sıraya koyma hatasına düşebilir. Bu, Yürütme Connascense'sidir.

İşte başka bir örnek. Mesajları tutan bir kuyruk hayal edin. İlk mesajda “Listeyi başlat” yazıyor. Daha sonra sonraki iki mesajın içinde listeye öğe eklemek için liste öğeleri bulunur. Ardından son olarak kuyrukta “Listeyi sonlandır” yazan bir mesaj bulunur. Öğeleri kuyruktan çeken tek bir çalışan iş parçacığınız varsa, bu harika çalışır. Tüm eşyalar sırayla çıkarılacaktır. Ancak, öğeleri kuyruktan çeken birden fazla iş parçacığınız varsa ve iş parçacıklarından biri diğerlerinden biraz daha hızlı çalıştıysa ve sonra, son "İşte başka bir öğe" mesajı işlenmeden önce "Listeyi sonlandır" mesajını kuyruktan kaldırdı mı? Bu kötü olurdu. Bu aynı zamanda Connascence of Execution'ın neden olduğu bir hata olacaktır.

Zamanlama Connascence'si

Zamanlama Connascence'si, yürütme zamanlamasının uygulamanın ürettiği çıktıda bir fark yaratması durumunda meydana gelir. Bunun en belirgin örneği, iki iş parçacığının aynı kaynağı takip ettiği ve iş parçacıklarından yalnızca birinin yarışı kazanabildiği iş parçacıklı bir yarış durumudur. Zamanlamanın Connascence'ını bulmak ve teşhis etmek oldukça zordur ve kendisini tahmin edilemeyecek şekillerde ortaya çıkarabilir.

Değer Connascence'si

Değer Connascence'si, çeşitli değerlerin modüller arasında uygun şekilde koordine edilmesi gerektiğinde ortaya çıkar. Örneğin, şuna benzeyen bir birim testiniz olduğunu hayal edin:

[Test] 
procedure TestCheckoutValue; 
var 
  PriceScanner: IPriceScanner; 
begin 
  PriceScanner := TPriceScanner.Create; 
  PriceScanner.Scan('Frosted Sugar Bombs'); 
  Assert.Equals(50, PriceScanner.CurrentBalance); 
end;

Bu yüzden testi yazdık. Şimdi, Test Odaklı Geliştirme ruhuna uygun olarak, testin mümkün olduğunca kolay ve basit bir şekilde geçmesini sağlayacağım.

procedure TPriceScanner.Scan(aItem: string);
begin
  CurrentBalance := 50;
end;

Artık TPriceScanner ile testimiz arasında sıkı bir bağlaşım var. Açıkçası Connascense of Name'e sahibiz çünkü her iki sınıf da CurrentBalance ismine dayanıyor. Ancak bu nispeten düşük bir seviyedir ve tamamen kabul edilebilirdir. Tip Connascence'ımız var, çünkü her ikisinin de TPriceScanner türü üzerinde anlaşması gerekiyor, ancak yine de bu iyi huylu. Anlam Uyumuna sahibiz, çünkü her iki rutinin de 50 sayısına sabit kodlanmış bir bağımlılığı var. Bunun yeniden düzenlenmesi gerekiyor. Ancak asıl sorun, her iki sınıfın da Buzlu Şeker Bombalarının fiyatını, yani “Değerini” bilmesi nedeniyle ortaya çıkan Değer Connascence'sidir. Fiyat değişirse bizim çok basit testimiz bile bozulur.

Çözüm, daha düşük bir Connascence düzeyine yeniden düzenleme (refactoring) yapmaktır. Yapabileceğiniz ilk şey, Buzlu Şeker Bombalarının fiyatı (değer) bilgisinin yalnızca tek bir yerde muhafaza edilmesini sağlayacak şekilde yeniden düzenleme yapmaktır:

procedure TPriceScanner.Scan(aItem: string; aPrice: integer);
begin CurrentBalance := aPrice; end;

ve şimdi testimiz aşağıdaki gibi okunabilir:

[Test]
procedure TestCheckoutValue; var PriceScanner: IPriceScanner; begin PriceScanner := TPriceScanner.Create; PriceScanner.Scan('Frosted Sugar Bombs', 50); Assert.Equals(50, PriceScanner.CurrentBalance); end;

Ve artık iki modül arasında Değer Connascence'miz yok ve testimiz hâlâ başarılı. Harika.

Kimlik Connascence'si

Kimlik Uyumu, iki bileşenin aynı nesneye gönderme yapması gerektiğinde ortaya çıkar. İki modül aynı şeye atıfta bulunuyorsa ve ardından biri bu referansı değiştirirse, diğer nesnenin de aynı referansa değişmesi gerekir. Genellikle incelikli ve tespit edilmesi zor bir connascence şeklidir. Sonuç olarak bu, connascence'nin en karmaşık biçimidir.

Aşağıdaki kodu ele alalım:

program Identity;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; type TReportInfo = class private FReportStuff: string; procedure SetReportStuff(const Value: string); public property ReportStuff: string read FReportStuff write SetReportStuff; end; procedure TReportInfo.SetReportStuff(const Value: string); begin FReportStuff := Value; end;
type TInventoryReport = class
private FReportInfo: TReportInfo; public constructor Create(aReportInfo: TReportInfo); property ReportInfo: TReportInfo read FReportInfo write FReportInfo; end;
TSalesReport = class private FReportInfo: TReportInfo; public constructor Create(aReportInfo: TReportInfo); property ReportInfo: TReportInfo read FReportInfo write FReportInfo; end;
constructor TInventoryReport.Create(aReportInfo: TReportInfo); begin FReportInfo := aReportInfo; end; constructor TSalesReport.Create(aReportInfo: TReportInfo); begin FReportInfo := aReportInfo; end; var ReportInfo: TReportInfo; NewReportInfo: TReportInfo; InventoryReport: TInventoryReport; SalesReport: TSalesReport; begin try ReportInfo := TReportInfo.Create; InventoryReport := TInventoryReport.Create(ReportInfo); SalesReport := TSalesReport.Create(ReportInfo); try // Do Stuff with reports NewReportInfo := TReportInfo.Create; try InventoryReport.ReportInfo := NewReportInfo;        // Do stuff with report        // But the reports now point to different ReportInfos.       // This is Connascence of Identity      finally       NewReportInfo.Free;      end;   finally      ReportInfo.Free;      InventoryReport.Free;      SalesReport.Free;   end;   except
    on E: Exception do     Writeln(E.ClassName, ': ', E.Message);   end; end.

Burada iki raporumuz var: bir envanter raporu ve bir satış raporu. Etki alanı, iki raporun her zaman aynı TReportInfo örneğine başvurmasını gerektirir. Ancak yukarıda da görebileceğiniz gibi raporlama sürecinin ortasında Envanter Raporu yeni bir ReportInfo örneğine kavuşuyor. Bu sorun değil, ancak Satış Raporunun da bu yeni TReportInfo örneğine atıfta bulunması gerekiyor. Başka bir deyişle, bir rapordaki referansı değiştirirseniz diğer raporun da aynı referansla değişmesi gerekir. Buna Kimlik Connascence'si denir, çünkü sistemin doğru çalışmaya devam etmesi için iki sınıfın da referanslarının kimliğini değiştirmesi gerekir.

Connascence Hakkında Ne Yapmalı?

Artık dokuz Connascence Düzeyini tanımladığımıza göre, kodumuzdaki bağlaşım konusunda ne yapmalıyız?

Bir miktar bağlaşımın gerçekleşmesi gerekirken, connascence'nizi mümkün olan en düşük seviyede tutmaya çalışmalısınız. Yani kodunuzdaki Connascence Derecesini azaltmalısınız. Çok temiz bir uygulama genellikle İsim ve Tür Connascence'sine sahip olacak ve Anlam ve Konum Connascence'sini mümkün olduğu kadar sınırlamaya çalışacaktır. Diğer tüm Connascence türleri gerçekten yeniden düzenlenmelidir.

Ayrıca kodunuzdaki Connascence Yerelliğini de artırmalısınız. Kodunuzdaki tüm tanımlayıcıların kapsamını azaltmak için çalışmalısınız. Bir türün kapsamını mümkün olduğu kadar düşük bir kapsam ile sınırlandırmalısınız. Birbirine ait olan şeyler bir arada tutulmalı, ait olmadıkları yerlere  gösterilmemelidir. DRY ilkesi – “Don't Repeat Yourself-Kendini Tekrarlama” – artan yerelliğin bir örneğidir. Tek Sorumluluk İlkesi (SRP-Single Responsibility Principle) de öyle.

Son olarak istikrarı tercih etmelisiniz. Connascence aslında sadece değişim ihtiyacının ölçüsüdür ve bir şeyi ne kadar az değiştirmeniz gerekiyorsa, sıkı bağlaşımın bir sonucu olarak hatalar o kadar az sıklıkla ortaya çıkar. Kararlı olan şeylerin bağlaşım hatalarına neden olma olasılığı çok daha düşük olacaktır

Sonuç

Hepimiz sıkı bağlaşımın kötü olduğu konusunda hemfikiriz (en azından öyle umuyoruz!). Ancak bağlaşım kavramı genellikle oldukça kötü tanımlanmıştır. Umarız bu kritik (bir yerde bir şey değişirse, başka bir yerde başka bir şeyin değişmesi gerektiği fikri) connascence'nin ne olduğu hakkında biraz daha spesifik konuşmanıza olanak tanır. Ayrıca, Connascence Düzeylerinin, yüksek düzeyde bağlaşıma sahip kodu bulmanıza ve bağlaşımınızı azaltmak için bu alanları yeniden düzenlemenize olanak sağlayacağını umuyorum. Kodunuzdaki genel Connascence düzeyini azaltmaya çalışırsanız, daha temiz ve bakımı daha kolay bir kod tabanına sahip olursunuz.

Yapılandırıcı Enjeksiyonu

Tamam, ilk bölümde bir sınıfı diğerine enjekte eden kodu gördünüz. Bu, bağlaşımı azalttı ve bağımlılığı zayıflattı. Sınıf yapılandırıcı aracılığıyla enjekte edildi. Bunun "Yapılandırıcı(Constructor) Enjeksiyonu" olarak adlandırıldığını öğrendiğinizde muhtemelen şaşkına döneceksiniz. Bu bölümde yapılandırıcı enjeksiyonu hakkında biraz daha derinlemesine konuşacağım.

Yapılandırıcı Enjeksiyonu

Yapılandırıcı Enjeksiyonu, bir sınıfın bağımlılıklarını aktarmak için yapılandırıcıyı kullanma işlemidir. Bağımlılıklar yapılandırıcının parametreleri olarak bildirilir. Sonuç olarak, yapılandırıcının gerektirdiği türden bir değişkeni aktarmadan sınıfın yeni bir örneğini yaratamazsınız.

Bu son nokta çok önemlidir; yapılandırıcıda parametre olarak bir bağımlılık bildirdiğinizde, "Üzgünüm millet, ancak bu sınıfı oluşturmak istiyorsanız bu parametreyi iletmelisiniz" diyorsunuz. Böylece bir sınıf, ihtiyaç duyduğu bağımlılıkları belirleyebilir ve bunları alacağı garanti edilebilir. Onlar olmadan sınıfı oluşturamazsınız. Şu kodu incelersek:

TPayrollSystem = class private FBankingService: TBankingService; public constructor Create(aBankingService: TBankingService); end;
constructor TPayrollSystem.Create(aBankingService: TBankingService); begin FBankingService := aBankingService; end;

TBankingService örneğini parametre olarak geçmeden bir TPayrollSystem oluşturamazsınız. (Maalesef sıfır geçebilirsiniz, ancak bunu bir dakika içinde halledeceğiz.) TPayrollSystem, bir TBankingService gerektirdiğini ve sınıf kullanıcılarının bir tane sağlaması gerektiğini çok açık bir şekilde bildiriyor.

Hiçbir zaman Nil kabul etme

Bahsettiğim gibi, yukarıdaki sınıfın parametre olarak nil alabilmesi ve alacak olması talihsiz bir durumdur. "Almak" diyorum çünkü sınıfın bir kullanıcısı sıfır değerini geçebilirken, sınıfın kendisinin sıfır değerini kabul etmesi gerekmez. Aslında, yapılandırıcılar ve normal yöntemler de dahil olmak üzere tüm yöntemlerin herhangi bir zamanda herhangi bir referans parametresi için değer olarak nil'i açıkça reddetmesi gerektiğini savunuyorum. Yöntem bir istisna oluşturmadan hiçbir zaman bir parametrenin sıfır olmasına izin verilmemelidir. Yukarıdaki TPayrollSystem'e nil değerini iletirseniz ve sınıf bunu kullanmaya çalışırsa erişim ihlali meydana gelecektir. Erişim ihlalleri de kötüdür. Bunlardan kaçınılmalıdır ve bu durumda kaçınılabilir.

Yukarıdaki kod aslında şöyle olmalıdır:

  TPayrollSystem = class 
  private 
    FBankingService: TBankingService; 
  public 
    constructor Create(aBankingService: TBankingService); 
  end; 

constructor TPayrollSystem.Create(aBankingService: TBankingService); 
begin 
  if aBankingService = nil then 
  begin 
    raise Exception.Create('What the heck do you think you are doing?How dare you pass me a nil banking service!?!'); 
  end; 
  FBankingService := aBankingService; 
end; 

Bu kod hiçbir zaman dahili alanın nil olmasına izin vermez. Birisi yapıcının parametresi için değer olarak nil değerini geçmeye cesaret ederse bu bir istisna (exception) oluşturacaktır. Bu böyle olmalı. Sıfırı kabul edebilirsiniz, ancak kodunuzun her yerinde bunu kontrol etmeniz gerekir ve bunu kim ister? Barney Fife'ın ölümsüz sözleriyle, giriş noktasında kabul etmeyi reddederek, sıfır kullanımını daha başlangıçta ortadan kaldırmalısınız.

Sıfırın denetlenmesi basmakalıp bir koddur ve Spring4D çerçevesi (framework), sıfır kontrolünün kolayca yapılması için bir araç sağlar. Bir parametre olarak sıfırın iletilmesine karşı korumaya "Koruma Deseni" adı verilir ve Spring4D'nin, belirli durumların oluşmasını kontrol etmenize izin veren bir dizi statik yönteme sahip Guard adlı bir kayıt sağladığını bilmek sizi şaşırtmayacaktır.

Koruma Kalıbı (Guard Pattern) aslında program yürütmesinin devam edebilmesi için True olarak değerlendirilmesi gereken herhangi bir Boolean ifadesi olarak tanımlanır. Genellikle bir yöntemin devam edebilmesi için belirli ön koşulların karşılandığından emin olmak ve takip eden kodun düzgün bir şekilde yürütülebilmesini sağlamak için kullanılır. Bir referansın Nil olmadığının kontrol edilmesi muhtemelen Koruma Kalıbının en yaygın (ancak tek değil) kullanımıdır.

Eldeki durumda, bir parametrenin nil olmasına karşı koruma sağlamak için Guard Pattern'i kullanıyoruz, dolayısıyla kodumuzu basitleştirmek için Guard sınıfını kullanabiliriz:

  TPayrollSystem = class
  private
    FBankingService: TBankingService;
  public
    constructor Create(aBankingService: TBankingService);
  end;
constructor TPayrollSystem.Create(aBankingService: TBankingService);
begin
  Guard.CheckNotNull(aBankingService, 'aBankingService');
  FBankingService := aBankingService;
end;

Guard.CheckNotNull iki parametre alır. Birincisi kontrol edilecek öğe, ikincisi ise string olarak kontrol edilen öğenin adıdır. Şimdi, yapılandırıcıya nil değerini iletirseniz şu hatayı alırsınız:

Tamam, nil parametre geçişi hakkında yeterince durduk. Mesajı şimdiye kadar almış olmalısın.

Yapılandırıcı Enjeksiyonu Ne Zaman Kullanılır?

Sınıfınızın düzgün çalışması için gerektirdiği bir bağımlılığa sahip olduğunda Constructor Injection'ı kullanmalısınız. Sınıfınız bağımlılık olmadan çalışamıyorsa, onu yapılandırıcı aracılığıyla enjekte edin. Sınıfınızın üç bağımlılığa ihtiyacı varsa, yapıcıda üçünü de talep edin. (Anti-Patterns bölümünde, yapıcıda çok sayıda bağımlılığa yol açacağınız durumu ele alacağız.)

Ek olarak, söz konusu bağımlılığın ömrü tek bir yöntemden daha uzun olduğunda Constructor Injection'ı kullanmalısınız. Yapıcıya aktarılan bağımlılıklar, sınıftaki birden fazla yöntemi kapsayan kullanımıyla genel olarak sınıf için faydalı olmalıdır. Bir bağımlılık yalnızca tek bir noktada kullanılıyorsa, Metot Enjeksiyonu (gelecek bölümde ele alınacaktır) kullanılmalıdır. 

Yapılandırıcı Enjeksiyonu, bağımlılık enjeksiyonunu yapmanın ana yolu olmalıdır. Çok basit: Bir sınıfın bir şeye ihtiyacı vardır ve bu nedenle onu daha inşa edilmeden önce ister. Koruma Kalıbını kullanarak, bu bağımlılığı depolayan alan değişkeninin geçerli bir örnek olacağını bilerek sınıfı güvenle kullanabilirsiniz. Üstelik bunu yapmak gerçekten basit ve anlaşılır. Açık ve ayrıştırılmış kod için Constructor Injection başvuracağınız teknik olmalıdır. Ancak araç kutusundaki tek araç bu olmamalıdır. Sırada – bağımlılıkları enjekte etmenin diğer yolları ve nedenleri geliyor.

Property Enjeksiyonu

Tamam, gerekli bağımlılıkları bildirmek istediğimizde Constructor Injection'ı kullanırız. Ancak bağımlılık gerekmediğinde ne yapmalı? Bazen bir sınıfın kesinlikle gerekli olmayan ancak gerçekten sınıf tarafından kullanılan bir bağımlılığı olabilir. Bir örnek, gramer denetleyicisinin yüklü olduğu veya bulunmadığı bir belge sınıfı olabilir. Eğer varsa harika; sınıf bunu kullanabilir. Eğer böyle bir şey yoksa harika; sınıf, yer tutucu olarak varsayılan bir uygulamayı içerebilir.

Buradaki çözüm Property Enjeksiyonudur. Sınıfınıza, söz konusu sınıfın geçerli bir örneğine ayarlanabilecek bir property eklersiniz. Bağımlılık bir property olduğundan istediğiniz gibi ayarlayabilirsiniz. Bağımlılık istenmiyor veya ihtiyaç duyulmuyorsa property'yi olduğu gibi bırakabilirsiniz. Kodunuz bağımlılık varmış gibi davranmalı, bu nedenle kodun gerçek bir bağımlılıkla veya gerçek bir bağımlılık olmadan çalıştırılabilmesi için hiçbir şey yapmama varsayılan uygulaması sağlamalısınız. (Unutmayın, hiçbir şeyin nil olmasını istemiyoruz, dolayısıyla varsayılan uygulama geçerli olmalıdır). Dolayısıyla, sınıfın kullanıcısı çalışan bir uygulama sağlamak isterse bunu yapabilir, ancak istemezse, içeren sınıfın hala çalışmasına izin verecek bir çalışma varsayılanı vardır.

Bir bağımlılık isteğe bağlı olduğunda ve/veya sınıf başlatıldıktan sonra bir bağımlılık değiştirilebildiğinde Property Injection'ı kullanın. İçeren sınıfın kullanıcılarının söz konusu arayüzün kendi uygulamalarını sağlayabilmesini istediğinizde bunu kullanın. Property Injection'ı yalnızca söz konusu arayüzün varsayılan uygulamasını sağlayabildiğiniz zaman kullanmalısınız. Property Enjeksiyonu bazen "Setter Enjeksiyonu" olarak da anılır.

Herhangi bir varsayılan uygulama muhtemelen işlevsel olmayan bir uygulama olacaktır. Ama öyle olmak zorunda değil. Çalışan bir varsayılan uygulama sağlamak istiyorsanız sorun değil. Ancak, Property Injection'ı kullanarak ve bu sınıfı içeren nesnenin yapılandırıcısında oluşturarak kendinizi bu uygulamaya bağladığınızı unutmayın. Ama umutsuzluğa kapılmayın; Container kullanmaya başladığımızda bununla baş etmenin yolları var. Ancak henüz o kadar uzakta değiliz, bu nedenle uyarı.

Bir örnek elbette işlerin nasıl yapıldığını gösterecektir. Yukarıda anlattıklarımı yapan koda bir göz atalım; isteğe bağlı dilbilgisi denetleyicisine sahip bir belge sınıfı.

İlk olarak bir arayüzle başlayacağız:

type
IGrammarChecker = interface   ['{9CA7F68C-8A42-4B8C-AD1A-14C04CAE0901}'] // Ctrl-Alt-G     procedure CheckGrammar; end;

Şimdi bunu iki kez implemente edeceğiz. Bir kez hiçbir şey yapmama varsayılanı olarak ve bir kez daha "gerçek" bir dilbilgisi denetleyicisi olarak.

type
TDefaultGrammarChecker = class(TInterfacedObject, IGrammarChecker) private procedure CheckGrammar; end;
TRealGrammarChecker = class(TInterfacedObject, IGrammarChecker)
  procedure CheckGrammar;
end;
procedure TDefaultGrammarChecker.CheckGrammar;
begin
  // do nothing, but we'll WriteLn just to prove we were here
  WriteLn('Do nothing');
end;
procedure TRealGrammarChecker.CheckGrammar;
begin
  WriteLn('Grammar has been checked');
end;

Her iki implementasyon da bir WriteLn kullanarak birşeyler yazıyor, "işlevsel olmayan" implementasyon bile. Sadece her şeyin düzgün çalıştığından emin olmak istedim. Yine, TDefaultGrammarChecker'ın, bizi her zaman nil olup olmadığını kontrol etmek zorunda kalmaktan kurtaracak, işlevsel olmayan, varsayılan bir uygulama olması amaçlanmıştır.

Şimdi dilbilgisi denetleyicisi için Property'si olan bir sınıfa ihtiyacımız var.

  TDocument = class 
  private 
    FText: string; 
    FGrammarChecker: IGrammarChecker; 
    procedure SetGrammarChecker(const Value: IGrammarChecker);
  public 
    constructor Create(const aText: string); 
    procedure CheckGrammar; 
    property Text: string read FText write FText; 
    property GrammarChecker: IGrammarChecker read FGrammarChecker
                write SetGrammarChecker; 
  end; 

procedure TDocument.CheckGrammar; begin FGrammarChecker.CheckGrammar; end;

constructor TDocument.Create(const aText: string); begin inherited Create; FText := aText; FGrammarChecker := TDefaultGrammarChecker.Create; end;

procedure TDocument.SetGrammarChecker(const Value: IGrammarChecker); begin Guard.CheckNotNull(Value, 'Value in TDocument.SetGrammarChecker'); FGrammarChecker := Value; end;

Bu kodla ilgili dikkat edilmesi gereken bazı noktalar şunlardır:

  • Yapılandırıcısı, belge metnini parametre olarak alır. Daha sonra okunabilir/yazılabilir property olarak gösterilir, böylece isterseniz değiştirebilirsiniz.
  • Yapılandırıcı ayrıca varsayılan dilbilgisi denetleyicisinin bir örneğini de oluşturur. Bunun, Property Injection'ın tehlikelerinden biri olan sabit kodlanmış bir bağımlılık yarattığını bir kez daha unutmayın. Ancak bağımlılık, hiçbir şey yapmama varsayılanıdır ve sürekli olarak sıfır olup olmadığını kontrol etmemizi engeller.
  • GrammarChecker özelliğinin setter'i, FGrammarChecker'ın dahili değerinin hiçbir zaman nil olamayacağını garantileyen bir Guard çağrısı içerir.

Dikkat edebileceğiniz bir başka şey de, eğer isterseniz GrammarChecker özelliğinin aslında "salt yazılır" bir özellik olabileceğidir. Yani, yalnızca değeri değiştirebilir ve hiçbir zaman gerçekten okuyamazsınız.

Yazılmakta olan değer yalnızca dahili olarak kullanıldığında ve hiçbir zaman sınıf dışındaki kod tarafından çağrılmayacağında, salt yazılır bir özellik oluşturabilirsiniz. GrammarChecker gibi bir şey salt yazılabilir bir özellik olarak nitelendirilebilir

Şimdi, her şeyi uygulayan ve Property Injection'ı çalışırken gösteren bazı kodlar:

procedure Main; 
var 
  Document: TDocument; 
begin 
  Document := TDocument.Create('This is the document text.'); 
  try 
    WriteLn(Document.Text); 
    // Varsayılanı kullan, işlevsiz dilbilgisi denetçisi
    Document.CheckGrammar; 
    // Bağımlılığı "gerçek" dilbilgisi denetçisi olarak değiştir.
    Document.GrammarChecker := TRealGrammarChecker.Create; 
    // Artık dilbilgisi denetleyicisi "gerçek" bir denetleyicidir.
    Document.CheckGrammar; 
    // Bu bir istisna yaratacaktır ve yaratmalıdır da.
    Document.GrammarChecker := nil; 
  finally 
    Document.Free; 
  end; 
end;

Yukarıdaki kodla ilgili dikkat edilmesi gerekenler şunlardır: * Bir metni yapılandırıcı parametresi olarak alarak bir belge oluşturur. * CheckGrammar'ı çağırır, ancak varsayılan dil bilgisi denetleyicisi hiçbir şey yapmaz, dolayısıyla konsolda öyle yazar. * Ancak daha sonra "gerçek" bir dilbilgisi denetleyicisi eklemek için Property Injection'ı kullanırız ve CheckGrammar'ı çağırdığımızda dilbilgisi "gerçek" olarak kontrol edilir. * Daha sonra dilbilgisi denetleyicisini nil set etmeye çalışırız, ancak bu, Guard cümlesi nedeniyle bir istisna (exception) oluşturur.

Böylece Property Injection isteğe bağlı bağımlılıklar sağlamanıza olanak tanır. Ayrıca gerekirse bir bağımlılığı değiştirmenize de olanak tanır. Örneğin, belge sınıfınız farklı dillerden metinler alabilir ve bu nedenle, belgenin dili değiştikçe dil bilgisi denetleyicisinin de değişmesini gerektirebilir. Property Injection buna izin verecektir.


Metot Enjeksiyonu

Peki ya sınıfınızın ihtiyaç duyduğu bağımlılık çoğu zaman farklı olacaksa? Peki ya bağımlılık bir arayüzse ve sınıfa aktarmak isteyebileceğiniz birkaç uygulamanız varsa? Property Injection'ı kullanabilirsiniz, ancak bu durumda, sık sık değişen bağımlılığı kullanan yöntemi çağırmadan önce, zamansal bağlaşım olasılığını ayarlayarak property'yi her zaman belirliyor olursunuz.

Zamansal Bağlaşım (Temporal Coupling) temel olarak Yürütme Connascence'si ile aynıdır; işlerin doğru çalışması için yürütme sırasının belirli bir şekilde gerçekleşmesi gerektiği fikri.

Yapılandırıcı (Constructor) ve Property Enjeksiyonu genellikle sık sık değişmeyecek bir bağımlılığınız olduğunda kullanılır, dolayısıyla bağımlılığınız birçok uygulamadan biri olabileceğinde kullanıma uygun değildirler.

Metot Enjeksiyonunun devreye girdiği yer burasıdır.

Metot Enjeksiyonu, kullanım noktasında bir bağımlılık enjekte etmenize olanak tanır, böylece istediğiniz herhangi bir uygulamayı daha sonra kullanmak üzere saklama konusunda endişelenmenize gerek kalmadan parametre olarak geçebilirsiniz. Genellikle özel işlem gerektiren diğer bilgileri ilettiğinizde kullanılır. Örneğin:

unit uPropertyInjectionMultiple;

interface type TRecipe = class private FText: string; public property Text: string read FText write FText; end;

IFoodPreparer = interface
['{3900BE64-B0EC-4281-9D92-96B191FDE5BC}']     procedure PrepareFood(aRecipe: TRecipe); end;
TBaker = class(TInterfacedObject, IFoodPreparer) procedure PrepareFood(aRecipe: TRecipe); end;
TShortOrderCook = class(TInterfacedObject, IFoodPreparer) procedure PrepareFood(aRecipe: TRecipe); end;
TChef = class(TInterfacedObject, IFoodPreparer)     procedure PrepareFood(aRecipe: TRecipe); end;
TRestaurant = class private     FName: string; public     constructor Create(const aName: string):     procedure PrepareFood(aRecipe: TRecipe; aPreparer: IFoodPreparer);     property Name: string read FName; end; implementation

constructor TRestaurant.Create(const aName: string); begin FName := aName; end;
procedure TRestaurant.PrepareFood(aRecipe: TRecipe; aPreparer: IFoodPreparer); begin aPreparer.PrepareFood(aRecipe) end;
procedure TBaker.PrepareFood(aRecipe: TRecipe); begin Writeln('Use baking skills to do the following: ' + aRecipe.Text); end; procedure TShortOrderCook.PrepareFood(aRecipe: TRecipe); begin Writeln('Use the grill to do the following: ' + aRecipe.Text); end;
procedure TChef.PrepareFood(aRecipe: TRecipe); begin Writeln('Use well-trained culinary skills to prepare the following: ' + aRecipe.Text); end;
end.

Burada, tarife bağlı olarak farklı bir hazırlayıcı gerektirebilecek bir tarif kavramıyla karşı karşıyayız. Belirli bir tarif için uygun hazırlayıcı türünün ne olacağını yalnızca çağıran kod bilebilir. Örneğin, bir tarif kısa süreli bir aşçıya ihtiyaç duyabilirken, başka bir tarif bir fırıncıya veya şefe ihtiyaç duyabilir. Kodu yazarken ne tür bir IFoodPreparer'a ihtiyaç duyulacağını bilmiyoruz ve bu nedenle yapılandırıcıdaki bağımlılığı gerçekten parametre olarak geçip o tek uygulamaya takılıp kalamayız.

Ayrıca, her yeni veya farklı bir IFoodPreparer gerekli olduğunda bir property set etmek de acemicedir. Ve property'nin bu şekilde ayarlanması, zamansal eşleşmeye (Yürütme Connascence'si) neden olur ve thread kullanılan bir ortamda kodun etrafında bir kilit gerektireceğinden thread güvenliği sorunlarına neden olacaktır.

En iyi çözüm, IFoodPreparer'ı kullanım noktasında metota parametre olarak geçmektir. 

Bağımlılığın her kullanımda değişebileceği durumlarda veya en azından kullanım noktasında hangi bağımlılığa ihtiyaç duyulacağından emin olamadığınızda Metot enjeksiyonu kullanılmalıdır.

Bağımlılığın her kullanıldığında değişmesi gerektiğinde Yöntem Enjeksiyonu kullanımına bir örnek. Araba boyama robotunun, boyadığı her arabadan sonra yeni bir boya tabancası ucuna ihtiyaç duyduğu bir durumu hayal edin. Yapılandırıcı (Constructor) Enjeksiyonu kullanarak şöyle başlayabilirsiniz:

type
IPaintGunTip = interface     procedure SprayCar(aColor: TColor); end;
TPaintGunTip = class(TInterfacedObject, IPaintGunTip)     procedure SprayCar(aColor: TColor); end;
  TCarPaintingRobot = class private     FPaintGunTip: IPaintGunTip; public     constructor Create(aPaintGunTip: IPaintGunTip);     procedure PaintCar(aColor: TColor); end;
constructor TCarPaintingRobot.Create(aPaintGunTip: IPaintGunTip); begin Guard.CheckNotNull(aPaintGunTip, 'aPaintGunTip');
FPaintGunTip := aPaintGunTip; end;
procedure TCarPaintingRobot.PaintCar(aColor: TColor); begin FPaintGunTip.SprayCar(aColor); // Uh oh -- what to do now? How do we free the tip? // And even if we could, what then? // How would we get a new one? end; procedure TPaintGunTip.SprayCar(aColor: TColor); begin Writeln('Spray the car with ', ColorToString(aColor)); end;

Metot Enjeksiyonu kullanarak bir yöntemi uygularken bir Guard cümleciği eklemelisiniz. Bağımlılık hemen kullanılacaktır ve tabii ki, onu nil'ken kullanmaya çalışırsanız anında Erişim İhlali hatasıyla karşılaşacaksınız. Açıkçası bundan kaçınılmalıdır.

Burada arabayı boyarken yeni bir boya tabancası ucu almamız gerekiyor. Ama nasıl? Arabayı boyadığımızda uç artık işe yaramıyor ama bu bir arayüz ve onu manuel olarak Free etmenin (Interface'ler kendilerini referans gösteren bir değişken kalmadığı zaman otomatik olarak Free'lenir) bir yolu yok ve bunu yapsak bile, bir dahaki sefere arabayı boyamamız gerektiğinde ne yaparız? Belirli bir araba için ne tür bir uç gerektiğini bilmiyoruz ve ilgimizi doğru bir şekilde yönlendirsek de, yeni bir uç oluşturma konusunda hiçbir şey bilmiyoruz. Ne yapmalı? Bunun yerine metod enjeksiyonunu kullanalım:

type
IPaintGunTip = interface     procedure SprayCar(aColor: TColor); end; TPaintGunTip = class(TInterfacedObject, IPaintGunTip) procedure SprayCar(aColor: TColor); end; TCarPaintingRobot = class public     procedure PaintCar(aColor: TColor; aPaintGunTip: IPaintGunTip); end; procedure TCarPaintingRobot.PaintCar(aColor: TColor; aPaintGunTip: IPaintGunTip);
begin aPaintGunTip.SprayCar(aColor); end; procedure TPaintGunTip.SprayCar(aColor: TColor); begin WriteLn('Spray the car with ', ColorToString(aColor)); end;

Artık bağımlılığı doğrudan metoda aktardığımızda, boyama işimiz bittiğinde arayüz kapsam dışına çıkıyor ve boya tabancasının ucu yok oluyor. Buna ek olarak, bir dahaki sefere bir arabanın boyanması gerektiğinde, tüketici yeni bir uç parametre olarak geçilecek ve bu da kullandıktan sonra Free edilecek. Kurtarmak için Metot Enjeksiyonu!

Bu nedenle, Metot Enjeksiyonu iki senaryoda faydalıdır: bir bağımlılığın uygulanmasının (implementation'un) değişeceği durumlarda ve bağımlılığın her kullanımdan sonra yenilenmesi gerektiğinde. Her iki durumda da, hangi uygulamanın (implementation'un) metota pas edileceğine karar vermek metodu çağıran kişiye kalmıştır.

(MÖ'nün notu: Burada uygulama ya da implementation olarak kastedilen bir Interface'den türetilen koda sahip olan sınıflardır. Yani burada TPaintGunTip, IPaintGunTip arayüzünün bir uygulaması ya da implementation'udur.)


Bağımlılık Enjeksiyon Konteyneri

Bu noktaya kadar Bağımlılık Enjeksiyon Kabı-konteyneri kavramına yalnızca kısaca değindim. Bunu birkaç nedenden dolayı bilerek yaptım. İlk olarak Dependency Injection Container'dan bahsetmeden önce Dependency Injection'ı anladığınızdan emin olmak istedim. Başlangıçta belirttiğim gibi, Bağımlılık Enjeksiyonu ve Bağımlılık Enjeksiyon Kabı kullanmak çok farklı iki şeydir. Bağımlılık Enjeksiyonunu, Bağımlılık Enjeksiyon Kabına dokunmadan yapabilirsiniz. Şu ana kadar yaptığımız şey buydu. İkincisi, Container'ın ne olduğu, nasıl kullanılacağı, nerede ve ne zaman kullanılacağı konusunda pek çok kafa karışıklığı var. Container'ın ne olduğu ve ne yaptığı konusunda kafa karışıklığı yaratmadan Bağımlılık Enjeksiyonu fikrini anlayabilmeniz için bu soruları yanıtlamayı mümkün olduğu kadar ertelemek istedim.

Ancak zamanı geldi ve şimdi ünlü veya kötü şöhretli Dependency Injection Container'a bir göz atacağız.

Bağımlılık Enjeksiyon Konteyneri Nedir?

“Bağımlılık Enjeksiyon Konteyneri Nedir?” Sorusunun cevaplanması, fikir ortaya çıktığından beri zor bir süreç oldu. Pek çok kişinin bu soruya nasıl cevap vereceği konusunda farklı görüşleri var. Elbette kendi tanımım var ama buna geçmeden önce konteynerin ne olmadığından bahsedeceğim.

Container yalnızca Create çağrısının yerine geçmez. Yaptığı işin büyük bir kısmı nesneler yaratmak olsa da, o kadar da basit değil. Kesinlikle tüm yapılandırıcı (constructor-create) çağrılarınızı Konteyner'e (veya - ürperti - ServiceLocator'a) yapılan çağrılarla değiştirmemelisiniz ve onu kesinlikle yerel metot değişkenleri oluşturmak için kullanmamalısınız. Örneğin, aşağıdaki kod bir anti-pattern olarak kabul edilmelidir:

procedure TWidgetManager.ProcessWidget(aWidget: TWidget);
var WidgetProcessor: IWidgetProcessor; begin WidgetProcessor := ServiceLocator.GetService; WidgetProcessor.ProcessWidget(aWidget); end;

Burada yalnızca TWidgetProcessor.Create çağrısını verilen konteynere yapılan çağrıyla değiştirdik. Bundan kararlılıkla kaçınılmalıdır. Genellikle, Konteyner'e erişmek için bir ServiceLocator sınıfı kullanma eğilimi nedeniyle buna ServiceLocator kalıbı(pattern'i) (veya inandığım gibi anti-pattern) denir. ServiceLocator anti-patternini daha sonraki bir bölümde tartışacağım. Bir Konteyner yalnızca sınıf uygulamalarının ve hizmetlerinin bir sözlüğü değildir. Eğer olaya bu şekilde bakarsanız, bu büyük bir global değişkenler kümesi haline gelir ve herkes global değişkenlerin istenmeyen bir durum olduğu konusunda hemfikirdir. Böyle bir görünüm, "Oluşturmanın yerine geçme" görünümüne benzer; çünkü onu tutarsanız, Konteyneri "nesnelerin alınabileceği bir yer"den başka bir şey olarak görmezsiniz, oysa aslında bundan daha fazlasıdır.

Konteyner Nedir?

Konteyner, uygulamanızın nesne grafiğini oluşturmanın bir yoludur. Yaşam süreleri ile birlikte bağımlılıkların oluşumunu da yönetir. Bir konteyner, bağımlılıkları gerektiği gibi, hatta ihtiyaç duyulmadan önce oluşturur ve yönetir. Sınıflarınızı bir kutunun içindeki boş balonlar gibi düşünün. Uygulamanız başladığında bir butona basıyorsunuz ve balonların tamamı hava ile doldurularak kullanıma hazır hale geliyor. Göreceğimiz gibi - ve metafora devam edersek - konteynere balonları doğru şekilde doldurması ve her birini şişirmesi için uygun miktarda hava verme talimatını verebilirsiniz. Balonların şişmesini geciktirebilir, hangi tür balonların şişirileceğini seçebilir ve hangi balonların demet halinde bir araya getirileceğini seçebilirsiniz. İsterseniz Konteyner size balonlarınız üzerinde tam kontrol sağlar. Böylece factory patterninin yüceltilmiş-büyütülmüş bir örneğinden daha fazlası haline gelir ve bunun yerine nesneleriniz için güçlü, esnek - evet - bir Konteyner haline gelir.

Neden Konteyner Gereklidir?

Bu soruya cevap vermeden önce Dependency Injection Container'a her zaman ihtiyaç duyulmadığını belirtmek isterim. Bazen eski güzel Constructor, Property ve Method Enjeksiyonu yeterlidir ve Konteyner aşırıya kaçar. Nesne grafiğinizin ne kadar karmaşık olduğuna bağlı olarak, kendi nesnelerinizi manuel olarak oluşturmaktan veya sınıflarınızı oluşturmak için fabrikaları kullanmaktan kurtulabilirsiniz.

Mark Seamann'ın "Saf DI" olarak adlandırdığı şeyi yapmanın utanılacak bir yanı yok. Aslında Saf DI oldukça aydınlatıcı olabilir. Bunu doğru yaparsanız, sonunda uygulamanızın Bileşim Köküne ulaşacaksınız (Bileşim Kökü biraz daha aşağıda ele alınacaktır), Nesne Grafiğinizin nasıl oluşturulduğuna dair çok net bir görüşe sahip olacaksınız. Uygulamanızın hangi nesnelere ihtiyaç duyduğu ve bunların nasıl oluşturulduğu açıkça görülecektir.

Saf DI da "tür"lü bir şekilde yazılmıştır. Bu Oluşturucu Enjeksiyon parametrelerinin hepsinin bir türü  (type) vardır ve eğer yazım doğru değilse derleyici şarlayacaktır. Derleme zamanı geri bildirimi anında gerçekleşir ve böylece çalışma zamanı geri bildirimine bağlı kaldığımız durumlarda ortaya çıkabilecek hataları bug'ları önlememize olanak tanır.

Kesinlikle bir DI Container'a "ihtiyaç duymadığınız" göz önüne alındığında, kendinizi büyük olasılıkla bir tane kullanmak isterken bulacaksınız. Muhtemelen bir Konteyner isteyeceksiniz çünkü herhangi bir sonucu olan bir uygulama, Kompozisyon Kökünde oldukça kapsamlı bir nesne grafiğine sahip olacaktır. Tüm bu nesneleri oluşturmak hantallaşabilir. Bir Konteyner sizi her şeyi manuel olarak oluşturma zorunluluğundan kurtarabilir.

Belki de şu soru zaten aklınıza gelmiştir: "Eğer bağımlılıklarımı istemeye devam edersem ve onları asla yaratmazsam, bunlar nerede yaratılacak?" Bu çok güzel bir soru. Cevap şu anda muhtemelen tahmin ettiğiniz şeydir: bunların hepsi Konteynerde yaratılmıştır. Konteyner bunların yaratımını ve ömrünü yönetir. Tek bir çağrıyla (bunun hakkında aşağıda konuşacağız) tüm uygulamanız için ihtiyacınız olan her şeyi yapabilir ve yönetebilir.

Konteynerin Nerede Kullanılacağı

Belki de bu da aklınıza takılan başka bir sorudur: "Nesnelerimin oluşturulmasını Bağımlılık Enjeksiyonu aracılığıyla 'geri itmeye' devam edersem, tüm oluşturma çağrılarım DPR dosyasında sonlanmayacak mı? Bu çok fazla Create çağrısı demek!" Bunu merak ediyorsunuz çünkü olacak olan tam olarak bu. Bağımlılık Enjeksiyonunu özel bir şekilde kullanırsanız - ki kullanmalısınız - Constructor'ların tümü uygulamanızın "Kompozisyon Kökü" olarak adlandırılan yere ulaşacaktır. Bir Delphi uygulamasında bu, DPR dosyasının ana bloğudur; her Delphi uygulamasının başladığı yerdir. Ve Konteyneri kullanmanız gereken yer burasıdır. Uygulamanız için tüm nesne grafiğini Çözümlemek için Container'a tek bir çağrı yapmanız gereken yer burasıdır.

Sınıflarınızın (Nesne Kompozisyonunuzun) birbirine bağlanmasını mümkün olduğu kadar geciktirmelisiniz. Ne kadar uzun süre geciktirirseniz, o kadar esnek olabilir ve nasıl yapacağınıza karar verme konusunda o kadar özgür olursunuz. Bunu yapmak, yani işleri geciktirmek, DI Konteynerinin kullanımını mümkün kılan şeydir. İşleri uygulamanızın kökündeki tek bir noktaya kadar geciktirebilir ve ardından DI Container'ın oluşturma işlemini sizin için yapmasına izin verebilirsiniz.

Konteyner Ne Zaman Kullanılmalı

Container'ın ne zaman kullanılacağı sorusunun iki cevabı vardır.

Birincisi “her zaman”. Oluşturduğunuz herhangi bir önemli uygulamada her zaman Dependency Injection ve Dependency Injection Container'ı kullanmalısınız. DI konteyner kullanımı, uygulama geliştirirken normal, kabul edilen, günlük bir yol olmalıdır. Tamsayılar, sınıflar ve listeler kadar araç kutunuzun bir parçası olmalıdır.

Yukarıda ikinci cevaba değindim: Container'ı uygulamanızın bileşik kökünde bir kez ve yalnızca bir kez kullanmalısınız. Bu noktada muhtemelen bunun nasıl mümkün olduğunu merak ediyorsunuzdur; pek çok sınıf var! – ama sizi temin ederim ki bu yapılabilir ve yapılmalıdır. Bunu önümüzdeki bölümlerde tartışacağım.

Ancak elbette Bağımlılık Enjeksiyon Kabı kullanımının bununla ilişkili maliyetleri vardır ve doğal olarak bir tane kullanmaya karar vermeden önce bu maliyetleri göz önünde bulundurmalısınız. DI Container kullanmanın ana maliyeti, kişinin onu öğrenmek için harcaması gereken zamandır. (Aslında konuyla ilgili bir kitabın tamamını satın alıp okumalısınız!) Bir DI Container'ın kullanımına yönelik planlama ve tasarım yapmak bedava değildir. Ayrıca, Uygulamanıza önemsiz veya küçük olmayan bir çerçeve eklemenin ek maliyetleri vardır.

DI Container kullanmanın diğer bir maliyeti de tip kontrolü (type checking) için derleme zamanı kaybıdır. Bu önemsiz değildir. Derleyici, hatalara karşı ilk savunma hattınızdır ve eğer eski Bağımlılık Enjeksiyonu kullanıyorsanız, derleyici çoğunlukla bir hata yapıp yapmadığınızı size söyleyecektir. Ancak bir DI Container ile bunu kaybedersiniz. Nesne grafiğiniz çalışma zamanında oluşturulduğundan ve bağlandığından, hata yaparsanız çalışma zamanı hataları alırsınız. Belirli bir arayüz için bir uygulamayı kaydetmeyi unutursanız, bu eksik uygulamayı çalışma zamanında almaya çalışana kadar bunu bilemezsiniz. Bu az bir maliyet değil. Bunun yaşanmaması için mutlaka kodunuzun doğru şekilde oluşturulmasına dikkat etmeniz gerekecektir.

Bununla birlikte, eğer bir kişi DI Konteynerini doğru şekilde kullanırsa, faydalar maliyetlerden çok daha ağır basacaktır. Bu kitabın geri kalanında bu faydalardan bahsedeceğim.

Yetenekler ve İşlevsellik

Hangi Konteyner Kullanılmalı?

Kendiniz için oluşturduğunuz bir konteyneri kullanmalısınız. Spring for Delphi çerçevesi (Spring4D framework) tekil bir GlobalContainer sağlar, ancak bunun global bir değişken olduğu göz önüne alındığında kullanımından kaçınılmalıdır. Bunun yerine, kendi konteyner örneğinizi oluşturmalı ve bunu uygulamalarınızı kaydetmek (registering) ve çözmek (resolving) için kullanmalısınız.

Basit Nesne Kaydı

Container'a bir sınıf kaydetmek olabildiğince kolaydır:

MyContainer.RegisterType<TMyClass>

Bu kadar. Artık Container'ınız TMyClass'ı biliyor ve ihtiyaç duyduğu anda TMyClass'ın bir örneğini alabiliyor. Başka bir sınıfta bağımlılık olarak TMyClass varsa Container bunu otomatik olarak nasıl alacağını bilecektir. Örneğin, yapıcıya TMyClass enjekte edilmiş başka bir sınıfı kaydettirdiğinizi varsayalım:

type
  TAnotherClasss = class 
    constructor Create(aMyClass: TMyClass); 
  end; 
  .... 
   
    MyContainer.RegisterType<TAnotherClass>

Daha sonra TAnotherClass'a başvurursanız, Container size kurucunuz için TMyClass'ın bir örneğini sağlayacaktır. Hiçbir şey yapmanıza gerek yok. Container ihtiyacınızı görecek, bildiği tüm bağımlılıklara sahip en spesifik kurucuyu bulacak ve sınıfı sizin için yaratacaktır. Aslında tüm bunları siz istemeden önce yapacaktır. Bunların hepsi sizin için “nesne grafiğinizi oluşturmanın” bir parçası. Bu, Container'ın kullanılmayı bekleyen bir sınıflar paketinden daha fazlası olduğunun basit bir örneğidir.

Arayüzün Uygulanması

Daha önce, bir uygulamaya (implementation) değil, her zaman bir soyutlamaya (genellikle bir arayüze-interface'e) göre kodlamanız gerektiğini belirtmiştim. Konteyner bunu yapmayı gerçekten kolaylaştırıyor. Bir arayüzü ve bir uygulamayı birlikte kaydederek, Container'a belirli bir sınıfın bir arayüzü uyguladığını söyleyebilirsiniz ve ardından yukarıdaki gibi, belirli bir arayüz için bir uygulamaya ihtiyaç duyduğunuzda Container bunu sağlayacaktır: (Burada ILogger : arayüz, TLogger : uygulama-implementation)

MyContainer.RegisterType<ILogger, TLogger>;

Zeki okuyucular bunun önceki kitaplarımdaki arayüzlere göre sınıfları kaydetmeye yönelik farklı bir sözdizimi olduğunu fark edecektir. Daha önce bunları uygulama metodunu (the implements method) kullanarak kaydetmiştiniz, ancak kaydı yapmanın yukarıdaki yolu artık bir sınıfı ve arayüzü ilişkilendirmek için tercih edilen yöntemdir.

Yukarıdaki kod temel olarak "ILogger arayüzünü kaydedin ve bunun için TLogger uygulamasını kullanın" diyor. Bu sayede Dependency Injection yaparken arayüzleri kullanabilirsiniz ve Container her zaman soyutlamalara karşı kodlama arayışınızda size destek olacaktır.

Tek Arayüze Çoklu Uygulama 

Peki ya aynı arayüzü uygulayan birden fazla sınıfınız varsa? Sınıfları adlarına göre kaydedebilirsiniz:

MyContainer.RegisterType<ILogger, TFileLogger>('file');
MyContainer.RegisterType<ILogger, TConsoleLogger('console');
MyContainer.RegisterType<ILogger, TDatabaseLogger('database');

Yukarıdaki örnekler göz önüne alındığında, ada göre üç kayıt arasından istediğiniz uygulamayı seçebilirsiniz.

Ömür Yönetimi

Konteynerin temel özelliklerinden biri, size sağladığı nesnelerin ömrünü yönetebilme yeteneğidir. Delphi'de elbette bir nesnenin inşası sırasında ayrılan hafızanın uygun şekilde bertaraf edilmesinden siz sorumlusunuz. Konteynerin nesneyi sizin için tahsis etmesini sağladıysanız, Konteynere o nesnenin ömrünün nasıl yönetileceği konusunda talimat verebilirsiniz.

Delphi'de genellikle oluşturduğunuz herhangi bir nesnenin yok edilmesinden (freeing) siz sorumlusunuz. VCL'de, başka bir VCL nesnesinin sahip olduğu herhangi bir VCL nesnesinin, sahibi olan nesne tarafından yok edilmesi istisnası vardır. Ayrıca nesnenize arayüz (interface) olarak başvuruluyorsa, derleyici referans sayımı yapacak ve referans tamamen kapsam dışına çıktığında ve referans sayısı sıfıra düştüğünde nesnenizi otomatik olarak yok edecektir.

Ancak nesne grafiğinizi oluşturma sorumluluğunu Bağımlılık Enjeksiyon Konteynerine devrederseniz, Konteyner, oluşturduğu nesnelerin ömrünü yönetme görevini üstlenir. Bu, Konteyner bir nesnenin ömründen sorumlu olduğundan bunu yapmak zorunda olmadığınız anlamına gelir. Bu aynı zamanda nesnelerinizin kullanım ömrünün nasıl yönetilmesini istediğinizi konteynere bildirmeniz gerektiği anlamına da gelir. Bunu, aşağıdaki tabloda tanımlandığı şekilde Konteynere yapılan çağrılarla yapabilirsiniz:

AsTransient(Geçici) : Bu varsayılandır. Her istek için bir örnek oluşturur ve bu istek, değişken kapsam dahilinde olduğu sürece devam eder. Yani her istek için yeni bir örnek oluşturulur.

AsSingleton(Tekil) : Sınıfın tek bir örneğini oluşturur ve o sınıf için yapılan her istek için bu örneği döndürür. Bu tek sınıf, nesneye yapılan tüm referanslar için kullanılır.

AsSingeltonPerThread(Her thread için tek) : AsSingleton ile aynıdır ancak iş parçacığı (thread) başına bir örnek oluşturur.

AsPooled(Havuzlu) : Kullanıcı tarafından yapılandırılabilen boyutta bir nesne havuzu oluşturur ve ardından örnekleri bu havuzdan verir.

Şimdiye kadar Spring4D Container'ın akıcı bir arayüz kullandığını fark etmiş olabilirsiniz. Kaynak koduna bakıldığında, Container'a yapılan her çağrının her zaman TRegistration<T> örneğini döndürdüğünü ve çağrıları Container'a birlikte zincirlemenizi sağladığını gösterir. TRegistration<T>, Container'ın tüm işlevlerini yöneten ve aynı zamanda bir sınıfın tüm kaydını tanımlayan tek bir ifade oluşturmanıza olanak tanıyan bir sınıftır. Yani örneğin aşağıdakine benzer ifadelere sahip olabilirsiniz:

MyContainer.RegisterType<<IFirearm, TRifle>('rifle')
          .AsSingleton.InjectProperty('MetalSight', 'sight')
          .InjectField('Clip'); 

bu, bir sınıfı bir arayüz karşılığında kaydedecek ve onu tekil olarak beyan edecek, ayrıca bir özellik değeri ve bir alan değeri enjekte edecektir. Bağımlılıkları bildirmenin güçlü ve okunması kolay bir yoludur. Akıcı arayüzler genellikle cümlelere benzer.

Referansınızın tekil olmasını, yani belirli bir kayda yapılan tüm çağrılar için tek bir örnek olmasını istiyorsanız, bunu aşağıdaki gibi beyan edin:

MyContainer.RegisterType<IWeapon, TSword>.AsSingleton; 

Bu, bir IWeapon istediğinizde, tüm istekler için size aynı TSword örneğinin geri verilmesini sağlayacaktır.

AsSingletonPerThread, adından da anlaşılacağı gibi, belirli bir iş parçacığı (thread) içindeki her isteğe aynı örneği sağlar. Uygulamanın birden fazla örneği olabilir, ancak her iş parçacığı (thread) her zaman kendi örneğini alacaktır.

AsTransient varsayılan davranıştır. AsTransient, yapılan her istek için yeni bir örnek oluşturacaktır. Bir arayüz referansı kullandığınızı varsayarsak, bu geçici örnek, arayüz kapsam dahilinde kaldığı sürece (yani referans sayısı sıfırdan büyük olduğu sürece, "normal" referans sayımı kullandığınızı varsayarak) yaşayacaktır. Geçici, ömür boyu yönetim seçenekleri arasında en az verimli olanıdır çünkü Container'ın uzun süre yaşayan çok sayıda sınıf oluşturmasına neden olabilir.

AsPooled, istek üzerine kullanılmak üzere bir örnek havuzu oluşturacaktır. Havuzdaki minimum ve maksimum öğe sayısını belirleyebilirsiniz. Bir öğe Konteyner tarafından çözümlendiğinde öğe havuzundan alınacaktır. Havuzdaki tüm öğeler Konteyner oluşturulduğunda oluşturulur ve dolayısıyla program başladığında tümü kullanılabilir durumdadır. Oluşturma pahalı olduğunda ve bu maliyeti peşin ödemek istediğinizde veya sınırlı sayıda kaynağa sahip olduğunuzda ve kaynağı temsil eden sınıfın kaynak sayısından daha fazla örneğinin oluşturulmamasını sağlamak istediğinizde havuzlamayı kullanın. Havuza alınan öğeler hiçbir zaman paylaşılmaz; dolayısıyla bu da onu seçmek için bir neden olabilir.

DelegateTo

Bazen sınıfınızın kurucusu Container ile mükemmel bir şekilde işbirliği yapmayabilir ve çözülebilir bir bağımlılık haline gelebilir. Aşağıdaki sınıfı düşünün:

type
  IWindowsUser = interface
    ['{432973CE-CDDF-45CC-9BA0-EC089F23EAF4}']
    function GetUserName: string;
    property UserName: string read GetUserName;
  end;
  TWindowsUser = class(TInterfacedObject, IWindowsUser)
  private
    FUserName: string;
    function GetUserName: string;
  public
    constructor Create(const aUserName: string);
    property Username: string read GetUsername; 
  end;

İlk olarak, Windows sistemindeki bir kullanıcı adını temsil eden IWindowsUser adlı bir arayüz tanımlıyoruz. Ayrıca yapılandırıcıya parametresi olarak bir string alan bir uygulama sınıfı da sağlıyoruz. Bu arayüzün Konteynerimizin içine uygulanmasını istiyoruz, bu nedenle aşağıdakileri beyan ederiz:

MyContainer.RegisterType<IWindowsUser, TWindowsUser>.DelegateTo(function: TWindowsUser
           begin
             Result := TWindowsUser.Create(GetLocalUserName);
           end)

Daha sonra, IWindowsUser arayüzünün uygulaması olarak TWindowsUser'ı kaydediyoruz (register). Bir sınıfın yapılandırıcı parametrelerinin otomatik olarak çözümlenmesi için o sınıfı Container'a kaydetmeniz gerektiğini unutmayın. Ancak burada bir aksaklık var: Yapılandırıcı parametre olarak bir string alıyor; yeni bir kullanıcı Windows'ta her oturum açtığında değişen bir string. Bu nedenle, Container'ın bir TWindowsUser örneği oluşturmaya nasıl devam etmesi gerektiği açık değildir. Bu nedenle, Konteyner için TWindowsUser'ın bizim için DelegateTo çağrısı aracılığıyla tam olarak nasıl oluşturulmasını istediğimizi tanımlarız. Bu metot, bir TWindowsUser döndüren anonim bir fonksiyonu alır ve size Container'a bir TWindowsUser'ın nasıl oluşturulacağını söyleme fırsatı verir.

Bir ek not olarak GetLocalUserName şu şekilde tanımlanır:

function GetLocalUserName: string;
var
  aLength: DWORD;
  aUserName: array [0 .. Max_Path - 1] of Char;
begin
  aLength := Max_Path;
  if not GetUserName(aUserName, aLength) then
  begin
    raise Exception.CreateFmt('Win32 Error %d: %s', [GetLastError,     SysErrorMessage(GetLastError)]);
  end;
  Result := string(aUserName);
end;

Konteyner aksi takdirde bir sınıfın örneğinin nasıl oluşturulacağını bilemeyeceği durumlarda DelegateTo'yu kullanın. Bunun nedeni, yapılandırıcının parametrelerinin Konteyner tarafından çözülememesi veya yapıcının ihtiyaç duyduğu bilgilerin Konteynerde depolanamayan harici bilgilere bağlı olması olabilir.

DelegateTo kullanımıyla ilgili bazı uyarılar vardır. DelegateTo kullanımını sınırlandırmaya çok dikkat etmeli ve Container'ın bu sorunu çözmesinin yollarını bulmalısınız. Aksi takdirde, DelegateTo'ya yalnızca Container adını verdikleri çok sayıda çağrıyla kolayca karşılaşabilirsiniz. Bu gerçekten Pure DI'nin başka bir biçimi haline geliyor, ancak konteynerin yükü de dahil.

Sonuç

Container'ın temelleri budur. DI Container son derece kullanışlı bir araçtır. Tek bir kod satırıyla tek bir noktada tüm nesne grafiğinizi oluşturmanıza olanak tanır. Bu nedenle uygulamanız, bağımlılıklarını isteyen ve Container'ın kendisi hakkında hiçbir şey bilmeyen (MÖ'nün notu : çünkü bu nesneler birbirlerinin ve konteynerin kodlarını oluşturan implementation'ları göremiyorlar), gevşek bağlaşmış birçok sınıftan oluşabilir. Daha sonra, Container ile yapabileceklerinizi daha da güçlendirecek daha gelişmiş konuları ele alacağım, ancak şimdilik Container'ın nasıl çalıştığını ve nasıl kullanılması gerektiğini gösteren temel bir örneğe göz atmak için yeterli araca sahip olmalısınız. . Bir sonraki bölümde yapacağım şey bu.



Adım Adım Örnek

Şu ana kadar Konteynerin spesifik yeteneklerini gösteren basit örneklere baktık. Peki tüm bunlar gerçekten nasıl çalışıyor? Burada gerçekten neyden bahsediyoruz? Bu bölümde, "normal" olarak başlayan, yani işlerin geleneksel olarak nasıl yapıldığına dair bir örneğe göz atacağız ve ardından temiz ve ayrıştırılmış olacak şekilde nasıl yeniden düzenlenebileceğini (refactoring) ve aynı zamanda DI konteynerinin verdiği güçle test edilmesinin, ayrıştırılmasının ve bakım yapılmasının kolay olduğunu göstermeye çalışacağız.

Başlangıç

Bu bölümün kodunu şu adreste bulabilirsiniz: Kod

Öncelikle demo uygulamamızın kökünde, projenin DPR dosyasında çağrılan DoOrderProcessing çağrısıyla başlayacağız:

procedure DoOrderProcessing;
var
  Order: TOrder;
  OrderProcessor: TOrderProcessor;
begin
  Order := TOrder.Create;
  try
    OrderProcessor := TOrderProcessor.Create;
    try
      if OrderProcessor.ProcessOrder(Order) then
      begin
        WriteLn('Order successfully processed....');
      end;
    finally
      OrderProcessor.Free;
    end;
  finally
    Order.Free;
  end;
end;

Burada oldukça tipik bazı kodlar görüyoruz. Bir sipariş oluşturulur ve sipariş işlemcisi tarafından işlenir. İşlem tamamlandıktan sonra işlemci yok edilir (free). Çok basit, temel kod. Muhtemelen buna benzer bir şeyi milyonlarca kez yapmışsınızdır.

TOrderProcessor neye benziyor?

type
  TOrderProcessor = class
  private
    FOrderValidator: TOrderValidator;
    FOrderEntry: TOrderEntry;
  public
    constructor Create;
    destructor Destroy; override;
    function ProcessOrder(aOrder: TOrder): Boolean;
  end;
constructor TOrderProcessor.Create;
begin
  FOrderValidator := TOrderValidator.Create;
  FOrderEntry := TOrderEntry.Create;
end;
destructor TOrderProcessor.Destroy;
begin
  FOrderValidator.Free;
  FOrderEntry.Free;
  inherited;
end;
function TOrderProcessor.ProcessOrder(aOrder: TOrder): Boolean;
var
  OrderIsValid: Boolean;
begin
  Result := False;
  OrderIsValid := FOrderValidator.ValidateOrder(aOrder);
  if OrderIsValid then
  begin
    Result := FOrderEntry.EnterOrderIntoDatabase(aOrder);
  end;
  WriteLn('Order has been processed....');
end;

TOrderProcessor'un bildirimi ve uygulanması hakkında dikkat edilmesi gereken bazı noktalar şunlardır:

  • Yapılandırıcı parametresizdir. Sonuç olarak, iki bağımlılık, TOrderValidator ve TOrderEntry, TOrderProcessor'un yapılandırıcısındaki Constructor çağrıları aracılığıyla sabit kodlanmıştır. Kendimizi bu iki sınıfın verilen uygulamalarına sıkı bir şekilde bağladık.
  • Bu, bu sınıfın test edilmesini zorlaştırıyor çünkü bu iki bağımlılığı sahtelerle değiştiremeyiz.
  • Normal olduğu gibi, bağımlılıkların ömrünü manuel olarak yönetmemiz gerekiyor, bu da sınıf için bir yok etme işlemiyle sonuçlanıyor.

Yapılandırıcı Enjeksiyonuna Giriş

İşleri daha iyi hale getirmek için bazı adımlar atalım. İlk olarak, TOrderProcessor'ın bağımlılıklarını, özellikle de bazı Constructor Injection'larını enjekte etmek için bağımlılık enjeksiyonunu kullanacağız:

type
  TOrderProcessor = class
  private
    FOrderValidator: TOrderValidator;
    FOrderEntry: TOrderEntry;
  public
    constructor Create(aOrderValidator: TOrderValidator; aOrderEntry: TOrderEntry);
    function ProcessOrder(aOrder: TOrder): Boolean;
  end;
constructor TOrderProcessor.Create(aOrderValidator: TOrderValidator; aOrderEntry: TOrderEntry);
begin
  FOrderValidator := aOrderValidator;
  FOrderEntry := aOrderEntry;
end;
function TOrderProcessor.ProcessOrder(aOrder: TOrder): Boolean;
var
  OrderIsValid: Boolean;
begin
  Result := False;
  OrderIsValid := FOrderValidator.ValidateOrder(aOrder);
  if OrderIsValid then
  begin
    Result := FOrderEntry.EnterOrderIntoDatabase(aOrder);
  end;
  WriteLn('Order has been processed....');
end;

Artık işlev içindeki bağımlılıkların ömründen sorumlu olmadığımızı (çağrıyı yapan kişidir) ve dolayısıyla Destroy işleminin gittiğini unutmayın.

Bu, elbette DoOrderProcessing prosedürümüzü biraz değiştiriyor, çünkü oluşturmayı uygulamanın tabanına daha yakın bir yere "geri ittik":

procedure DoOrderProcessing;
var
  Order: TOrder;
  OrderProcessor: TOrderProcessor;
  OrderValidator: TOrderValidator;
  OrderEntry: TOrderEntry;
begin
  Order := TOrder.Create;
  try
    OrderValidator := TOrderValidator.Create;
    OrderEntry := TOrderEntry.Create;
    OrderProcessor := TOrderProcessor.Create(OrderValidator, OrderEntry);
    try
      if OrderProcessor.ProcessOrder(Order) then
      begin
        WriteLn('Order successfully processed....');
      end;
    finally
      OrderProcessor.Free;
      OrderValidator.Free;
      OrderEntry.Free;
    end;
  finally
    Order.Free;
  end;
end;

Burada uygulamanın “kökünde” sipariş doğrulayıcıyı ve sipariş giriş sınıflarını oluşturuyoruz. (DoOrderProcessing, DPR dosyasında çağırdığımız ana yordam). Zaten bu basit değişiklikle TOrderProcessor'ı biraz daha esnek hale getirdik. Kendimizi hala belirli uygulamalarla (implementation) bağlaşmış olsak da, TOrderProcessor'un kendisi artık belirli bir TOrderValidator ve TOrderEntry örneğine bağlı değildir. Bu sınıfın sahtesini veya ilgili sınıfların herhangi bir soyunu kabul edebilir. Çok fazla değil ama önemli bir şey.

Bir Arayüze Kodlama

Bu bağlantıyı nasıl daha da gevşetebiliriz? Elbette arayüzlerle tanışacağız. Unutmayın, biz her zaman bir uygulamaya (implementation) değil, bir arayüze (interface) kodlama yapmak istiyoruz. Şu ana kadar bir uygulamaya kodladık ve bunun neden olduğu sınırlamaları görebilirsiniz; biz bu uygulamalara (implementation'lara) bağlıyız. Kendimizi bu bağlardan kurtaralım.

Öncelikle bazı arayüzleri tanımlayacağız:

IOrderValidator = interface
    ['{CF8834A3-F815-4F6B-A177-7AB801BEC95E}']    // Ctrl+Shift+G
  function ValidateOrder(aOrder: TOrder): Boolean;
end;
IOrderEntry = interface
    ['{406EA68D-0733-429E-9E48-73BC660B1C72}']
  function EnterOrderIntoDatabase(aOrder: TOrder): Boolean;
end;
IOrderProcessor = interface
    ['{C690B9D5-8C26-4DFE-AD27-2D7A4610ACBC}']
  function ProcessOrder(aOrder: TOrder): Boolean;
end;

Bunlar tanımlandıktan sonra, uygulamalar (implementation-normal class) için yazmak yerine bunlara (interface) karşı kod yazabiliriz:

type
  TOrderProcessor = class(TInterfacedObject, IOrderProcessor)
  private
    FOrderValidator: IOrderValidator;
    FOrderEntry: IOrderEntry;
  public
    constructor Create(aOrderValidator: IOrderValidator; aOrderEntry: IOrderEntry);
    function ProcessOrder(aOrder: TOrder): Boolean;
  end;
constructor TOrderProcessor.Create(aOrderValidator: IOrderValidator; aOrderEntry: IOrderEntry);
begin
  FOrderValidator := aOrderValidator;
  FOrderEntry := aOrderEntry;
end;
function TOrderProcessor.ProcessOrder(aOrder: TOrder): Boolean;
var
  OrderIsValid: Boolean;
begin
  Result := False;
  OrderIsValid := FOrderValidator.ValidateOrder(aOrder);
  if OrderIsValid then
  begin
    Result := FOrderEntry.EnterOrderIntoDatabase(aOrder);
  end;
  WriteLn('Order has been processed....');
end;
İlk olarak, sipariş doğrulayıcıya ve sipariş giriş sınıflarına yapılan tüm referansları, sınıf türleri yerine arayüz olacak şekilde değiştirdik. Bu, artık test amaçlı sahte uygulamalar da dahil olmak üzere her türlü uygulamayı gerçekten TOrderProcessor'a aktarabileceğimiz anlamına geliyor. Bir arayüze karşı kodlama yaptığımız için Free'ye yapılan çağrıları da kaldırabiliriz. DoOrderProcessing çağrısı şu anda şöyle görünüyor:
procedure DoOrderProcessing;
var
  Order: TOrder;
  OrderProcessor: IOrderProcessor;
  OrderValidator: IOrderValidator;
  OrderEntry: IOrderEntry;
begin
  OrderValidator := TOrderValidator.Create;
  OrderEntry := TOrderEntry.Create;
  Order := TOrder.Create;
  try
    OrderProcessor := TOrderProcessor.Create(OrderValidator,                                     OrderEntry);
    if OrderProcessor.ProcessOrder(Order) then
    begin
      WriteLn('Order successfully processed....');
    end;
  finally
    Order.Free;
  end;
end;

Bu zaten çok daha basit. TOrderValidator ve TOrderEntry'nin belirli uygulamalarına (implementation) hala sabit kodlanmış durumdayız, ancak referansları arayüz olduğundan bu bağlaşımdan bir adım daha uzaklaştık. Sonuç olarak kod zaten çok daha temiz görünüyor.

Bu iki Create çağrısı daha da "geri itilebilir"; bunları doğrudan kompozisyon köküne koyalım, böylece DoOrderProcessor onlara sıkı bağlantı gerektirmez. İşte yeni DoOrderProcessing:

procedure DoOrderProcessing(aOrderValidator: IOrderValidator; aOrderEntry: IOrderEntry);
var
  Order: TOrder;
  OrderProcessor: IOrderProcessor;
begin
  Order := TOrder.Create;
  try
    OrderProcessor := TOrderProcessor.Create(aOrderValidator, aOrderEntry);
    if OrderProcessor.ProcessOrder(Order) then
    begin
      WriteLn('Order successfully processed....');
    end;
  finally
    Order.Free;
  end;
end;

ve işte DPR dosyasındaki şu anda onu çağıran kod:

var
  OrderValidator: IOrderValidator;
  OrderEntry: IOrderEntry;
begin
  try
    OrderValidator := TOrderValidator.Create;
    OrderEntry := TOrderEntry.Create;
    DoOrderProcessing(OrderValidator, OrderEntry);
    ReadLn;
  except
    on E: Exception do
      WriteLn(E.ClassName, ': ', E.Message);
  end;
end.

Bu noktada Dependency Injection’ı elimizden geldiğince taşıdık. Nesnelerimizin oluşturulmasını uygulamamızın Bileşik Köküne (bu durumda Delphi'deki DPR dosyasının ana bloğuna) kadar ittik. Burada tüm bağımlılıklarımızı DPR dosyasının en başında oluşturuyoruz. Olayları bundan daha geriye itemeyiz.

Şimdi, bunların hepsi harika ve her şey - tüm bağımlılık oluşturma işlemimiz uygulamanın kökünde merkezileştirilmiştir - ancak bu büyük bir soruna yol açabilir. Uygulamamız çok daha karmaşık hale gelseydi ve bu kalıbı (pattern) izlemeye devam edersek, DPR dosyasında çok sayıda şeyin oluşturulmasına sahip olurduk. Bu iyi olurdu, ama gerçekten çok hızlı bir şekilde hantallaşabilir. Hantallık, kodumu tanımlamak isteyeceğim bir kelime değil. Ah, keşke tüm bu yaratma işlemlerini yönetmenin bir yolu olsaydı!

Konteyneri Girin

Elbette tüm bu nesneleri yaratmayı yönetmenin bir yolu var: Bağımlılık Enjeksiyon Konteyneri. Birikecek tüm sınıfları oluşturabilir ve hepsini Konteynere koyabiliriz. Şimdi bunu yapalım.

Yeni bir birim-unit oluşturalım ve tüm kayıtları oraya koyalım. Bu şekilde, her şey merkezileştirilir ancak düzgün bir şekilde ortadan kaldırılır. Birimi uRegistration.pas olarak adlandıracağız ve şu şekilde görünmesini sağlayacağız:

unit uRegistration;
interface
procedure RegisterClassesAndInterfaces;
implementation
uses
  Spring.Container
, uOrderEntry
, uOrderValidator
, uOrderProcessor
;
procedure RegisterClassesAndInterfaces(aContainer: TContainer);
begin
  aContainer.RegisterType<IOrderProcessor, TOrderProcessor>.AsSingleton;
  aContainer.RegisterType<IOrderValidator, TOrderValidator>.AsSingleton;
  aContainer.RegisterType<IOrderEntry, TOrderEntry>.AsSingleton;
  aContainer.Build;
end;
end.

Burada yaptığımız şey oldukça basit. Öncelikle TOrderProcessor tipini konteynere kaydettik. Bu, Container'ın sınıf hakkında bilgi sahibi olmasını sağlar ve göreceğimiz gibi Container'ın tüm bağımlılıklarını otomatik olarak çözmesine olanak tanır. Ek olarak, belirli uygulamaları uygulayacak bağımlılıkları tutan 2 sınıfı kaydederiz. Bu, konteynerin, onlara karşı kayıtlı somut sınıfları kullanarak arayüzlere yapılan referansları çözümlemesine neden olacaktır. Konteyner, bir IOrderValidator örneği istendiğinde, bunu uygulamak için geçerli bir TOrderValidator örneği sağlayabilecektir. Oldukça havalı. Son olarak, tüm sınıflar tekil (singleton) olabileceğinden (onlardan her zaman aynı şeyi yapmaları istenecektir) onları AsSingleton'un ömür boyu yönetim (AsSingleton) çağrısıyla kaydederiz.

Artık bu sınıfları ve arayüzleri kaydettirdiğimize göre her türlü harika şey oluyor. Bunlardan ilki, DPR dosyasının gerçekten basit hale gelmesidir. DPR'nin kalbi şu şekilde görünüyor:

var
  Container: TContainer;
begin
  try
    Container := TContainer.Create;
    try
      RegisterClassesAndInterfaces(Container);
      DoOrderProcessing;
      ReadLn;
    finally
      Container.Free;
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Bu kodda dikkat edilmesi gereken birkaç nokta var:

  • Bu bölümün demo uygulamasını indirip çalıştırdıysanız kodun beklendiği gibi çalıştığını göreceksiniz. İlk bakışta bu bir sürpriz olabilir. Ama elbette her şey açıklanacak.
  • İki bağımlılığa ilişkin değişkenler ortadan kalktı; ileride göreceğimiz gibi artık onlara ihtiyacımız yok.
  • DoOrderProcessing'deki parametreler de ortadan kalktı. Onlara da ihtiyacımız yok.
  • Kodda hiçbir şey yaratılmıyor. Bu biraz gizemli. Ama yine her şey açıklanacak.
  • Container.Build'a bir çağrı var. Bu çağrı, Konteynerin nesne grafiğini oluşturmasını bilmesini sağlamak için gereklidir. Konteynerin sihrini gerçekleştirmesini sağlamak için ihtiyacınız olan tek şey bu. Bu çağrıyı, bizim burada yaptığımız gibi, uygulamanın en kökünde bir kez ve yalnızca bir kez yapmalısınız. Bunu bir kereden fazla çağırmak yalnızca sürecin iki kez çalışmasına ve CPU döngülerinin boşa gitmesine neden olur.

DoOrderProcessing çağrısında gerçekten harika şeyler oluyor. İşte şu anki halinde:

procedure DoOrderProcessing(aContainer: TContainer);
var
  Order: TOrder;
OrderProcessor: IOrderProcessor;
begin
  Order := TOrder.Create;
  try
    OrderProcessor := aContainer.Resolve<IOrderProcessor>;
    if OrderProcessor.ProcessOrder(Order) then
    begin
      WriteLn('Order successfully processed....');
    end;
  finally
    Order.Free;
  end;
end;

Parametrelerin ortadan kalktığına bir kez daha dikkat edin. Ardından OrderProcessor referansının aContainer.Resolve'a yapılan tek bir çağrıyla karşılandığına dikkat edin. Bu tek çağrı pek çok şey yapar. İlk olarak, IOrderProcessor'un bir uygulamasını (bu durumda TOrderProcessor'un bir örneğini) tamamen başlattığını fark edeceksiniz. İşte Container'ı bir fabrikadan daha fazlası haline getiren "sihir" burada gerçekleşiyor

TOrderProcessor'un beyanını hatırlıyor musunuz?

TOrderProcessor = class(TInterfacedObject, IOrderProcessor)
private
  FOrderValidator: IOrderValidator;
  FOrderEntry: IOrderEntry;
public
  constructor Create(aOrderValidator: IOrderValidator; aOrderEntry: IOrderEntry);
  function ProcessOrder(aOrder: TOrder): Boolean;
end;

Gördüğünüz gibi yapılandırıcı iki bağımlılık alır. Yani tüm bu kod çalışıyor ve hiçbir yerde bu iki arayüz parametresine eklenecek örnekler (object instance) oluşturmuyoruz. Burada neler oluyor?

Olan şu ki Konteyner ne yapacağını bilecek kadar akıllı. Bu arayüzlerin her ikisi için de uygulamaları kaydettiğinizden, Konteyner neyi kaydettiğinizi görebilir ve yapıcı parametrelerini uygun şekilde örnekleyerek TOrderProcessor'un bir örneğini oluşturabilir. Tıpkı Container'ın kendi kendine söylediği gibi:

"Tamam. IOrderProcessor için bir uygulama oluşturmam istendi. TOrderProcessor'un IOrderProcessor'ı uygulayacak şekilde kayıtlı olduğunu görüyorum. Bu yüzden TOrderProcessor'un bir örneğini oluşturmam gerekiyor. Hey, bak, bir yapılandırıcısı var! Bu harika ama çözmem gereken iki bağımlılık var. Bunlardan ilki IOrderValidator'dır. Hey, bu arayüz için kayıtlı bir sınıfım var! Ayrıca IOrderEntry için de bir tane var! Ne kadar rahatladım! Bu, arayüzler için bu sınıfların örneklerini oluşturabileceğim, bu iki uygulamayı TOrderProcessor'un yapılandırıcısına aktarabileceğim ve ardından bunu IOrderProcessor'un bir uygulaması olarak döndürebileceğim anlamına geliyor. Ve sonra işim bitti!"

Bunların hepsi Konteynerin içinde "otomecikali-otomatik+sihirli" gerçekleşir. Oğlum, bu Konteyner kesinlikle akıllı, değil mi?

Bu akıllıca şeyler(!) sonucunda birkaç şey yapabildik:

  • Yazmamız gereken kod miktarını büyük ölçüde azaltabildik. Konteyner tüm oluşturma işlemini gerçekleştirdiğinden, Create'e herhangi bir çağrımız yok. Ne kadar az kod yazmamız gerekiyorsa, o kadar az kodu korumamız gerekir. Bu bir kazanç.
  • Kodumuzu çok gevşek bir şekilde birleştirebildik. Tüm sınıfların her birinin kendi birimlerinde tek başına durduğunu unutmayın. Arayüzleri kendi birimlerine koyabiliriz ve her şeyin bir araya geldiği tek yer, konteyner ile her şeyi kaydeden birimdir ve orada bile ilişki sadece bir arayüz ile uygulama arasındadır. Bu oldukça incedir ve bu nedenle gevşek bağlantıya neden olur. Başka bir deyişle, endişelenmemiz gereken tek şey İsim Uyumu'dur.
  • Test yapmak gerçekten kolaylaşıyor çünkü her sınıf tek başına duruyor ve bağımlılıklarını açıkça beyan ediyor. Açıkça bildirilen bağımlılıklar, bu bağımlılıkların yerine sahteleri kolayca koyabileceğiniz ve sınıfları kolayca test edilebilir hale getirebileceğiniz anlamına gelir.

Bu arada, birden fazla yapılandırıcıya (constructor'a) sahip bir sınıfı kaydederseniz, konteyner her birine bakacak ve çözebileceği en spesifik olanı bulacaktır. Yani, bir çözülebilir bağımlılığın yanı sıra iki çözülebilir bağımlılığı olan bir yapılandırıcınız varsa, konteyner ikincisini kullanacaktır. Ancak sınıfınızın, sınıftaki tüm bağımlılıkları bildiren tek bir yapılandırıcıya sahip olmasını öneririm.

Sonuç

Ve bu şekilde Konteyner, Konteynere yalnızca bir Resolve çağrısı yaparken tam bir nesne grafiği oluşturabilir. Bunu yapmak gerçekten hoş bir kodla sonuçlanır. Tüm işlerimizi yapmak için Container'ı kullanarak kendimizi nesneleri oluşturmak ve yok etmek gibi birçok tesisat kodu yazmaktan kurtarıyoruz ve iş mantığını yazmaya odaklanmamızı sağlıyor. Ve şimdi kodunuz gevşek bağlı (loosely coupled) olacak ve dolayısıyla bakımı ve test edilmesi kolay olacak.

Gelişmiş Konteyner Kullanımı

Şu ana kadar size Yapılandırıcı Enjeksiyonu, Property (veya Setter) Enjeksiyonu ve Method Enjeksiyonu dahil olmak üzere Bağımlılık Enjeksiyonunun temellerini gösterdim. Dependency Injection konteynerinin nasıl çalıştığına dair temel bilgilere bir göz attım ve size Konteynerin "sihrini" gerçekleştirmesinin temel bir örneğini gösterdim. Artık konteynerin kodunuzu daha da güçlü kılmak için yapabileceği bazı gelişmiş şeylere göz atmanın zamanı geldi.

Çalışma Zamanında Çoklu Uygulamalar

Daha önce de belirttiğim gibi tek bir arayüze birden fazla uygulama (implementation-interface için class'ın kodları) kaydedebilirsiniz. Bu bölümde buna bir göz atacağız.

Meyveyi kim sevmez? Meyvenin bir yerde yetiştirilmesi ve büyüdükten sonra toplanması gerekiyor. Ancak meyve toplamanın birçok farklı yolu vardır. Yani eğer meyve toplamayı modellemek istiyorsak, bir arayüz oluşturmak gerekecektir:

type
  IFruitPicker = interface
  ['{DD861C60-D9A0-411C-8448-2E3B798026DB}']
    procedure PickFruit;
end;

Artık programlayabileceğimiz bir arayüze sahip olduğumuza göre meyveleri istediğimiz şekilde toplayabiliriz. İşte üç uygulama:

type
  THumanFruitPicker = class(TInterfacedObject, IFruitPicker)
    procedure PickFruit;
  end;
  TMechanicalFruitPicker = class(TInterfacedObject, IFruitPicker)
    procedure PickFruit;
  end;
  TAndroidFruitPicker = class(TInterfacedObject, IFruitPicker)
    procedure PickFruit;
  end;
procedure THumanFruitPicker.PickFruit;
begin
  WriteLn('Carefully hand-pick the fruit.....');
end;
procedure TMechanicalFruitPicker.PickFruit;
begin
  WriteLn('Pick the fruit with a mechanical device....');
end;
procedure TAndroidFruitPicker.PickFruit;
begin
  WriteLn('Pick the fruit with android-like robots....');
end;
end.

Artık meyveleri insanların, makinelerin ve android robotların seçmesini sağlayabiliriz. Tüm bu meyve toplama yöntemlerini konteynere nasıl kaydedip meyve toplamak için birini nasıl seçebiliriz? Elbette aşağıdaki gibi:

procedure RegisterFruitPickers(aContainer: TContainer);
begin
  aContainer.RegisterType<IFruitPicker, THumanFruitPicker>('human').AsDefault;
  aContainer.RegisterType<IFruitPicker, TMechanicalFruitPicker>('mechanical');
  aContainer.RegisterType<IFruitPicker, TTAndroidFruitPicker>('android');
end;

Üç kaydın her birinin, kaydı tanımlayan benzersiz bir dize içerdiğine dikkat edin. Bu şekilde çalışma zamanında istediğimiz uygulamayı ismine göre seçebiliriz. THumanFruitPicker'ın AsDefault yöntemiyle bildirildiğine de dikkat edin. Bu, bir ad belirtilmeden bir IFruitPicker için istekte bulunulursa THumanFruitPicker'ın kullanılacağı anlamına gelir.

Hangi meyve toplayıcıyı kullanacağınızı seçmenizi sağlayan DPR dosyası:


type
  TPickerType = (human, mechanical, android);
var
  Name: string;
  Selection: integer;
  FruitPicker: IFruitPicker;
  Container: TContainer;
begin
  try
    Container := TContainer.Create;
    try
      RegisterFruitPickers(Container);
      WriteLn('Enter in the fruit picker to use: ');
      WriteLn('Pick 1 for a human, 2 for a machine, and 3 for an android robot.');
      ReadLn(Selection);
      Name := GetEnumName(TypeInfo(TPickerType), Selection - 1);
      FruitPicker := Container.Resolve<IFruitPicker>(Name);
      FruitPicker.PickFruit;
    finally
      Container.Free;
    end;
  except
    on E: Exception do
      WriteLn (E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

Burada üç meyve toplayıcıyı tanımlayan basit bir numaralandırma yapacağız. Daha sonra istediğiniz meyve toplayıcı için bir sayı seçmenize ve daha sonra IFruitPicker arayüzünü istediğimiz uygulamayla çözmek için kullanacağımız bir dize elde etmek için (TypInfo.pas biriminden) küçük bir tür bilgisi kullanmanıza izin veriyoruz.

Uygulamayı çalıştırıp makine toplayıcı “2”yi seçtiğinizde konsolda aşağıdaki sonuç elde edilir:


Program yürütüldüğünde makine meyve toplayıcısının toplaması sonuçları

Böylece belirli bir arayüz için birden fazla uygulamayı kaydedebilir ve istediğiniz zaman istediğiniz uygulamayı seçebilirsiniz.

Tembel Başlatma (Lazy Initialization)

Container.Build'ı çağırdığınızda, konteyner nesne grafiğinin tamamını aynı anda oluşturacaktır. Bu sorun değil; uygulamanız ihtiyaç duyulduğunda işleri çalıştırmaya hazır olmalıdır. Ancak bazen belirli bir uygulamanın oluşturulması maliyetli olabilir veya uygulamanızın normal çalışması sırasında kullanılması pek mümkün olmayan bir uygulama olabilir. Bu durumda, uygulamanın "tembel" başlatılmasını yapmak isteyebilirsiniz. Tembel başlatma, verilen nesnenin yapısının gerçekten talep edilene kadar ertelenmesi anlamına gelir. Bu şekilde belki de hemen tahsis edilmesi gerekmeyen bazı kaynaklardan tasarruf edebilir veya nadiren kullanıldıkları takdirde bu kaynakların tahsis edilmesini tamamen engelleyebilirsiniz.

Spring4D framework'ü, Lazy<T> kaydı aracılığıyla tembel başlatma yapmanız için bir araç sağlar. Türünüzü Tembel olarak bildirebilirsiniz ve çerçeve, istenen türü yaratmayacaktır.

Her zaman olduğu gibi bir arayüz bildirerek başlayacağız:

type
  IDatabaseConnector = interface
  ['{8E00247F-F910-41C1-9122-523F2CB6E5CB}']
    procedure Connect(const aName: string);
  end;

Bu sadece bir demo, bu yüzden gerçek veritabanı bağlantıları yapmayacağız, ancak "bağlanıyor"muş gibi yaparken biraz gecikmeye neden olarak bunu simüle edeceğiz. Bunu göz önünde bulundurarak, IDatabaseConnector arayüzümüzün bir uygulamasını burada bulabilirsiniz:

  TDatabaseConnector = class(TInterfacedObject, IDatabaseConnector)
  private
    FConnected: Boolean;
  public
    constructor Create;
    procedure Connect(const aName: string);
    property Connected: Boolean read FConnected write FConnected;
  end;

  constructor TDatabaseConnector.Create;
  begin
    inherited Create;
    FConnected := False;
    Connect(Self.ClassName);
  end;

  procedure TDatabaseConnector.Connect(const aName: string);
  begin
    if not Connected then
    begin
      WriteLn('Now connecting with ', aName);
      Sleep(3000);
      WriteLn('Connected! Sorry I took so long!');
      Connected := True;
    end;
  end;

Bu sınıf, IDatabaseConnector arayüzünü uygular, ancak bağlanması biraz zaman alır. Konsola yazıp üç saniye bekledikten sonra tamamlandığını konsola tekrar yazarak "bağlanır". Yine bu bir demo bağlantısı ama göreceğiniz gibi bizim amaçlarımız için yeterli.

Asıl eğlence, bu türü gerçekten kullanan bir sınıfta ilan ettiğimizde başlar. Örneğin, burada nesnedeki bir alanı dolduran, yapıcıdaki bir IDatabaseConnector parametresini kabul eden TDatabaseConnectionManager adlı bir sınıf var.

  TDatabaseConnectionManager = class
  private
    FDatabaseConnector: IDatabaseConnector;
  public
    constructor Create(aDatabaseConnector: IDatabaseConnector);
  end;

constructor TDatabaseConnectionManager.Create(aDatabaseConnector: IDatabaseConnector);
begin
  inherited Create;
  FDatabaseConnector := aDatabaseConnector;
end;
Bu sınıf, bir IDatabaseConnector örneği oluşturup ilettiğinde, bu örneği daha sonra kullanmak üzere depolar. Ancak her şeyi Konteynere kaydedersek:
procedure RegisterStuff(aContainer:TContainer);
begin
  aContainer.RegisterType<TDatabaseConnector, IDatabaseConnector>;
  aContainer.RegisterType<TDatabaseConnectionManager>;
  aContainer.Build;
end;

IDatabaseConnector uygulaması Container tarafından otomatik olarak oluşturulacaktır. Peki ya bunun olmasını istemiyorsan? Değişkeni biraz farklı şekilde bildirebilirsiniz:

  TDatabaseConnectionManagerLazy = class
  private
    FDatabaseConnector: Lazy<IDatabaseConnector>;
  public
    constructor Create(aDatabaseConnector: Lazy<IDatabaseConnector>);
    procedure ConnectToDatabase;
  end;

constructor TDatabaseConnectionManagerLazy.Create(aDatabaseConnector: Lazy<IDatabaseConnector>);
begin
  inherited Create;
  FDatabaseConnector := aDatabaseConnector;
end;

procedure TDatabaseConnectionManagerLazy.ConnectToDatabase;
begin
  FDatabaseConnector.Value.Connect(Self.ClassName);
end;
Buradaki tek fark, yalnızca IDatabaseConnector yerine Lazy<IDatabaseConnector> olarak bildirilen FDatabaseConnector bildirimidir. Aynı şey yapıcının parametresi için de geçerlidir. Bunu Tembel olarak bildirerek, Container'a bağlantıyı hemen değil, talep üzerine oluşturması talimatını verir. Bu talep, tembel olarak başlatılan değişkenin Value özelliğini istediğinizde ortaya çıkar.

Bütün bunları uygulayalım ve nasıl çalıştığını görelim.

İşte DPR dosyası tarafından çağrılan Main adlı bir prosedür:

procedure Main(aContainer: TContainer);
var
  DatabaseConnectionManager: TDatabaseConnectionManager;
  DatabaseConnectionManagerLazy: TDatabaseConnectionManagerLazy;
begin
  aContainer.RegisterType<TDatabaseConnector, IDatabaseConnector>;
  aContainer.RegisterType<TDatabaseConnectionManager>;
  aContainer.RegisterType<TDatabaseConnectionManagerLazy>;
  aContainer.Build;

  WriteLn('Everything is registered');
  ReadLn;

  WriteLn('About to create DatabaseConnectionManager');
  DatabaseConnectionManager := aContainer.Resolve<TDatabaseConnectionManager>;
  WriteLn('Note that Connection is made without anything being done on your part.');
  WriteLn('The connection occurs as part of the magic of the Container.');
  WriteLn('All Done');
  WriteLn;
  ReadLn;

  WriteLn('About to create DatabaseConnectionManagerLazy');
  DatabaseConnectionManagerLazy := aContainer.Resolve<TDatabaseConnectionManagerLazy>;
  WriteLn('Note that connection is not made until specifically asked for by you hitting Return.');
  ReadLn;

  WriteLn('Okay, now you have asked for the connection, and it will be made');
  DatabaseConnectionManagerLazy.ConnectToDatabase;
  WriteLn;

  WriteLn('All Done');
  WriteLn;
end;
Bunun demosunu yapmak biraz zor, bu yüzden adım adım ilerleyeceğiz.

  • İlk olarak, her şey kaydedilir: IDatabaseConnector arayüzünü uygulayan TDatabaseConnector sınıfı ve arayüzü kullanan iki sınıf, biri "düzenli olarak" ve diğeri "tembel".
  • Bu gerçekleştiğinde, uygulama her şeyin kaydedildiğini bildirir ve ReadLn'i çağırır, böylece her şeyin kayıtlı olduğunu ve kullanım için beklediğini görebilirsiniz.
  • Daha sonra Enter tuşuna bastığınızda size bir TDatabaseConnectionManager oluşturmak üzere olduğunu söyler ve bunu yapar.
  • Bu noktada bağlantının kurulduğunu görmelisiniz. Gecikme olmaz; bağlantı siz herhangi bir şey yapmadan önce bile doğru şekilde kurulur. Bunun nedeni, Container'ın Build çağrısı üzerine her şeyi otomatik olarak oluşturmasıdır. Bağlantıda bir gecikmeyi simüle ettiğimiz için bağlantının kurulmasının üç saniye sürdüğünü unutmayın.
  • Sırada TDatabaseConnectionManagerLazy var. Bu sınıf, veritabanı bağlantısını Tembel olarak bildirir ve bu nedenle siz özellikle talep edene kadar oluşturulmaz. Bunun bir örneğini Container oluşturup bağlanmadan Container aracılığıyla çözebiliriz. Yalnızca ConnectToDatabase'i gerçekten çağırdığımızda ve ilk olarak IDatabaseConnector'ı istediğimizde bağlanır. Return tuşuna bastığınızda bunu çalışırken görebilirsiniz ve siz Return tuşuna ikinci kez basana kadar hiçbir şey olmaz.
  • Uygulama tüm bunlar olurken size rapor ediyor, bu nedenle çıktıyı dikkatlice okursanız ne olduğunu anında görebilirsiniz. Bağlantının kurulması için üç saniye beklemeniz gerektiğini unutmayın. İsterseniz bağlantının özellikle talep edilene kadar yapılmadığından emin olmak için çok uzun bir süre bekleyebilirsiniz.
  • Her iki durumda da bağlantının ne zaman kurulduğunu anlayabilirsiniz çünkü uygulama bittiğinde konsola "All Done-Hepsi Yapıldı" yazar.

İki bağlantı yöneticisi sınıfı arasında yalnızca iki temel fark vardır. İlki - TDatabaseConnectionManager - özel bir şey yapmaz - yalnızca daha sonra kullanmak üzere IDatabaseConnector referansını saklar. Farklılıkları olan ikinci sınıf olan TDatabaseConnectionManagerLazy'dir. İlk olarak, IDatabaseConnector'a olan referansını Lazy olarak bildirir ve bunun yalnızca kodda ilk referans verildiğinde oluşturulmasını sağlar. İkincisi, ConnectToDatabase yönteminde gerçek referanslamayı yapar. (Lazy kaydının, tembel olarak oluşturulan örneğe gerçek referansı içeren Value adında bir Property'ye sahip olduğunu unutmayın.) Bu noktada uygulama atanır ve sınıf gerçekten başlatılır.

Fabrikaların Kaydedilmesi

Tamam, şu ana kadar her şey yolunda. Ancak muhtemelen rahatsız edici bir gerçeği fark etmişsinizdir: Tüm bağımlılıklarımız kayıtlı arayüzlerdir. Ya yapılandırıcımızın dizelere (string), tam sayılara ya da diğer ilkel türlere ihtiyacı varsa?

Konteynerin bununla başa çıkmanın bir yolu var. Konteyner'e kayıtlı olmayan yapıcı parametreleriyle sınıfın nasıl oluşturulacağını anlatmak için bir "fabrika" kaydetmenizi sağlar.

Kahve Makinesi Uygulaması

Bir kahve makinesi için hangi kahvenin yapılacağını ve kahvenin ne kadar sürede demleneceğini yöneten bir arayüz hayal etmeye ne dersiniz? Ve elbette, o kahve makinesinin nasıl kahve yapılacağını bilmesi gerekiyor, dolayısıyla aşağıdaki arayüz:

type
  ICoffeeMaker = interface
    ['{73436E03-EF65-44F5-9606-F706156CBEB5}']
    procedure MakeCoffee;
  end;
Ancak dediğimiz gibi uygulamanın bir kahve türüne ve bir demleme süresine ihtiyacı var, dolayısıyla şunu anlıyoruz:

type
  ICoffeeMaker = interface
    ['{73436E03-EF65-44F5-9606-F706156CBEB5}']
    procedure MakeCoffee;
  end;

type
  TCoffeeMaker = class(TInterfacedObject, ICoffeeMaker)
  private
    FCoffeeBrand: string;
    FBrewingMinutes: integer;
  public
    constructor Create(const aCoffeeBrand: string; const aBrewingMinutes: integer);
    procedure MakeCoffee;
  end;

constructor TCoffeeMaker.Create(const aCoffeeBrand: string; const aBrewingMinutes: integer);
begin
  inherited Create;
  FCoffeeBrand := aCoffeeBrand;
  FBrewingMinutes := aBrewingMinutes;
end;

procedure TCoffeeMaker.MakeCoffee;
begin
  WriteLn('Pour hot water over the ', FCoffeeBrand, ' so that it brews for ', FBrewingMinutes, ' minutes.');
end;
Şimdi bu sınıfın, mevcut haliyle Container'a yerleştirilmesine gerek yok, ancak örneğin TCoffeeMaker'ı bağımlılık olarak alacak bir TKitchen sınıfı kavramını kolayca hayal edebiliriz ve bu nedenle sınıfın kaydedilmesi gerekir. Düzgün bir şekilde çözülmesi için konteynerle birlikte. Ancak yine de yapıcı bir dize ve bir tamsayı alır. Ne yapalım?

Şimdi bu sınıfın, mevcut haliyle, Konteyner'a konulmasına gerek yoktur, ancak şunu kolayca hayal edebiliriz: TCoffeeMaker'ı bağımlılık olarak alacak bir TKitchen sınıfı ve dolayısıyla sınıfın düzgün bir şekilde çözümlenmesi için konteynere kaydedilmesi gerekir. Ancak yine de yapılandırıcı bir string (dize) ve bir tamsayı alır. Ne yapalım?

Konteynere kahve makinesini nasıl oluşturacağını anlatmak için bir fabrikayı (factory) kaydedebiliriz. Bu tür fabrika anonim bir yöntem biçimini alır:

type
  {$M+}
  TCoffeeMakerFactory = reference to function(const aCoffeeBrand: string; const aBrewingMinutes: integer): ICoffeeMaker;
  {$M-}
Anonim bir fonksiyonu fabrika (factory) olarak düşünmek kolaydır. Bu, belirli bir şeyi, ICoffeeMaker'ın uygulamasını (implementation) - burada TCoffeeMaker - yaratmaya yönelik bir "taslak"tır, "plan"dır. Ayrıca planın imzasının uygulayıcı sınıfımızın yapılandırıcısının imzasıyla eşleştiğine de dikkat edin. Tahmin edebileceğiniz gibi bu bir tesadüf değil. Anonim yöntemin Konteyner söz konusu olduğunda bir fabrika olması için {$METHODINFO} açık olmalıdır. Anonim işlevi bildirdikten sonra onu Container'a fabrika olarak kaydedebiliriz:
procedure RegisterStuff(aContainer: TContainer);
begin
  aContainer.RegisterType<ICoffeeMaker, TCoffeeMaker>.AsDefault;
  aContainer.RegisterFactory<TCoffeeMakerFactory>;
  aContainer.Build;
end;
Öncelikle TCoffeeMaker'ın ICoffeeMaker arayüzüne normal kaydını yapıyoruz. Daha sonra TCoffeeMakerFactory anonim fonksiyonunu Container'ın RegisterFactory metoduna aktarıyoruz. Kap, Fabrikadaki anonim işlevin sonuç türüne bakacak ve hangi arayüzü çözmesi gerektiğini anlayacak kadar akıllıdır. Artık ihtiyacımız olan bilgiyi alabilir ve fabrikayı doğru sınıfı oluşturacak şekilde çözebiliriz:
procedure Main(const aContainer : TContainer);
var
  CoffeeName: string;
  BrewingMinutes: integer;
  CoffeeMakerFactory: TCoffeeMakerFactory;
  CoffeeMaker: ICoffeeMaker;
begin
  Write('What kind of coffee do you want to make? ');
  ReadLn(CoffeeName);
  Write('How many minutes? ');
  ReadLn(BrewingMinutes);
  CoffeeMakerFactory := aContainer.Resolve<TCoffeeMakerFactory>();
  CoffeeMaker := CoffeeMakerFactory(CoffeeName, BrewingMinutes);
  CoffeeMaker.MakeCoffee;
end;

Bu kod biraz ilginç olduğundan aşağıdakilere dikkat etmelisiniz:

  • CoffeeMakerFactory değişkeni TCoffeeMakerFactory türündedir veya başka bir deyişle fabrika olan anonim fonksiyona bir referanstır. Bu kod satırı "Bana bir kahve makinesinin fabrikasını bulun" diyor. Birden fazla uygulamanız varsa bunları kaydedebilir ve adlarına göre alabilirsiniz.
  • Resolve çağrısı parametreli bir tür olan TCoffeeMakerFactory'yi alır. Beklediğiniz gibi ICoffeeMaker arayüzünü almadığına dikkat edin. Ayrıca derleyiciye bir nesne prosedürünü değil, anonim işlevi döndürdüğünü bildirmek için parantezleri kullanır.
  • Bir kere anonim fonksiyonun referansını aldığımızda, onu gerekli parametrelerle çağırabiliriz. ICoffeeMaker adında bir arayüz döndürecektir. Bu biraz Konteyner Sihiri (MÖ'nün notu : Konteyner, bizim için fabrika fonksiyonunu oluşturuyor, yani bunu ayrıca o arayüzün her nesnesi için ayrı ayrı yazmamıza gerek kalmıyor). Container temel olarak fabrika referansına bakar ve fabrikanın yöntem imzasıyla eşleşen bir kurucuya sahip fabrika fonksiyonunun sonucunun kayıtlı bir uygulamasını bulur. Daha sonra iletilen parametrelerle bu sınıfı oluşturur ve başlatılan uygulamaya bir arayüz döndürür.
  • Buradan arayüzün MakeCoffee metodunu çağırabilir ve beklediğiniz sonuçları alabilirsiniz.

Bu arada, eğer gerçekten maceraperest hissediyorsanız, tüm bunları tek bir kod satırıyla yapabilirsiniz:

//CoffeeMakerFactory := aContainer.Resolve<TCoffeeMakerFactory>();
//CoffeeMaker := CoffeeMakerFactory(CoffeeName, BrewingMinutes);
aContainer.Resolve<TCoffeeMakerFactory>()(CoffeeName, BrewingMinutes) .MakeCoffee;
...
Artık, türü ne olursa olsun, yapıcı parametreleriyle çözülebilecek, Container'a kayıtlı bir sınıfımız var. Güzel. Ama şimdi düşünüyorsunuz (bunu biliyorum çünkü gerçekten akıllısınız ve elbette bunu düşünüyorsunuz) "Peki ya ICoffeeMaker'ın iki uygulaması varsa ve bunlar farklı yapıcı parametre listelerine sahipse?" Bu bir sorun değil. Sadece biraz daha çalışmanız gerekiyor. İlk olarak, burada farklı bir yapıcı parametre listesine sahip farklı bir uygulama var:
type
  TCupCoffeeMaker = class(TInterfacedObject, ICoffeeMaker)
  strict private
    FCupType: string;
  public
    constructor Create(const aCupType: string);
    procedure MakeCoffee;
  end;

constructor TCupCoffeeMaker.Create(const aCupType: string);
begin
  inherited Create;
  FCupType := aCupType;
end;
procedure TCupCoffeeMaker.MakeCoffee;
begin
  WriteLn('Put the ' + FCupType + ' cup in the coffee maker and press the "Brew" button');
end;

Bu, yalnızca yapılandırıcıdaki fincan tipine ihtiyaç duyan "fincan" tarzı bir kahve makinesidir, işte fabrika beyanlarımız şu anda şöyle görünüyor:

procedure RegisterStuff(aContainer: TContainer);
begin
  aContainer.RegisterType<ICoffeeMaker, TCoffeeMaker>('regular');
  aContainer.RegisterType<ICoffeeMaker, TCupCoffeeMaker>('cup');
  aContainer.RegisterFactory<TCoffeeMakerFactory>.AsFactory('regular');
  aContainer.RegisterFactory<TCupCoffeeMakerFactory>.AsFactory('cup');
  aContainer.Build;
end;
ve işte hepsini böyle çağırıyoruz:
procedure Main(const aContainer : TContainer);
var
  CupName: string;
  CupCoffeeMakerFactory: TCupCoffeeMakerFactory;
  CoffeeMaker: ICoffeeMaker;
begin
  Write('What kind of cup do you want to make?');
  ReadLn(CupName);
  CupCoffeeMakerFactory := aContainer.Resolve<TCupCoffeeMakerFactory>();
  CoffeeMaker := CupCoffeeMakerFactory(CupName);
  CoffeeMaker.MakeCoffee;
end;
Her zamanki gibi dikkat edilmesi gerekenlerin bir listesini burada bulabilirsiniz:

  • İlk olarak, ICoffeeMaker uygulaması olan birden fazla sınıf olduğundan RegisterType çağrılarının isimleri vardır.
  • Daha sonra, RegisterFactory çağrısının, o fabrikanın uygulamasının adını parametre olarak alan AsFactory çağrısına sahip olduğuna dikkat edin. Bu, Container'ın hangi uygulamanın hangi fabrikayla ilişkilendirileceğini bilmesi için gereklidir.
  • Son olarak kodun geri kalanı beklediğiniz gibidir. Fabrikayı bir referansa alırsınız (ki bu elbette anonim bir metotdur) ve daha sonra istediğiniz bardak tipini ileterek onu çağırırsınız.


Basit Parametreleri Kaydetme

Önceki bölümde, yapılandırıcılarında isteğe göre parametreleri olan nesneler oluşturmak için anonim bir fonksiyonu fabrika olarak nasıl kaydedebileceğinizi gördük. Bu bölümde yapılandırıcının kendisindeki öznitelikleri (attributes) kullanarak bunu yapmanın daha doğrudan bir yolunu inceleyeceğiz. Aslında, basit türleri ada göre kaydedebilir ve ardından bunları nitelikleri ve kapsayıcıyı kullanarak çözebiliriz. Bunun nasıl çalıştığına bir göz atalım.

İşte basit bir kişi sınıfı:

uses
  System.SysUtils
 ,Spring.Container.Common
 ,Spring.Container;

type
  TPerson = class
  private
    FOccupation: string;
    FName: string;
    FAge: integer;
    function GetName: string;
    function GetAge: integer;
    function GetOccupation: string;
  public
    constructor Create([Inject('name')]aName: string;
                       [Inject('age')]aAge: integer;
                       [Inject('occupation')]aOccupation: string);
    property Name: string read GetName;
    property Age: integer read GetAge;
    property Occupation: string read GetOccupation;
  end;

Yapılandırıcı parametrelerinin her birinin [inject] özniteliğiyle etiketlenmesi dışında dikkate değer hiç bir şey yok. Onları konteynere tanımlayacak bir parametre olarak eklenmiş adları vardır. Bu öznitelikler, konteynerdeki ilkel veri tiplerinin kayıtlarına işaret edecektir. Bu kayıtlar şöyle görünür:

procedure Main(aContainer: TContainer);
var
  TempName: TPerson;
begin
  Randomize;
  aContainer.RegisterType<TPerson>;
  aContainer.RegisterType<string>('name').DelegateTo(
                function: string
                begin
                  Result := GetLocalUsername;
                end
                ).AsDefault;
  aContainer.RegisterType<string>('occupation').DelegateTo(
                function: string
                begin
                  Result := 'plumber'; 
                end
                );
  aContainer.RegisterType<integer>('age').DelegateTo(
                function: integer
                begin
                  Result := Random(100);
                end
                );
  aContainer.Build;

  TempName := aContainer.Resolve<TPerson>;
  WriteLn(TempName.Name, ' is ', TempName.Age, ' years old', ' and is a ', TempName.Occupation);
  ReadLn;
end;

Burada bilinmedik hiçbir şey yok; muhtemelen neler olduğunu anlayabilirsiniz. Kaydedilen türler ilkeldir (iki dize (string) ve bir tamsayı) ve özniteliklerde (attribute) gördüğümüz dize (string) değerlerinin aynısı kullanılarak kaydedilirler. Daha sonra konteynere dizeleri (string'leri) ve tamsayıyı nasıl elde edeceğini söyleyen anonim bir fonksiyonu tanımlamak için DelegateTo'yu çağırırız. (Ad değerini almak için önceki bölümdeki GetLocalUserName çağrısını kullandığımızı unutmayın.)

Buradaki tüm kodlardan bu noktaya kadar olan fark, ilkel türleri kaydetmemizdir. Daha önce sınıfları kaydetmiştik, ancak burada keyfi ilkel türleri kaydediyoruz. Kayıt için bir ad ve konteynere ilkelin değerinin ne olması gerektiğini söyleyen anonim bir metot sağladığınız sürece istediğiniz herhangi bir türü kaydedebilirsiniz. Gösterildiği gibi, daha sonra parametreleri etiketlersiniz (eğer isterseniz bir metottaki  parametreler olabilirler) ve değerler bizim için konteyner tarafından otomatik olarak ayarlanır. Artık bir sınıfı konteynerin içinde tamamen çözülebilir hale getirmenin başka bir yolu daha oldu.

Öznitellikler (Attributes)

Bu bölüme Delphi'de niteliklerin nasıl çalıştığını bildiğinizi varsayarak başlıyorum. (Değilse, "Delphi'de Kodlama - Coding In Delphi" kitabımda bunlarla ilgili her şeyi okumaktan çekinmeyin.) Spring for Delphi framework, kayıt çağrıları yerine kullanılabilecek bir dizi özellik sağlar.

[Inject] Attribute

Spring4D framework'ünde InjectAttribute öznitelik sınıfı vardır-tanımlanmıştır. Bu elbette [Inject] öznitelik olarak kullanılır (MÖ'nün notu: Devamında gelen Attribute kısmı yazılmaz, Delphi kendisi bunu ekler. Bu özniteliklerin temel yazım şeklidir). Öznitelikler yapılandırıcılar (constructors), yöntemler (methods), özellikler (property-properties) ve parametreler de dahil olmak üzere bir sınıftaki istediğiniz sayıda dil öğesini etiketlemenize olanak tanır.(Ayrıca alanları onunla etiketlemenize de olanak tanır, ancak bir sonraki bölümde göreceğimiz gibi Field Injection bir anti-kalıptır.)

Bir üyenin bu şekilde etiketlenmesi, Injectxxxx yöntemlerinin sınıf/arayüz kayıt çağrılarına eklenmesine eşdeğerdir. Bu nedenle, [Inject] niteliğinin amacı, belirli bir sınıf üyesini türüne göre konteynere enjekte edilecek şekilde kaydetmektir.

Aşağıdaki kodu göz inceleyelim:

unit uAttribute;

interface

uses
      Spring
    , Spring.Container
    , Spring.Container.Common
    ;

type
  IHorse = interface
    ['{BDBB0FE7-D369-4EC7-B2A0-FC012136B87E}']
    procedure Ride;
  end;

  ICowboy = interface
  ['{337002BB-2219-46A2-BF28-5CFD8A6873AD}']
    procedure SetHorse(aValue: IHorse);
    function GetHorse: IHorse;
    procedure DoCowboyStuff;
    property Horse: IHorse read GetHorse write SetHorse;
  end;

  THorse = class(TInterfacedObject, IHorse)
  public
    procedure Ride;
  end;

  TCowboy = class(TInterfacedObject, ICowboy)
  private
    FHorse: IHorse;
    procedure SetHorse(aValue: IHorse);
    function GetHorse: IHorse;
  public
    procedure DoCowboyStuff;
    property Horse: IHorse read GetHorse write SetHorse;
  end;

procedure BeACowboy(aContainer: TContainer);

implementation

procedure BeACowboy(aContainer: TContainer);
var
  Cowboy: ICowboy;
begin
  Cowboy := aContainer.Resolve<ICowboy>;
  Cowboy.DoCowboyStuff;
end;

procedure TCowboy.DoCowboyStuff;
begin
  WriteLn('Yippee Kay Yay!');
  Horse.Ride;
end;

function TCowboy.GetHorse: IHorse;
begin
  Result := FHorse;
end;

procedure TCowboy.SetHorse(aValue: IHorse);
begin
  FHorse := aValue;
end;
procedure THorse.Ride;
begin
  WriteLn('Gallop along the prairie!');
end;

end.

ve kaydetme kodu:

procedure RegisterStuff(aContainer: TContainer);
begin
  aContainer.RegisterType<ICowboy, TCowboy>.InjectProperty('Horse');
  aContainer.RegisterType<THorse, IHorse>;
  aContainer.Build;
end;
Bu kodda tanıdık gelmemesi gereken hiçbir şey yok. "İş", kayıt kodunda InjectProperty çağrısıyla yapılır. Bu, konteynere verilen kaydetme çağrısıyla çözülecek (resolve) 'Horse' adında bir property'nin olacağını söyler. Bu, sınıfınızın hiçbir yerinde Create'i asla çağırmamanıza ve bunun yerine konteynerin bu işi sizin için yapmasına izin vermenize olanak tanır.

Artık hemen hemen aynı şeyi aşağıdakilerle yapabilirsiniz:

  TCowboy = class(TInterfacedObject, ICowboy)
  private
    FHorse: IHorse;
    procedure SetHorse(aValue: IHorse);
    function GetHorse: IHorse;
  public
    procedure DoCowboyStuff;
    [Inject]
    property Horse: IHorse read GetHorse write SetHorse;
  end;

ve ardından kaydı aşağıdaki gibi basitleştirin:

procedure RegisterStuff(aContainer: TContainer);
begin
  aContainer.RegisterType<TCowboy, ICowboy>;
  aContainer.RegisterType<THorse, IHorse>;
  aContainer.Build;
end;
Ben de "hemen hemen" aynı şeyi söylüyorum çünkü özniteliği kullandığınızda, IHorse kaydıyla hangi özelliği (property) ilişkilendirmek istediğinizi tam olarak belirtirsiniz. InjectProperty kullanıyorsanız, Konteyner bunu IHorse türünde "Horse" adlı bir özelliğe (property'ye) sahip konteynere kayıtlı herhangi bir sınıftaki herhangi bir özellikle ilişkilendirecektir. Öte yandan öznitelik, ilişkilendirmeyi öznitelikle etiketlenen özellik ile sınırlar. Bu ince ama önemli bir farktır.

Ömür Boyu Öznitelikler

Daha önce konteynere, sizin için atadığı ve döndürdüğü örneklerin (instance: Yaratılmış, bellekte yer alan ve çalışan sınıf.) ömrünü yönetmesi için nasıl talimat verilebileceğini tartışmıştık. O zaman size gösterdiğim gibi bu, kayıt işlemi sırasında bir çağrıyla yapıldı:

Container.RegisterType.AsSingleton;

Bu kod, konteynere, bir TSword uygulamasını aldığında her zaman aynı TSword örneğini döndürmesini söyler. Ancak aynı şeyi [Singleton] özniteliğini (attribute) TSword sınıfına ekleyerek de yapabilirsiniz:

[Singleton]
TSword = class(TInterfacedObject, IWeapon)
  procedure Wield;
end;

Bu, Container'a IWeapon uygulamasının tekil olması gerektiğini söyleyecektir; yani Container bir IWeapon örneği sağladığında her zaman tam olarak aynı örneği döndürmelidir.

Sonuç

Spring4D Dependency Injection konteyneri oldukça güçlü ve yeteneklidir. Gördüğümüz gibi arayüzleri ve uygulamaları eşleştirmekten daha fazlasını yapabilir.

Bağımlılık Enjeksiyonu Anti-Kalıpları (Anti-Patterns)

Şu ana kadar size Dependency Injection yapmak ve Dependency Injection Container'ı kullanmak için iyi modeller ve uygulamalar göstermeye çalıştım. Mutlaka yapılması gereken doğru yollar vardır. Ve tam tersi, kesinlikle bazı şeylerin yapılmaması gereken yollar da var. Bu bölümde, kullanmaya meyilli olabileceğiniz ancak kullanmamanız gereken, genellikle anti-kalıplar olarak adlandırılan bu yanlış yollardan bazıları ele alınmaktadır. Bu anti-kalıplar denenmiş ancak temiz, iyi, iyi yazılmış kod üretme konusunda yetersiz oldukları gösterilmiş olanlardır.

Servis Bulucu (Service Locator)

Kolayca en bilinen, en tartışmalı ve en çok suiistimal edilen anti-kalıp, ServiceLocator anti-kalıpıdır. Ben bunu iyi bilinen ve tartışmalı olarak nitelendiriyorum çünkü ServiceLocator uzun bir süre boyunca Bağımlılık Enjeksiyonu yapmak için yararlı bir model olarak kabul edildi. Ben buna istismar diyorum çünkü insanlar hala bunu kullanıyor ve bunun faydalı bir kalıp olduğuna inanıyorlar. Burada ServiceLocator'ın gerçekten bir anti-kalıp olduğunu ve bunu uygulamalarınızda kullanmamanız gerektiğini tartışacağım.

ServiceLocator caziptir. Oluşturmak için çağrılarınızı ServiceLocator.Resolve<IMyInterface> çağrısıyla değiştirmek son derece kolaydır. Bir arayüz uygulamasına ihtiyaç duyduğunuzda ServiceLocator'ı kullanmak gerçekten doğaldır.

ServiceLocator caziptir. Oluşturmak için çağrılarınızı ServiceLocator.Resolve<IMyInterface> çağrısıyla değiştirmek son derece kolaydır. Bir arayüz uygulamasına ihtiyaç duyduğunuzda ServiceLocator'ı kullanmak gerçekten doğaldır.

Ancak gördüğümüz gibi ServiceLocator'a gerek yok. Container, ServiceLocator'ı kullanmayı düşündüğünüz işlerin %99'unu yapabilir. Container, başka bir kayıtlı sınıfın sahip olabileceği herhangi bir kayıtlı bağımlılığı otomatik olarak çözebildiğinden, ServiceLocator'ı uygulamanızın kökünde yalnızca bir kez kullanabilirsiniz. Container, uygulama başlamadan önce tüm nesne grafiğinizi tamamen bağlayabilir. Uygulamanızın kökündeki tek bir çözüm çağrısına ihtiyacınız var, ancak bu, Container'ın getirdiği büyük faydanın karşılığında ödenmesi gereken küçük bir bedel haline geliyor.

ServiceLocator'dan kaçınılmasını gerektiren birkaç sebep daha var:

  • ServiceLocator tekildir ve tekil değişkenler global(küresel) değişkenlerdir. Ne pahasına olursa olsun global değişkenlerden kaçınılmalıdır.
  • ServiceLocator'ı Create çağrılarınızın yerine kullanırsanız, aslında Container'ın büyük bir global değişken kümesinden başka bir şey olmamasına neden olursunuz. Uygulamanızın herhangi bir yerinden herhangi bir sınıf örneğini alabiliyorsanız Container'ı bu şekilde kullanıyorsunuz demektir ve söylediğim gibi global değişkenlerden kaçınılmalıdır.
  • Bağımlılıkları parametre geçmek yerine ServiceLocator'ı kullanmak bu bağımlılıkları gizler. Constructor Injection'ın temel amaçlarından biri bir sınıfın bağımlılıklarını açıkça ilan etmektir. Bağımlılıkları bir sınıfa parametre geçmek yerine ServiceLocator'ı kullanırsanız, yapılandırıcı enjeksiyonunun avantajlarını altüst edersiniz.
  • ServiceLocator'ı kullanarak derleme zamanı hatası yerine çalışma zamanı hatası yaratırsınız ve derleme zamanı hataları büyük ölçüde tercih edilir. Constructor Injection'ın yanlış kullanılması derleyici hatasına neden olur. ServiceLocator'ın yanlış kullanılması çalışma zamanı hatasına neden olur. İlkini tercih etmelisiniz.
  • ServiceLocator'ı kullandığınızda Konteynerin şekillendirilebilirliğinden yararlanamazsınız. Konteyner, nesne grafiğinizi doğru şekilde oluşturacak şekilde ayarlanabilir. Hatta farklı sebeplerden dolayı farklı kompozisyonlar sağlayacak şekilde bile kurulabilir. Ancak, ServiceLocator ile Container'dan rastgele uygulamaları kaparsanız, nesnelerinizi doğru bir şekilde oluşturmak için Container'ı kullanmanın tüm avantajlarını kaybedersiniz.

Sonuç olarak, ServiceLocator bir anti-model olarak görülmeli ve bundan kaçınılmalıdır. Bunun yerine, tüm nesnelerinizi çözümlemek için kapsayıcıya güvenin ve uygulamanızın bileşik kökünde tek bir Çözümleme çağrısı yapın. Bunu kullanmak, yüzeysel bakıldığında, çok cazip çünkü çok kullanışlı bir teknik gibi görünüyor, ancak gördüğümüz gibi, bu çekicilik aptalın altınıdır ve aslında belaya yol açar.

Field (Alan) Enjeksiyonu

Field (Alan) Enjeksiyonu Nedir

Field (Alan) Enjeksiyonu, bir sınıfa alan değeri vererek bağımlılık enjekte ettiğiniz bir Bağımlılık Enjeksiyonu türüdür. İşte basit, açıklayıcı bir örnek:

unit uFieldInjection;

interface
uses
  Spring.Container
  , Spring.Container.Common
  ;
type
  IBrake = interface
    ['{74DBE39C-F52F-42C4-B7CB-8009F7EDF1E1}']
    procedure StopVehicle;
  end;

  IEngine = interface
    ['{0BC34CC8-DE81-4073-9CA4-A160CFB9A64A}']
    procedure PropelVehicle;
  end;

  ICar = interface
    ['{B2C1C9FB-E388-4F0B-9197-2BCAB5A2A396}']
    procedure Drive;
  end;

  TBrakes = class(TInterfacedObject, IBrake)
    procedure StopVehicle;
  end;

  TEngine = class(TInterfacedObject, IEngine)
    procedure PropelVehicle;
  end;

  TCar = class(TInterfacedObject, ICar)
  private
    FBrakes: IBrake;
    [Inject]
    FEngine: IEngine;
  public
    procedure Drive;
  end;

procedure MakeCarGo(aContainer: TContainer);

implementation

procedure MakeCarGo(aContainer: TContainer);
var
  Car: ICar;
begin
  Car := aContainer.Resolve<ICar>;
  Car.Drive;
end;

procedure TBrakes.StopVehicle;
begin
  WriteLn('Step on the brake pedal and make the car stop');
end;

procedure TEngine.PropelVehicle;
begin
  WriteLn('Burn gas and make the car go');
end;

procedure TCar.Drive;
begin
  FEngine.PropelVehicle;
  FBrakes.StopVehicle;
end;
ve kaydetme kodları:
procedure RegisterStuff(aContainer: TContainer);
begin
  aContainer.RegisterType<TBrakes, IBrake>;
  aContainer.RegisterType<TEngine, IEngine>;
  aContainer.RegisterType<ICar, TCar>.InjectField('FBrakes');
end;
Yukarıdaki kodla ilgili dikkat edilmesi gereken bazı noktalar şunlardır:

  • TCar FBrakes ve FEngine olmak üzere iki alan için Field Enjeksiyonunu kullanır. TCar'ı ICar arayüzünü uygulayacak şekilde kaydederken, FBrakes'i oluşturma sırasında enjekte edilecek bir alan olarak kaydeden bir InjectField çağrısı ekler. FEngine alanını kaydetmek için [Inject] özniteliğini kullanır. Her iki teknik de tamamen aynıdır ve aynı şeyi yapar.
  • TCar konteynere kayıtlı olduğundan ve alan türleri için kayıtlı sınıflar bulunduğundan, her şey otomatik olarak "bağlanır" ve MakeCarGo'ya yapılan bir çağrı beklendiği gibi davranır.

Alan (Field) Enjeksiyonu Neden Kötü Bir Fikirdir?

Field Injection çok cazip olabilir. Daha az kod gerektirdiğinden yapılandırıcı enjeksiyonundan "daha temiz" görünebilir. Kolay görünebilir çünkü tek gereken tek bir özniteliktir. Ancak aldanmayın; Field Injection bir anti-modeldir. İşte nedeni:

  • Özel bir üyeye erişime izin vererek kapsüllemeyi ihlal eder. Kapsülleme ihlali, nesne yönelimli programlama dünyasında büyük bir hayır-hayırdır. Böylece "private" alandaki üyeleri bozmuş olur. Alanların özel olmasının bir nedeni vardır ve harici bir varlık tarafından değil, sınıf tarafından dahili olarak yönetilmelidir. Alan değerleri normalde bir constructor veya setter metoduyla atanır. Sınıfın dışından bir değere dayalı olarak alan değeri atamamalısınız. Yukarıdaki örneğimizde, iki alan özeldir (private) ancak değerlerini, onlara erişmesine izin verilen konteynerden alırlar. Bu iyi değildir!
  • Field Injection kapsüllemeyi ihlal etmesinin yanı sıra bir sınıfın bağımlılıklarını da gizler. Bir sınıfın yalnızca genel arayüzünü biliyor veya görüntülüyorsanız, alanlar özel bölümlerde gizlendiğinden, bir sınıfın sahip olduğu tüm bağımlılıkları göremezsiniz. Bağımlılığın orada olduğunu bile bilmiyor olabilirsiniz. Yapıcı ve özellik enjeksiyonu ile bağımlılıklar görülecek ve bilinecek şekilde oradadır. Field Injection ile sınıfın kullanıcısı bağımlılığı asla göremeyebilir ve hatta bağımlılığın kullanım için mevcut olduğundan emin olmadan sınıfı kullanmayı deneyebilir. Bu, gerçekleşmeyi bekleyen bir erişim ihlalidir. TCar'ın açık (public) arayüzüne bakıldığında hiçbir bağımlılık görülmez. Yapılandırıcı hiçbir parametre almaz ve ayarlanabilecek hiçbir özellik yoktur. Sahip olduğunuz tek şey TCar'ın açık (public) arayüzü olsaydı, TCar'ın gerçekte neye ihtiyaç duyduğu ve ne yaptığı konusunda büyük bir mutlulukla cahil kalırdınız.
  • Ayrıca, özel bağımlılığı olan bir sınıfı nasıl test edersiniz? Bu bağımlılık nasıl taklit edilir? Gerçekten yapamazsınız. Alan enjeksiyonunu kullandığınızda, temelde test edilemeyen bir sınıf yazıyorsunuz ve bu aptalca bir fikir gibi görünüyor.
  • Field Injection döngüsel bağımlılıklara izin verir. Bir dış varlıktan alan değeri iletirseniz referansın döngüsel bir bağımlılık oluşturmadığından emin olamazsınız. Constructor Injection, döngüsel bir bağımlılık oluşturmak için dahili referansların "kaçmasına" asla izin vermeyerek bunu önler.
  • Son olarak Field Injection, özellikle de bir sınıftaki birçok alan için kullanıyorsanız, bir sınıfın karmaşıklığını gizleyebilir ve sınıfın gerçekte olduğundan daha basit olduğunu düşünmenizi sağlayabilir. Sonuçta başka bir alan nedir? Ancak bağımlılıklarınızı kurucu aracılığıyla enjekte ederseniz, bunlar kısa sürede birikebilir ve Tek Sorumluluk İlkesini ihlal ettiğiniz açıkça ortaya çıkabilir. IRadio, ITransmission, ISeats ve bir sürü başka alanı TCar'a eklemek gerçekten çok kolay olurdu, üstelik sınıfları kaydetmek dışında fazla bir şey yapmanıza gerek kalmadan. Yapıcı sade kalacak ve her şey yolunda görünecek. Ama ne yazık ki öyle değil. Bir araba tasarlıyorsanız, TCar bildirimini çok yüksek seviyede tutmak için oldukça ciddi bir sınıf çerçevesini kırmak isteyeceksiniz; bağımlılıklar daha düşük bağımlılıklara doğru kademeli olarak inecek ve bu böyle devam edecek. Arabanızın yapıcısında yüzlerce parametre olsaydı, bu hantal olurdu. Bu iyi bir fikir değil ama yüz alana sahip olmak daha az acı verici olabilir. Ancak elbette bu alanların her biri gizlidir ve sınıfın test edilmesini zorlaştırır, bu nedenle enjekte edilen alanlardan kaçınmalısınız.

Özetle: Yapılandırıcı (Constructor) Enjeksiyonunu ve Özellik (Property) Enjeksiyonunu kullanın ve Alan (Field) Enjeksiyonunu kullanmayın.

Yapılandırıcı Aşırı Enjeksiyonu

Yukarıda Field Injection'dan bahsederken buna biraz değinmiştim. Yapıcı OverInjection, referansları sonraki her sınıfın yapıcısındaki bileşik köke doğru geriye doğru aktarmaya devam etme eğilimidir ve bu yapıcıların şişmesine ve çok fazla parametre almasına neden olur. ClassA'nın ClassB'ye, onun da ClassC'ye ve ClassD'ye bağlı olduğunu hayal edin. Şuna benzeyen bir kurucu elde edebilirsiniz:

constructor TClassG.Create(aClassA: TClassA; aClassB: TClassB; aClassC: TClassC; aClassD: TClassD; aCl\
assE: TClassE; aClassF: TClassF);

Bu kesinlikle arzu edilen bir durum değildir, ancak Constructor Injection kullanımında dikkatli olunması durumunda bunun nasıl olabileceği anlaşılabilir. Kendinizi bunu yaparken bulursanız, bir adım geri atmanın ve sınıf hiyerarşinizin sorunları olduğunu fark etmenin zamanı gelmiştir; bunların başlıcası muhtemelen sınıfınızın Tek Sorumluluk İlkesini takip etmemesidir. Bu kadar çok bağımlılığı alan bir sınıf, bu bağımlılıkları yalnızca diğer sınıflara aktarıyor olsa bile, muhtemelen çok fazla şey yapmaya çalışıyordur. Sınıflarınızı yalnızca tek bir şeyi yapan daha küçük, daha odaklı sınıflara bölmeye çalışın. Bu, Yapılandırma Aşırı Enjeksiyonunu önleyecektir.

Yapılandırıcı Aşırı Enjeksiyonu daha incelikli bir şekilde de meydana gelebilir. Aşağıdaki kodu göz önünde bulundurun:

unit uOverInjection;

interface

type
  IBankingService = interface
  ['{E367001A-94D1-4694-A0B9-FB0B3FD822ED}']
    procedure DoBankingStuff;
  end;

  IMailingService = interface
  ['{ED17BB98-CC8C-4DBF-9466-C20F6BBC4AE2}']
    procedure MailPayrollInfo;
  end;

  TEmployee = class(TObject)
  private
    FLastName: string;
    FFirstName: string;
    FWantsMail: Boolean;
  public
    property WantsMail: Boolean read FWantsMail write FWantsMail;
    property FirstName: string read FFirstName write FFirstName;
    property LastName: string read FLastName write FLastName;
  end;

  TMailingService = class(TInterfacedObject, IMailingService)
    procedure MailPayrollInfo;
  end;

  TBankingService = class(TInterfacedObject, IBankingService)
    procedure DoBankingStuff;
  end;

  TPayrollSystem = class
  private
    FBankingService: IBankingService;
    FMailingService: IMailingService;
  public
    constructor Create(aBankingService: IBankingService; aMailingService: IMailingService);
    procedure DoPayroll(aEmployee: TEmployee);
  end;

implementation

constructor TPayrollSystem.Create(aBankingService: IBankingService; aMailingService: IMailingService);
begin
  FBankingService := aBankingService;
  FMailingService := aMailingService;
end;

procedure TPayrollSystem.DoPayroll(aEmployee: TEmployee);
begin
  WriteLn('Doing Payroll');
  FBankingService.DoBankingStuff;
  if aEmployee.WantsMail then
  begin
    FMailingService.MailPayrollInfo;
  end;
end;

procedure TBankingService.DoBankingStuff;
begin
  WriteLn('Doing banking stuff');
end;

procedure TMailingService.MailPayrollInfo;
begin
  WriteLn('Mail out payroll information');
end;

end.
Bu harika görünüyor. Ancak iletilen iki bağımlılık olmasına rağmen bu bağımlılıklardan birinin (IMailingService) her zaman kullanılmadığına dikkat edin. Buna karşın IMailingService mutlaka gereklidir. TMailingServices sınıfı “açgözlü” davranıyor ve her zaman ihtiyaç duyduğundan fazlasını istiyor. Bir sınıfın yapılandırıcısı yalnızca gerçekten ihtiyaç duyduğu bağımlılıkları istemeli, yalnızca ihtiyaç duyduğu bağımlılıkları istememelidir. Bu türdeki Yapıcı Aşırı Enjeksiyonu, Kod Kokusu olarak değerlendirilmelidir. Bunun yerine, mektup hizmetinin bir örneğini (instance) almak için Fabrika gibi başka bir yöntem kullanmayı düşünebilirsiniz.

Bunu aynı zamanda Tek Sorumluluk İlkesinin (basit ama açıklayıcı) bir ihlali olarak da kabul etmeliyiz. Yani TPayrollSystem sınıfı çok fazla şey yapıyor. Maaş bordrosu ve posta işleri yapıyor. Bunun yerine, muhtemelen sadece maaş bordrosu konusunda endişelenmeli ve ardından "gönderme" endişesini ortadan kaldıran bir bağımlılığa sahip olmalıdır. Başka bir deyişle, muhtemelen bir ISendPayrollInfo kavramı ve ardından çalışanın isteklerine dayalı olarak çeşitli uygulamalar (implementation-sınıf) (salyangoz posta, e-posta, elden teslim vb.) olmalıdır. TPayrollSystem gönderimin nasıl gerçekleştiğiyle ilgilenmemelidir.

Bu nedenle iki tür anti-pattern Constructor Aşırı Enjeksiyonu vardır. Birincisi, yapıcıda çok fazla bağımlılık parametresi geçiyor (sayı üçe ulaştığında sinirlenmeye başlıyorum…). Bu, Tek Sorumluluk İlkesi ışığında sınıf tasarımının yeniden değerlendirilmesine neden olmalıdır. İkincisi, doğrudan sınıf tarafından kullanılmayan bağımlılıkların veya her zaman ihtiyaç duyulmayan bağımlılıkların aktarılmasıdır. Bu da bir Kod Kokusu olarak değerlendirilmeli ve yeniden düzenlenmelidir.

Konteynerdeki VCL Bileşenleri

Daha önce Enjekte Edilebilirler ve Yaratılabilirler kavramını tartışmıştım. Bazı sınıflar (Yaratılabilirler) manuel olarak oluşturulmalı ve konteynere yerleştirilmemelidir. WI, RTL sınıfları (TSringList, TList, TStream, vb.) gibi belirli sınıfların nasıl Oluşturulabilir olduğundan ve Container'a kaydedilmemesi gerektiğinden bahsettim. Konteynerde hiçbir zaman kaydedilmemesi gereken diğer bir sınıf grubu da TComponent'ten gelen herhangi bir VCL kontrolüdür. Bu özellikle TForm ve TDataModule için geçerlidir. Bunun birkaç nedeni var:

  • VCL kontrollerinin kendi ömür boyu yönetim sistemleri vardır ve bunları Container'a koymak, bileşenlerin kime ait olduğu ve bunların ne zaman imha edilmesi gerektiği konusunda hem geliştirici hem de Container açısından kafa karışıklığına neden olur.
  • Birçok VCL kontrolü görseldir ve bu nedenle görsel kapları (formlar, paneller, grup kutuları ve benzeri gibi) tarafından yönetilmeleri gerekir. Bu bileşenleri Dependency Injection Container'a koymak bu sahiplik ilişkilerini bozar. Dependency Injection Container ve görsel kontroller iyi karışmaz ve birlikte kullanılmamalıdır.
  • Zor. İşe yaraması için bile birçok çemberin üzerinden atlamak zorundasınız. İşleri basit tutun ve uygulamanızın çalışmasını sağlamak için kapsayıcıyı iş dersleri ve kendi yazdığınız diğer nesneler için ayırın.

Çoklu Yapılandırıcılar

Bağımlılık Enjeksiyonu ile ilgili bir sınıfın yalnızca bir kesin yapılandırıcıya sahip olması gerekir. Bu yapılandırıcı, sınıfın gerektirdiği tüm bağımlılıkları beyan etmelidir. Bir yapılandırıcının parametrelerinin, bir sınıfın onsuz yaşayamayacağı bağımlılıkların kesin listesi olması gerektiğini unutmayın. Bir sınıfın birden fazla kurucusu varsa, o zaman birden fazla gerekli bağımlılık kümesini bildiriyor demektir ve bu hiçbir anlam ifade etmez.

Birden fazla yapılandırıcı, Container'ın bir sınıfın nasıl düzgün şekilde oluşturulacağını çözmesini imkansız olmasa da zorlaştıracaktır. Tüm parametreler birden fazla yapılandırıcı için çözülebilir olsa bile Container'ın hangi yapılandırıcıyı seçmesi gerektiği sorusu belirsiz bırakılır. Bir DI Container'ın birden fazla yapılandırıcıdan birini seçmek için belirli kuralları olabilir, ancak bu karar geliştirici için net olmayabilir. Sınıfta yapılan değişiklikler, geliştiricinin haberi olmadan farklı bir kod yoluna neden olabilir. İyi değil.

Konteyneri Kodunuzla Karıştırma

Birincisi, bu konuda suçluyum ve yakın zamanda Stefan Glienke bunu önerdiğinde bunun bir anti-model olduğunu fark ettim. Muhtemelen internette size söyleyeceğim şeyin yanlış yol olduğunu gösteren demo kodunu bulacaksınız. Demo kodumu temizlemeye çalıştım – bu kitabın kodu doğru olmalı – ve umarım bunu tamamen yapmışımdır (ortada çok fazla demo kodum var…). Ne yazık ki - yaşa ve öğren. (MÖ'nün notu : Benim yazdığım bazı örnek projelerde de aynı yöntemi kullanmıştım. Bunu tekrar gözden geçirip daha uygun bir yol bulmaya çalışacağım)

Kayıtları Unit'in initialization bölümüne koyarak sınıflarınızın ve arayüzlerinizin kaydedilmesini kodunuzla karıştırmak bir anti-kalıptır. Bunu yaptığınızda sınıfınızı Container'a bağlarsınız ve daha önce tartıştığımız gibi gereksiz bağlaşımlardan kaçınmalısınız.

Örneğin: Birim test protokolü, sınıflarınızı ayrı ayrı test etmeniz gerektiğini belirtir. Eğer kaydınızı initialization kısmında yaparsanız Container’ı dahil etmeden kodunuzu test edemezsiniz.

Bunun yerine ayrı bir Unit oluşturup kayıt kodunuzu o Unit'in initialization bölümüne koyun ve ardından o birimi projeye dahil ederek yalnızca DPR dosyasında kullanın. Bu, Konteyneri kodunuzdan ayrı tutarken kaydedilmesi gereken her şeyi kaydetmenize olanak tanır. Bu kitabın örnek kodu bu tekniği göstermektedir.

Sonuç

Bağımlılık Enjeksiyonu ile işleri yapmanın doğru yolları olduğu gibi, işleri yapmanın yanlış yolları da vardır. Bu bölümde, Bağımlılık Enjeksiyonu yapılırken cazip gelen ancak iyi bir fikir olmayan bazı anti-kalıplar tartışıldı. Onlardan uzak durun ve birçok sorundan kaçınmış olun.

Basit, Kullanışlı ve Eksiksiz bir Örnek

Giriş

Tamam, çok fazla teorimiz ve birçok temel örneğimiz vardı, bu yüzden şimdi bunların hepsini gerçekten yararlı bir şeyler yapan, gerçek, çalışan bir demoda bir araya getirmenin zamanı geldi.

Aşağıdaki örnek, dosyaları görüntülemenizi sağlayan basit bir uygulamadır. Daha fazla dosya türü görüntüleyici yazabilmeniz ve bunları tasarım zamanında uygulamaya ekleyebilmeniz açısından genişletilebilir. Varsayılan olarak bir metin dosyası görüntüleyici ve en popüler grafik türleri için bir görüntüleyici sağlar.

Ve tabii ki temel tasarım modeli olarak Dependency Injection'ı kullanıyor. Ana kullanım Constructor Injection yoluyladır, ancak bazı Property Injection'ları da dahil ettim.

Arayüzler

Doğal olarak uygulama, bu arayüzlerin uygulamalarına değil, arayüzlere bağlıdır (bu noktaya henüz yeterince değindim mi?). İşte bu arayüzler, kendi Unit'lerinde elbette:

unit uFileDisplayerInterfaces;

interface

uses
  Vcl.ExtCtrls
  ;
type
  IDisplayOnPanel = interface
  ['{C334B1AE-F562-4EA6-B98D-BB52F1CBE7A7}']
    procedure DisplayOnPanel(const aPanel: TPanel);
  end;

  IDisplayFile = interface
  ['{3437F0E6-2974-4C1A-BA07-2598A2774855}']
    procedure DisplayFile(const aFilename: string; const aPanel: TPanel);
  end;

  IFileExtensionGetter = interface
  ['{9E28B18F-3CDF-4F6E-B629-D3E02E0E0E6C}']
    function GetExtension(const aFilename: string): string;
  end;

  IFilenameGetter = interface
  ['{48E1FFD8-73EA-43DF-B722-4A86206BDFCE}']
    function GetFilename: string;
  end;

  IFileDisplayerRegistry = interface
  ['{7211F4E0-0E7E-4216-912D-069E21660DE1}']
    procedure AddDisplayer(aExt: string; aDisplayer: IDisplayFile);
    function GetDisplayer(aExt: string): IDisplayFile;
    function GetExtensions: TArray<string>;
  end;

implementation

end.
Uygulamanın üç ana bağımlılığı vardır:
  • IDisplayFile – Bu, dosyayı gerçekten görüntülemekten sorumlu arayüzdür. Görüntülenmesi gereken her şeyin görüntüleneceği dosya adını ve bir TPanel'i gerektirir.
  • IFileExtensionGetter – Bu arayüz, dosyayla nasıl başa çıkılacağını bilmek için gerekli olan belirli bir dosyadan uzantıyı almak üzere tasarlanmıştır. Uygulamamız, System.IOUtils'den bir işlev çağrısını çağırır, ancak bu, istediğiniz şekilde belirlenebilir.
  • IFilenameGetter – Bu arayüzün amacı, görüntülenecek dosya adını almak için bir araç sağlamaktır. Bizim uygulamamızda, kullanıcıya açık bir iletişim kutusu aracılığıyla bilgi verilir, ancak arayüz düzgün bir şekilde uygulandığı sürece herhangi bir yolla bir dosya adına ulaşılabilir.

Dosya Görüntüleyici

Tüm işi yapan ve bağımlılıkları alan ana sınıfa bir göz atalım. Bağımlılıklarını yönetmek için Constructor Injection'ı kullanan bağımsız bir sınıftır:

unit uFileDisplayer;

interface

uses
    Vcl.ExtCtrls
  , uFileDisplayerInterfaces
  ;
type
  TFileDisplayer = class(TInterfacedObject, IDisplayOnPanel)
  private
    FFilenameGetter: IFilenameGetter;
    FFileExtensionGetter: IFileExtensionGetter;
    FFileDisplayer: IDisplayFile;

    procedure ClearPanelChildren(const aPanel: TPanel);
  public
    constructor Create(aFilenameGetter: IFilenameGetter; aFileExtensionGetter: IFileExtensionGetter);
    procedure DisplayOnPanel(const aPanel: TPanel);
  end;

implementation
uses
  System.Classes,
  uFileDisplayerRegistry
  ;

procedure TFileDisplayer.ClearPanelChildren(const aPanel: TPanel);
var
  Component: TComponent;
  i: integer;
begin
  for i := 0 to aPanel.ControlCount - 1 do
  begin
    Component := aPanel.Controls[i] as TComponent;
    Component.Free;
  end;
end;

constructor TFileDisplayer.Create(aFilenameGetter: IFilenameGetter; aFileExtensionGetter: IFileExtensionGetter);
begin
  inherited Create;
  FFilenameGetter := aFilenameGetter;
  FFileExtensionGetter := aFileExtensionGetter;
end;

procedure TFileDisplayer.DisplayOnPanel(const aPanel: TPanel);
var
  LExt: string;
  LFilename: string;
begin
  ClearPanelChildren(aPanel);
  LFilename := FFilenameGetter.GetFilename;
  LExt := FFileExtensionGetter.GetExtension(LFilename);
  FFileDisplayer := FileDisplayerRegistry.GetDisplayer(LExt);
  FFileDisplayer.DisplayFile(LFilename, aPanel);
end;

end.
Her zamanki gibi yukarıdaki kodla ilgili dikkat edilmesi gereken bazı noktalara göz atalım:
  • TFileDisplayer sınıfı IDisplayOnPanel arayüzünü uygular. Bu sınıfı oldukça güzel buluyorum. Bağımlılıklarını ister, bu bağımlılıkları tek bir şey yapmak için kullanır ve tüm bunları aslında hiçbir şey uygulamadan yapar. Sevimli.
  • Yapılandırıcı, arayüz olarak iki bağımlılığı enjekte eder: bir IFilenameGetter ve bir IFileExtensionGetter. Yapılandırıcının çok basit olduğunu ve yalnızca bu arayüz referanslarını daha sonra kullanmak üzere sakladığını unutmayın.
  • DisplayOnPanel yöntemi, IDisplayOnPanel'i uygulayan yöntemdir ve bağımlılıkların kullanıldığı yöntemdir. Gerekli bilgileri toplar, uzantıyı alır, doğru Görüntüleyiciyi elde etmek için uzantıyı kullanır ve ardından bu görüntüleyicide DisplayFile'ı çağırır.
  • Dosya görüntüleyicileri yönetmek için FileDisplayerRegistry adlı bir sınıf kullanır. Birazdan bu sınıf hakkında konuşacağız.
  • Ve işte bu. Şimdi tek ihtiyacımız olan görüntüleyicileri uygulamak ve ardından bu ince sınıfı VCL kullanıcı arayüzüne bağlayabiliriz ve işleyen bir uygulamamız olur. Bu güzel, çünkü kullanıcı arayüzüne gevşek bir şekilde bağlanabiliyoruz (loosely couple) ve işin çoğunu soyutlamalara karşı kodlama yaparak yapabiliyoruz. Yukarıdaki sınıfın tam olarak bunu yaptığını unutmayın. Sınıftaki her kod satırı bir arayüze göre kodlanıyor. Bu arayüzlerin nasıl uygulandığını bilmiyor veya umursamıyor.

IFilenameGetter için kullandığımız uygulama şöyle:

unit uFilenameGetter;

interface

uses uFileDisplayerInterfaces;
type
  TFilenameGetter = class(TInterfacedObject, IFilenameGetter)
  private
    function GetFilename: string;
  end;

implementation

uses
    Vcl.Dialogs
  , Spring.Container
  ;

function TFilenameGetter.GetFilename: string;
begin
  PromptForFileName(Result);
end;

end.

ve IFileExtensionGetter için:

unit uFilenameExtensionGetter;

interface

uses uFileDisplayerInterfaces;

type
  TFileExtensionGetter = class(TInterfacedObject, IFileExtensionGetter)
  private
    function GetExtension(const aFilename: string): string;
  end;

implementation

uses
    System.IOUtils
  , Spring.Container
  ;

function TFileExtensionGetter.GetExtension(const aFilename: string): string;
begin
  Result := TPath.GetExtension(aFilename);
end;

end.
Her ikisi de oldukça basit ve kendini açıklıyor. Arayüzleri uyguladıkları için, isterseniz bunları kolayca farklı şekilde uygulayabilirsiniz ve arayüzün sözleşmesini karşıladıkları sürece uygulama içinde gayet iyi performans göstereceklerdir. Bu nedenle bir uygulamaya değil, bir arayüze kod yazarsınız. Dosya adınızı bir veritabanından almak istiyorsanız, bunu belirli arayüzün uygulanması (implementation'u) dışında hiçbir şeyi değiştirmeden yapabilirsiniz.

Tamam, uygulamamız gereken bir sonraki arayüz IDisplayFile. Bu arayüz asıl işin yapıldığı yerdir. Uygulamamız için, biri metin dosyalarını görüntüleyebilen ve diğeri grafik dosyalarını görüntüleyebilen iki tane uygulama yazıyoruz.

İşte metni görüntüleyebilen TTextFileDisplayer:

unit uTextFileDisplayer;

interface

uses
    uFileDisplayerInterfaces
  , Vcl.ExtCtrls
  ;

type
  TTextFileDisplayer = class(TInterfacedObject, IDisplayFile)
    procedure DisplayFile(const aFilename: string; const aPanel: TPanel);
  end;

implementation

uses
    Vcl.StdCtrls
  , Vcl.Controls
  ;

procedure TTextFileDisplayer.DisplayFile(const aFilename: string; const aPanel: TPanel);
var
  Memo: TMemo;
begin
  Memo := TMemo.Create(aPanel);
  Memo.Parent := aPanel;
  Memo.Align := alClient;
  Memo.ReadOnly := True;
  Memo.Lines.LoadFromFile(aFilename);
end;

end.

Bu sınıf basittir (tüm sınıfların olması gerektiği gibi, değil mi?). Aktarılan panele bir TMemo ekleyerek ve metin dosyasını bu panelde açarak IDisplayFile'ın tek yöntemini uygular. Daha basit olamazdı. Bir sınıfın bir şeyi yapıp onu iyi yapmasına dair bir başka güzel örnek.

Grafikleri görüntülemek için çok benzer bir uygulama:

unit uPictureDisplayer;

interface

uses
    uFileDisplayerInterfaces
  , Vcl.ExtCtrls
  ;

type
  TPictureDisplayer = class(TInterfacedObject, IDisplayFile)
    procedure DisplayFile(const aFilename: string; const aPanel: TPanel);
  end;

implementation

uses
    System.Classes
  , Vcl.Graphics
  , Vcl.Controls
  , Vcl.Imaging.JPEG
  , Vcl.Imaging.PngImage
  , Vcl.Imaging.GIFImg
  ;
procedure TPictureDisplayer.DisplayFile(const aFilename: string; const aPanel: TPanel);
var
  Image: TImage;
begin
  Image := TImage.Create(aPanel);
  Image.Parent := aPanel;
  Image.Align := alClient;
  Image.Picture.Bitmap := TBitmap.Create;
  Image.Picture.LoadFromFile(aFilename);
end;

end.
Bunun için çok fazla açıklamaya gerek yok; sınıf herhangi bir Bitmap, JPEG, GIF veya PNG dosyasının açılmasını sağlamak için TImage'ın yeteneklerinden yararlanıyor. Şık.

Hepsini Bir Araya Getirme

İşleri bizim için yöneten bir ana sınıf oluşturmak için Constructor Injection'ı kullandık. Dosyaları görüntülememize izin veren sınıflar uyguladık. O halde artık hepsini birbirine bağlamanın zamanı geldi.

Dosya Uzantılarını Kaydetme

Öncelikle tüm dosya görüntüleyicileri içerecek bir yere ihtiyacımız var. Bu görüntüleyicilerden hangisini bu uzantılar için kullanacağımızı bize söyleyen birkaç dosya görüntüleyicimiz ve dosya uzantımız olduğundan, IDictionary (Spring4D'den bir TDictionary benzeri) amaçlarımıza gayet uygundur. Çiftleri saklayıp geri alabilmek için bir kayıt defteri (registry) kullanabiliriz. Arayüze öncelikli kodlama yapmak istediğimizden, yapmasını istediğimiz işi yapan IFileDisplayerRegistry'yi (yukarıda görüldü) oluşturduk. İşte uygulama:

unit uFileDisplayerRegistry;

interface

uses
    uFileDisplayerInterfaces
  , Spring.Collections
  ;
type
  TFileDisplayerRegistry = class(TInterfacedObject, IFileDisplayerRegistry)
  private
    FDictionary: IDictionary<string, IDisplayFile>;
    FDefaultDisplayer: IDisplayFile;
  public
    constructor Create;
    procedure AddDisplayer(aExt: string; aDisplayer: IDisplayFile);
    function GetDisplayer(aExt: string): IDisplayFile;
    function GetExtensions: TArray<string>;
    // Property Injection: We have a default displayer, but you can provide your own if
    // you want to.
    property DefaultDisplayer: IDisplayFile read FDefaultDisplayer write FDefaultDisplayer;
  end;

function FileDisplayerRegistry: IFileDisplayerRegistry;

implementation

uses

    Spring.Container
  , uDefaultDisplayer
  ;

var
  FDR: IFileDisplayerRegistry;

function FileDisplayerRegistry: IFileDisplayerRegistry;
begin
  if FDR = nil then
  begin
    FDR := TFileDisplayerRegistry.Create;
    A Simple, Useful, and Complete Example 100
  end;
  Result := FDR;
end;

procedure TFileDisplayerRegistry.AddDisplayer(aExt: string; aDisplayer: IDisplayFile);
begin
  FDictionary.Add(aExt, aDisplayer);
end;

constructor TFileDisplayerRegistry.Create;
begin
  FDictionary := TCollections.CreateDictionary<string, IDisplayFile>;
  FDefaultDisplayer := TDefaultFileDisplayer.Create;
end;

function TFileDisplayerRegistry.GetDisplayer(aExt: string): IDisplayFile;
begin
  // never return nil. If a Displayer is not found, then return the default one.
  FDictionary.TryGetValue(aExt, Result);
  if Result = nil then
  begin
    Result := FDefaultDisplayer;
  end;
end;

function TFileDisplayerRegistry.GetExtensions: TArray<string>;
var
  Extension: string;
  i: Integer;
begin
  SetLength(Result, FDictionary.Count);
  i := 0;
  for Extension in FDictionary.Keys do
  begin
    Result[i] := FDictionary.Keys.ElementAt(i);
    Inc(i);
  end;

end;

end.
TFileDisplayerRegistry sınıfının üç yöntemi (method) vardır. Bunlardan ikisi, dosya uzantıları ile söz konusu dosya türünü görüntüleyebilen görüntüleyiciler arasındaki bağlantıyı yönetmek içindir. Görüntüleyicileri hem ekleyebilir hem de alabilirsiniz. Üçüncü yöntem (method), kullanıcıya hangi tür dosyaların açılabileceğini bilmeleri için desteklenen uzantıların bir listesini almak için kullanılır.

Ayrıca, Property Enjeksiyon kalıbını izleyen tek bir özelliğe (DefaultDisplayer) sahiptir. Seçilen dosya için kayıtlı bir uzantı olmaması durumunda kullanılacak varsayılan dosya görüntüleyici için bir görüntüleyici atamanıza olanak tanır. Sınıf, kayıtlı bir görüntüleyici bulunamadığında döndürülen varsayılan bir görüntüleyici (aşağıda görülmektedir) sağlar. (GetDisplayer'ın ne olursa olsun her zaman bir IDisplayFile uygulamasını döndürerek "Asla sıfır döndürme" kuralını izlediğini unutmayın). Ek olarak, kendi varsayılan dosya görüntüleyicinizi sağlamak istiyorsanız, bunu, tıpkı Özellik Enjeksiyonu bölümünde konuştuğumuz gibi, DefaultDisplayer özelliğine kendi görüntüleyicinizi atayarak yapabilirsiniz. Bakın, bu şey gerçekten işe yarıyor.

TFileDisplayerRegistry sınıfının Kayıt Defteri Kalıbını (Registry Pattern) uyguladığını ve bu nedenle Singleton olarak sunulduğunu da unutmayın. Singleton Kalıbının gözden düşmesine rağmen (bu aslında sadece yüceltilmiş bir global değişkendir….). Kayıt Kalıbı, sonuçta ortaya çıkan Kayıt Defteri sınıfının bir Singleton olmasını gerektirir. Bunu demo amacıyla bu şekilde yaptım; uygulamanın daha sağlam bir sürümü, buna gerek duymadan kaydı yönetebilir.

İşte seçilen dosya hakkında yalnızca bazı basit bilgilerin çıktısını veren varsayılan görüntüleyici:

unit uDefaultDisplayer;

interface

uses
    uFileDisplayerInterfaces
  , Vcl.ExtCtrls
  ;
type

  TDefaultFileDisplayer = class(TInterfacedObject, IDisplayFile)
  strict private
    function GetFileSize(aFilename: string): Int64;
    procedure DisplayFile(const aFilename: string; const aPanel: TPanel);
  end;

implementation
uses
    Vcl.StdCtrls
  , Vcl.Controls
  , System.IOUtils
  , System.SysUtils
  ;

procedure TDefaultFileDisplayer.DisplayFile(const aFilename: string; const aPanel: TPanel);
var
  Memo: TMemo;
begin
  Memo := TMemo.Create(aPanel);
  Memo.Parent := aPanel;
  Memo.Align := alClient;
  Memo.ReadOnly := True;
  Memo.Lines.Add('Filename: ' + aFilename);
  Memo.Lines.Add('File Size: ' + IntToStr(GetFileSize(aFilename)) + ' bytes');
  Memo.Lines.Add('Creation Time: ' + DateTimeToStr(TFile.GetCreationTime(aFilename)));
  Memo.Lines.Add('Last Access Time: ' + DateTimeToStr(TFile.GetLastAccessTime(aFilename)));
  Memo.Lines.Add('Last Write Time: ' + DateTimeToStr(TFile.GetLastWriteTime(aFilename)));
end;

function TDefaultFileDisplayer.GetFileSize(aFilename: string): Int64;
var
  SR : TSearchRec;
begin
  if FindFirst(aFilename, faAnyFile, SR) = 0 then
  begin
    Result := Int64(SR.FindData.nFileSizeHigh) shl Int64(32) + Int64(SR.FindData.nFileSizeLow)
  end else
  begin
    Result := -1;
  end;
  FindClose(SR) ;
end;

end.

Tüm bu görüntüleyicilerin ve sınıfların kaydedilmesini sağlamak için proje, uzantıların ve görüntüleyicilerin tüm kayıtlarını yapan bir prosedüre sahip uRegistration.pas adlı bir birim içerir. Aynı görüntüleyiciye birden fazla dosya türü kaydedebileceğinizi unutmayın:

unit uRegistration;

interface

uses
      Spring.Container
    ;

procedure RegisterInterfaces(aContainer: TContainer);
procedure RegisterDisplayers;

implementation

uses
      uFileDisplayerInterfaces
    , uFilenameGetter
    , uFileDisplayer
    , uFileNameExtensionGetter
    , uFileDisplayerRegistry
    , uTextFileDisplayer
    , uPictureDisplayer
    ;

procedure RegisterDisplayers;
var
  TextFileDisplayer: IDisplayFile;
  PictureFileDisplayer: IDisplayFile;
begin
  TextFileDisplayer := TTextFileDisplayer.Create;
  FileDisplayerRegistry.AddDisplayer('.txt', TextFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.pas', TextFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.dpr', TextFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.dproj', TextFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.xml', TextFileDisplayer);

  PictureFileDisplayer := TPictureDisplayer.Create;
  FileDisplayerRegistry.AddDisplayer('.jpg', PictureFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.bmp', PictureFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.png', PictureFileDisplayer);
  FileDisplayerRegistry.AddDisplayer('.gif', PictureFileDisplayer);
end;

end.

TTextFileDisplayer kullanılarak görüntülenebilecek çok sayıda farklı metin dosyasını kaydettim.

Arayüzlerin ve Uygulamaların Kaydedilmesi.

Elbette asıl sihir Bağımlılık Enjeksiyon Kabının içinde gerçekleşir. Arayüzlerin uygulamalarına bağlandığı, ihtiyaç duyulan nesnelerin yaratıldığı ve her şeyin sihirli bir şekilde bağlandığı yer burasıdır. İşte uRegistration biriminden gelen kayıt kodu:

procedure RegisterInterfaces(aContainer: TContainer);
begin
  GlobalContainer.RegisterType<IDisplayOnPanel, TFileDisplayer>;
  GlobalContainer.RegisterType<IFileExtensionGetter, TFileExtensionGetter>;
  GlobalContainer.RegisterType<IFilenameGetter, TFilenameGetter>;
  GlobalContainer.Build;
end;
Bu yapıldıktan sonra bir şey oluşturmanıza gerek kalmaz; konteyner tüm işi sizin için yapacaktır. Uygulamanın herhangi bir iş nesnesi oluşturmadığını fark ettiniz mi (tekil TFileDisplayerRegistry dosyasını kaydedin)? Bunun nedeni, Container'ın tüm arayüz referanslarının tüm oluşturulmasını ve kablolanmasını birlikte yapmasıdır. (Enjekte edilebilirler hakkında konuştuğumuzu hatırlıyor musunuz? Uygulamadaki çoğu şey enjekte edilebilirdir. Oluşturulabilen tek şey, dosya uzantısı kaydındaki IDictionary'dir.)

Kullanıcı Arayüzüne Bağlanma

Tabii ki, kullanıcının kullanımına sunmadığınız sürece bunların hiçbir yararı olmaz. Tasarım zamanında dosyaları görüntülememize izin verecek basit bir form:

Form Tasarımcısındaki Dosya Görüntüleyici uygulaması
Form Tasarımcısındaki Dosya Görüntüleyici uygulaması

Her şeyin işe yaramasını sağlayacak kod acıklı derecede basittir. Tüm ağır işleri zaten yaptık; artık uygulamada bir dosyayı göstermek için güzel mimarimizin avantajlarından yararlanmamız yeterli.

İlk olarak IDisplayOnPanel tipinin formuna özel bir değişken ekliyoruz:

type
  TFileDisplayerForm = class(TForm)
    Button1: TButton;
    Panel1: TPanel;
    Button2: TButton;
    ListBox1: TListBox;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    FContainer: TContainer;
    FileDisplayer: IDisplayOnPanel;
  end;

Daha sonra formun OnCreate olayına aşağıdaki kodu ekliyoruz:

procedure TFileDisplayerForm.FormCreate(Sender: TObject);
var
  Extensions: TArray;
  Ext: string;
  s: string;
begin
  FContainer := TContainer.Create;
  FileDisplayer := FContainer.Resolve;
  Extensions := FileDisplayerRegistry.GetExtensions;
  for Ext in Extensions do
  begin
    s := Format('*.%s', [Ext]);
    ListBox1.Items.Add(s);
  end;
end;

Burada Resolve'a tek çağrımızı yapıyoruz (unutmayın, yalnızca bir çağrı alıyoruz) ve liste kutusunu uygun uzantılarla dolduruyoruz. Orada pek bir şey olmuyor.

İşin özü - ve aslında ince dilimlenmiş et - "Dosyayı Al ve Görüntüle" düğmesinin arkasındadır:

procedure TFileDisplayerForm.Button1Click(Sender: TObject);
begin
  FileDisplayer.DisplayOnPanel(Panel1);
end;

Kullanıcı arayüzü için bu kadar. Cidden. Uygulamamızda oldukça fazla işlevsellik var ancak bunların çoğu kullanıcı arayüzüne bağlı değil. Ve olması gereken de budur.

Ve şimdi tüm zamanların en sevdiğim albümünden bir görseli gösteren uygulama:


Bu Uygulamayı Geliştirmenin-İlerletmenin Yolları

Bu uygulama bir demodur; belirli Dependency Injection tekniklerini çalışırken gösterir. Ancak demo uygulaması olduğu için işleri oldukça basit bırakmış. Uygulamayı aşırı karmaşık hale getirerek üzerinde durulması gereken ana noktaları gölgelemek istemedim.

Uygulamaya eklemeyi düşünebileceğiniz bazı şeyler şunlardır:

  • En açık şekilde, farklı dosya türleri için ek görüntüleyiciler yazabilirsiniz. Her dosya bir şekilde görüntülenebilir ve sonuçları bir TPanel'e yerleştirebildiğiniz sürece ekleyebileceğiniz şeylerin sınırı yoktur.
  • Yeni dosya türlerinin eklenmesi şu anda yeniden derlemeyi gerektirmektedir. Dosya görüntüleyicilerin dinamik olarak bağlanmasına izin veren bir şemayı kolayca tasarlayabilirsiniz.
  • Uygulamada çok az hata kontrolü var veya hiç hata yok, ancak muhtemelen çok az şeyin eklenmesi gerekiyor. Uygulama hiçbir şeyin sıfır olmadığını garanti eder, dolayısıyla bunu kontrol etmenize gerek yoktur. Ancak uygulama, örneğin dosyayı görüntülemeye çalışmadan önce dosyanın gerçekten var olup olmadığını kontrol etmez.
  • Kullanıcının ilgilendiği uzantıyı seçmesine izin vererek ve bu dosya türleri için arama iletişim kutusunu filtreleyerek dosya alma uygulamasını iyileştirebilirsiniz.
  • Dosya uzantısı kayıt defterini Singleton kullanmayan bir uygulamayla değiştirmeyi düşünebilirsiniz.

Sonuçlar

Bitirirken işte bazı düşünceler:

  • Tüm arayüzler tek bir Unit'tedir, dolayısıyla arayüzleri uygulayan birimlere değil, o birime bağlı olursunuz. Elbette elinizde bir sürü birim olabilir, ancak her birine bakmak ve ne işe yaradığını anlamak çok kolay olduğundan, ödenmesi gereken küçük bir bedel haline gelir.
  • Bu birimlerin her biri, tek bir şeyi yapan bir sınıf içerir. Tek Sorumluluk İlkesini takip etmek işleri temiz ve basit tutar. Sonuçta her sınıfın açık ve anlaşılır bir amacı vardır. Ve daha da önemlisi her sınıf test edilebilirdir.
  • Hem görüntüleyici kaydı hem de Dependency Injection Container kaydı için tüm kayıtlar kendi birimindedir. Burası her şeyin bağlandığı yerdir; uRegistration birimi, uygulama birimlerinin kullanıldığı yerdir. Arayüzler ve uygulamalar için tek “temas noktası” budur. Bu, alabileceğiniz en gevşek bağlantıdır.
  • Sonuçta temiz, genişletilebilir, gevşek bağlı bir uygulamamız var. Bu uygulamanın bakımını yapmak, güncellemek ve geliştirmek çok basit olacaktır. Yukarıda anlatılanları eklemek hiç de zor olmasa gerek.

Umarım şu ana kadar, uygulamayı arayüzler ve Dependency Injection ile oluşturmanın faydalarını görebilirsiniz.

Son düşünceler

İşte aldınız. Umarım bu kitabı faydalı bulmuşsunuzdur. Umarım uygulamalarınızı oluşturma konusundaki düşüncelerinizi değiştirmiştir. Umarım soyutlamalara karşı kodlamanın ve bağımlılıklarınızı enjekte etmenin bilgeliğini görürsünüz ve umarım önsözde yapmaya karar verdiğim şeyi başarmışımdır.

Kodu yazmak ve sürdürmek, her şeyi dağınık bir iplik yumağı içinde birbirine karıştırmadan da yeterince zordur. Kodunuzu, her şeyi bağımsız kılacak, bunları gevşek bir şekilde bir araya getirecek ve güçlü ancak test edilmesi ve bakımı kolay bir uygulamayla sonuçlanacak şekilde yazabilseydiniz, bunu yapardınız, değil mi? Umarım sizi Dependency Injection'ın bunu yapabileceğine ikna etmişimdir.

Önsözde söylediğim gibi – Bağımlılık Enjeksiyonu çok basit bir fikir: Sınıflarınıza bağımlılıklarını normalde yapıcı aracılığıyla vermeniz yeterli. Bu kitaptan elde ettiğiniz tek şey bu temel fikirse ne mutlu bana. Bu basit fikir, kodunuzu sıradandan muhteşeme dönüştürebilir.

Dependency Injection, kod yazma biçimimde - her şeyin daha iyisi yönünde - büyük bir fark yarattı. Size yalvarıyorum; bırakın o da sizin için aynısını yapsın.

























Hiç yorum yok:

Yorum Gönder