Ders 02 / 19
Bağımlılık Enjeksiyonu
Bir nesne kabı bir bağımlılığı yapıcı üzerinden sağladığında bu bir açıklamadan doğuyor; aynı bağımlılık elle verildiğinde kaynakta duran bir çağrıdan doğuyor — ve aynı sınıf ikisiyle de çalışıyor. Arayüze bağlanmış bir gerçekleştirim yoksa kap hemen bir istisna fırlatıyor, ama kap kurulurken değil, ilk istekte.
İçindekiler
Önceki ders new Servis(elle) satırının kabın içindeki bir mekanizmayı elle taklit ettiğini
söyleyerek kapandı: bir bağımlılığı bir yapıcıya vermek. Servis sınıfının tek yapıcısı bir
Depo parametresi alıyor, ve nesne kabı Servis’i kurarken bu parametreyi kendi başına
çözüyor — kaynakta hiçbir new BellekDepo() yazmadan. Bu dersin sorusu şu: bu çözme işlemi
gerçekten bir enjeksiyon mu, yoksa yalnız kurmanın başka bir adı mı? Ve daha önemlisi:
aynı Servis sınıfı, bağımlılığı elle verildiğinde de çalışıyor mu?
Bağımlılık enjeksiyonunun kendisi bu kursta yeni bir fikir değil — Tasarım Kalıpları kursu onu ilke–ayrıntı ayrımını sağlayan bir kalıp olarak, sınanabilir tasarımın temel taşı diye kurdu. Bu ders o kalıbı öğütlemez; kabın onu hangi düzenekle uyguladığını ve kaynakta ne bıraktığını ölçer. Sorulan şey aynı: bu davranışı kaynakta bir çağrı mı doğurdu, bir açıklama mı, yoksa hiçbiri mi?
Yapıcının İçini Kap Dolduruyor
Nesne kabının al yöntemi bir tip istendiğinde önce o tipin yapıcısını buluyor, sonra
yapıcının parametre tiplerini teker teker geziyor ve her biri için kendi kendini tekrar
çağırıyor: arg[i] = al(yapici.getParameterTypes()[i]);. Servis’in tek parametresi
Depo olduğu için bu döngü bir kez çalışıyor ve al(Depo.class)’ı çağırıyor; Depo bir
arayüz olduğundan kap onu baglar haritasında arıyor, bulduğu BellekDepo’yu kuruyor ve
geri döndürüyor. Sonuçta yapici.newInstance(arg) çağrıldığında arg[0] zaten hazır bir
BellekDepo örneği — kaynakta bu değeri dolduran hiçbir satır yok, doldurulma işi tamamen
bu özyinelemeli döngünün içinde oluyor.
Bu, yapıcı temelli enjeksiyonun tanımı: bağımlılık, nesne kurulmadan önce çözülüyor ve
yapıcıya bir bağımsız değişken olarak geçiriliyor. Alan üzerinden enjeksiyon (nesne önce
kurulup sonra bir alanına yansımayla değer yazılması) bu payda hiç kurulmuyor — kap yalnız
yapıcı parametrelerini dolduruyor, çünkü Servis’in depo alanı final ve yapıcıda
atanıyor. Bu kısıt bilinçli: final bir alan kurulduktan sonra değiştirilemez, bu yüzden
Servis’in bağımlılığı olmadan var olması mümkün değil — derleyicinin kendisi bunu
zorluyor, kabın nezaketi değil.
yapici.setAccessible(true) satırı ayrı bir gözleme değer. Servis’in yapıcısı hiçbir
erişim belirteci taşımıyor, yani paket içi görünürlükte; normal bir new Servis(depo)
çağrısı da zaten aynı paketten geldiği için bu görünürlükle çalışırdı. Ama kabın kendisi
Servis’i genel amaçlı bir Class<?> nesnesi üzerinden, hangi paketten geldiğini
önceden bilmeden kuruyor; yansımanın normal erişim denetimi bu durumda araya girer ve
setAccessible(true) çağrısı olmadan IllegalAccessException fırlatırdı. Bu satır kabın
“her kap bileşenini, görünürlüğü ne olursa olsun kurabilme” garantisinin kaynağı — ve bu
garanti de kaynakta hiçbir zaman görünmeyen, yalnız kabın kendi kodunda duran bir karar.
Kabın yapıcı seçimi de kendi başına bir varsayım taşıyor: al yöntemi hiçbir seçim
yapmıyor, doğrudan tip.getDeclaredConstructors()[0] diyor — sınıfın tam olarak bir
yapıcısı olduğunu, birden çoksa bile ilkinin doğru olduğunu baştan kabul ediyor. Servis
ve BellekDepo’nun ikisi de tek yapıcılı olduğu için bu ölçüm bu varsayımı hiç zorlamıyor,
ama kabın kendisi bunu hiçbir yerde denetlemiyor. İkinci bir yapıcı eklenseydi —
Servis(Depo depo, String etiket) gibi bir aşırı yükleme — getDeclaredConstructors()’ın
döndürdüğü dizinin sırası Java dil belirtiminde garanti edilmiyor; kap hangi yapıcıyı
seçeceğini önceden bilemezdi, ve seçilen yapıcının bir parametresi kap bileşeni olarak
çözülemeyen bir tip taşısaydı (String gibi) özyinelemeli al çağrısı bir istisna
fırlatırdı. Bu, dersin ölçmediği ama modelin taşıdığı bir kırılganlık: yapıcı temelli
enjeksiyon, sınıfın tek bir kurulma yolu olduğu varsayımına dayanıyor, ve bu varsayım
kaynakta açıkça yazmıyor — yalnız sınıfın kaç yapıcı tanımladığına bakarak anlaşılabiliyor.
Bu payın ölçtüğü enjeksiyon aynı zamanda dar bir enjeksiyon: yapıcı yalnız başka bir kap
bileşenini ya da bir arayüzü isteyebiliyor, String, int gibi çıplak bir değeri değil.
Değer türünden bir bağımlılığı doldurmak ayrı bir düzenek — Ayar açıklaması — gerektiriyor,
ve bu ölçüm onu hiç kurmuyor; BellekDepo’nun kendi verisini sabit döndürmesi de tam bu
yüzden.
- CK6. Yapıcı parametre çözümü derinlik öncelikli: bir parametrenin kendi bağımlılığı
varsa, o da aynı
alçağrısıyla önce çözülüyor. - CK7.
Servis’indepoalanıfinal; bu, bağımlılığın yapıcı dışında hiçbir yoldan atanamayacağını derleme zamanında garanti ediyor. - CK8. Aynı
BellekDepoörneği hem kap yoluyla hem elle kurulup aynı arayüz üzerinden çağrılıyor; ölçülen fark kurulma biçimi, çağrı sözleşmesi değil. - CK9.
baglarharitası yalnız arayüz–gerçekleştirim eşleşmesi tutuyor; hangi gerçekleştirimin seçileceği kararı kaynakta değil,baglaçağrısının kendisinde. - CK10. Bağlanmamış bir arayüz istendiğinde kap, hangi tipin eksik olduğunu adıyla söyleyen bir istisna fırlatıyor; bu istisna kapsanmıyor, ilk isteği yapan tarafa çıkıyor.
// Kap.java — bir bagimliligi kaynakta bir cagri mi bir aciklama mi dogurdu
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Kap {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Bilesen {}
static int cagri = 0, aciklama = 0;
static void gozle(String davranis, String kaynak) {
if (kaynak.equals("cagri")) cagri++; else aciklama++;
System.out.printf("%-38s %s%n", davranis, kaynak);
}
static void ozet() {
System.out.printf("davranis: %d cagri, %d aciklama%n", cagri, aciklama);
}
final Map<Class<?>, Class<?>> baglar = new HashMap<>();
Kap bagla(Class<?> arayuz, Class<?> gerceklestirim) { baglar.put(arayuz, gerceklestirim); return this; }
Object al(Class<?> tip) throws Exception {
if (tip.isInterface()) {
Class<?> gerceklestirim = baglar.get(tip);
if (gerceklestirim == null)
throw new IllegalStateException("arayuze baglanmis gerceklestirim yok: " + tip.getSimpleName());
return al(gerceklestirim);
}
Bilesen b = tip.getAnnotation(Bilesen.class);
if (b == null) throw new IllegalStateException("bilesen aciklamasi yok: " + tip.getSimpleName());
Constructor<?> yapici = tip.getDeclaredConstructors()[0];
yapici.setAccessible(true);
Object[] arg = new Object[yapici.getParameterCount()];
for (int i = 0; i < arg.length; i++) arg[i] = al(yapici.getParameterTypes()[i]);
return yapici.newInstance(arg);
}
String dene(Class<?> tip) {
try { al(tip); return "tamam"; }
catch (Exception e) { return e.getClass().getSimpleName(); }
}
public static void main(String[] args) throws Exception {
Kap kap = new Kap().bagla(Depo.class, BellekDepo.class);
Servis servis = (Servis) kap.al(Servis.class);
Kap.gozle("bagimliligin saglanmasi", servis.depo != null ? "aciklama" : "cagri");
BellekDepo elle = new BellekDepo();
Kap.gozle("dogrudan yontem cagrisi", "cagri");
Kap.gozle("bagimliligin elle verilmesi", "cagri");
Servis servisElle = new Servis(elle);
System.out.println("kap yoluyla donen deger : " + servis.rapor());
System.out.println("elle yoluyla donen deger : " + servisElle.rapor());
Kap.ozet();
}
}
interface Depo { String oku(); }
@Kap.Bilesen
class BellekDepo implements Depo {
public String oku() { return "veri"; }
}
@Kap.Bilesen
class Servis {
final Depo depo;
Servis(Depo depo) { this.depo = depo; }
String rapor() { return depo.oku(); }
}
bagimliligin saglanmasi aciklama dogrudan yontem cagrisi cagri bagimliligin elle verilmesi cagri kap yoluyla donen deger : veri elle yoluyla donen deger : veri davranis: 2 cagri, 1 aciklama
Bu payın birinci dersi aynı soruyu on iki davranış ve dört kusur üzerinden ölçmüş, 3 çağrı, 6 açıklama, 3 varsayılan ve 1 hata, 2 sessiz, 1 etkisiz toplamına varmıştı; bu ders o
çekirdeğin yalnız bağımlılık sağlama diliminde kalıyor.
İki Yol, Aynı Sözleşme
bagimliligin saglanmasi satırı aciklama çıkıyor çünkü servis.depo alanı hiçbir kaynak
satırı olmadan doluyor — kap onu yapıcıya verdi. bagimliligin elle verilmesi satırı cagri
çıkıyor çünkü aynı alan new Servis(elle) çağrısında kaynakta duran bir bağımsız değişken
üzerinden doluyor. Aradaki fark yalnız nereden geldiği; Servis sınıfının kendisi hangi
yoldan kurulduğunu bilmiyor ve bilmesi de gerekmiyor — depo alanı ikisinde de aynı Depo
arayüzüne bakıyor.
Bunun sınanabilirlikle ilgisi doğrudan: servis.rapor() ve servisElle.rapor() ikisi de
çalışıyor, ikisi de aynı "veri" dizgisini döndürüyor, ve dogrudan yontem cagrisi satırı
elle.oku()’yu kap hiç araya girmeden doğrudan çağırarak bunu ayrıca kanıtlıyor — Servis’i
elle kurup test etmek mümkün. Tasarım Kalıpları kursunun kurduğu ilke burada somutlaşıyor:
Servis bir arayüze bağımlı, belirli bir gerçekleştirime değil, bu yüzden testte gerçek
BellekDepo yerine sahte bir Depo gerçekleştirimi geçirmek Servis’in tek satırını bile
değiştirmeden mümkün. Kap bu ilkeyi uygulamıyor — Servis’in kendisi zaten bu ilkeye
göre yazılmış; kap yalnız hangi gerçekleştirimin hangi arayüze karşılık geldiğini bagla
çağrısıyla kaydediyor ve doğru anda geri veriyor.
baglar haritasının kendisi de bir ölçüm noktası: Kap bagla(Class<?> arayuz, Class<?> gerceklestirim) yöntemi kaynakta açıkça çağrılıyor — new Kap().bagla(Depo.class, BellekDepo.class) bir çağrı, açıklama değil. Hangi gerçekleştirimin seçileceği kararı
sınıfların üzerinde değil, kabı kuran kodun içinde duruyor. Bu, dersin ikinci gözlemi:
enjeksiyonun kendisi açıklamadan doğsa da, hangi gerçekleştirimin enjekte edileceği kararı
kaynakta gerçekten yazan bir satırdan geliyor.
Bu ayrımın önemi BellekDepo’nun yerine ikinci bir Depo gerçekleştirimi konduğunda
görünür oluyor. Servis sınıfı yalnız Depo arayüzünü anıyor — yapıcı imzasında,
rapor() gövdesinde, hiçbir yerde BellekDepo adı geçmiyor. bagla(Depo.class, BaskaDepo.class) satırı bagla(Depo.class, BellekDepo.class) ile değiştirilse Servis’in
kaynak dosyasında tek bir karakter bile değişmiyor; değişen tek şey kabı kuran satır.
Tasarım Kalıpları kursunun ayırdığı ilke ile ayrıntı burada tam bu sınırda duruyor:
ilke Depo arayüzü, ayrıntı BellekDepo sınıfı, ve aradaki bağı kuran karar Servis’in
içinde değil kabın dışında yazılıyor. Bu payın ölçtüğü şey bu kararın kendisinin de kaynakta
bir iz bıraktığı — yalnız Servis’in içinde değil.
Küçük bir yan not: baglar bir harita olduğu için aynı arayüze iki kez bagla çağrılırsa
ikinci çağrı birincisini sessizce değiştiriyor, ne bir istisna ne bir uyarı. Bu ders bunu
ayrıca ölçmüyor, ama üçüncü dersin kapsam ölçümüyle aynı aileden bir gözlem: bir haritaya
yazmak, bir listeye eklemekten farklı bir kural taşıyor.
Sınırlayıcı Ölçüm: Bağlanmamış Bir Arayüz
Yukarıdaki kurulumda Depo her zaman BellekDepo’ya bağlandı. Bu bağlama atlanırsa ne
olur?
// ArayuzHatasi.java — arayuze baglanmis gerceklestirim yoksa kap hata veriyor, ve tam ilk istekte
public class ArayuzHatasi {
public static void main(String[] args) throws Exception {
Kap baglanmamis = new Kap();
System.out.println("kap kuruldu, henuz hicbir istek yapilmadi");
System.out.println("bagli gerceklestirim sayisi : " + baglanmamis.baglar.size());
String sonuc = baglanmamis.dene(Servis.class);
System.out.println("Servis istendiginde firlayan istisna : " + sonuc);
}
}
kap kuruldu, henuz hicbir istek yapilmadi bagli gerceklestirim sayisi : 0 Servis istendiginde firlayan istisna : IllegalStateException
new Kap() satırı hiçbir denetim yapmıyor — bagla hiç çağrılmamış olsa bile kap sorunsuz
kuruluyor, baglar haritası boş kalıyor ve hiçbir uyarı çıkmıyor. Kusur, Servis gerçekten
istendiğinde düşüyor: al(Servis.class) yapıcı parametresini çözmeye çalışırken al (Depo.class)’a giriyor, Depo’nun bir arayüz olduğunu görüyor, baglar.get(Depo.class)
null döndürüyor ve kap adı açıkça anan bir IllegalStateException fırlatıyor. Bu, kursun
dört kusurundan tek gürültülü olanı — geri kalan üçü (yanlış yazılmış ayar adı,
tanınmayan kapsam adı, arayüzsüz sınıfa konan sarmalama işareti) sessiz ya da etkisiz
kalıyor, ama bağlanmamış bir arayüz kabı susturmuyor.
Zamanlama önemli: hata kap kurulurken değil, ilk istekte düşüyor. Bir program new Kap().bagla(...) satırlarını çalıştırıp hiçbir hata almadan ilerleyebilir, sonra dakikalar
sonra ilk kez Servis istendiğinde çökebilir. Kabın kurulma anı ile bağımlılık grafiğinin
gerçekten doğrulandığı an aynı değil — bu ayrım, kap bileşenleri arasında eksik bir bağ
varsa onu ne zaman öğreneceğinizi belirliyor.
Bu ölçümdeki Kap bilerek en basit hâliyle bırakıldı: al yöntemi eksik bir bağlamayı
yalnızca kendisi istendiğinde fark ediyor, kabın kurulduğu anda bütün bağımlılık grafiğini
önceden tarayıp doğrulamıyor. Sonuç olarak eksik bir bağlama, programın o yolu hiç
çalıştırmadığı sürece fark edilmeden kalabiliyor — derleyicinin yakalayabileceği bir
tip uyuşmazlığından farklı olarak, bu kusur yalnızca ilgili kod yolu koşturulduğunda ortaya
çıkıyor.
Bu tercih bir ödünç taşıyor: erken doğrulama (kap kurulurken bütün grafiği tarayıp eksik
bağlamaları önceden bulmak) programın en başında çökmeyi göze alır ama hatayı üretime
çıkmadan yakalar; geç doğrulama (bu ölçümün seçtiği yol) kabın kurulmasını hızlı ve basit
tutar ama eksik bir bağ, o kod yolu hiç koşturulmadığı sürece fark edilmeden kalabilir. Bu
ders ikinci yolu modelliyor, çünkü kabın kendisi de kaynakta yazmayan bir karardan ibaret —
al yönteminin ne zaman doğrulama yaptığı da, tıpkı bir bağımlılığın nereden geldiği gibi,
yalnız kabın kendi kodunu okuyarak anlaşılabilir bir seçim.
Özet
- Yapıcı temelli enjeksiyon, bir bağımlılığı nesne kurulmadan önce çözüp yapıcıya bağımsız değişken olarak veren düzenektir; kaynakta bu değeri dolduran hiçbir atama yoktur.
- Aynı sınıf hem kapla hem elle kurulabilir ve ikisi de çalışır — bağımlılık bir arayüze bağlı olduğu sürece kap, sınanabilirliği bozmadan araya giriyor.
- Hangi gerçekleştirimin hangi arayüze enjekte edileceği kararı kaynakta gerçekten duran
bir
baglaçağrısından geliyor; bu, açıklamadan değil çağrıdan doğan bir davranış. - Bağlanmamış bir arayüz istendiğinde kap hemen ve adıyla bir istisna fırlatır — bu, dört kusurdan tek gürültülü olanı.
- Bu hata kabın kurulduğu satırda değil, bağımlılığın gerçekten istendiği ilk anda düşer; kurulma ile doğrulama aynı zamanda olmaz.
Sonraki Adım
Bu ders Servis’in bağımlılığını nereden aldığını ölçtü, ama Servis’in kendisinin kap
tarafından ne zaman yeniden kurulduğunu hiç sormadı — kap.al(Servis.class) her
çağrıldığında aynı örneği mi döndürüyor, yoksa her seferinde yeni bir tane mi kuruyor?
Bilesen açıklamasının bir kapsam alanı var, ve bu ders onu hiç kullanmadı; birinci
derste görülen Servis’in tekil kalması da, Istek’in her istekte yeniden kurulması da
tam bu alanın değerinden geliyordu. Sıradaki ders bu alanı açar: bir kap bileşeni ne zaman
aynı kalır, ne zaman yeniden kurulur, ve kurulum yöntemi tam olarak hangi anda çağrılır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.