Ders 03 / 19
Bileşen Kapsamları ve Yaşam Döngüsü
Bir kap bileşeninin tekil kalması nesne kabının varsayılan kararı, her istekte yeniden kurulması ise açıkça yazılmış bir kapsam açıklamasından geliyor. Tanınmayan bir kapsam adı hiçbir hata vermiyor — bileşen sessizce tekil olmaktan çıkıyor ve bu ancak iki isteğin kimliğini `==` ile karşılaştırarak görülüyor.
İçindekiler
Önceki ders Bilesen açıklamasının bir kapsam alanı taşıdığını, ama o alanı hiç
kullanmadığını not ederek kapandı: 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? Bu ders tam bu soruyu açar —
kapsam alanının ne yaptığını ve bir kap bileşeninin ne zaman yeniden kurulduğunu ölçer;
ikinci yarısı da bir bileşenin kurulduktan sonra ama kullanılmadan önce çağrılan tek bir
yöntemi — yaşam döngüsü kancasını — inceler.
Kapsam Kararının Yeri
Bilesen açıklaması bir kapsam alanı taşıyor ve varsayılan değeri "tekil". al
yöntemi bir tip için bu değeri okuyor ve yalnız tekil.equals("tekil") doğruysa
tekiller haritasına bakıyor; kapsam başka bir değerse ("her-istekte" gibi) bu denetim
hiç yapılmıyor ve kod her seferinde yapici.newInstance(arg) satırına düşerek yeni bir
örnek kuruyor. Servis sınıfı @Kap.Bilesen yazıyor, parametre vermeden — bu, kapsam()’ın
varsayılan değerinin uygulanacağı anlamına geliyor. Istek sınıfı ise @Kap.Bilesen(kapsam = "her-istekte") yazıyor — açık bir değer.
Bu ayrımın doğrulanma yolu kimliktir, değer eşitliği değil: kap.al(Servis.class) == servis ifadesi iki referansın aynı bellek konumundaki nesneye işaret edip etmediğini
soruyor. Java’da bunu doğrulamanın başka bir kestirme yolu yok — iki Servis nesnesinin
alanları aynı olsa bile == yalnız gerçekten aynı nesne olduklarında true verir. Bu
dersin ölçümü kimlik karmasını (hashCode() çıktısını) hiçbir yerde basmıyor, çünkü o
değer koşumdan koşuma değişebilir ve kendisi bir kanıt değil; kanıt yalnız ==
karşılaştırmasının sonucu.
- CK11. Kapsam denetimi yalnız
tekildeğeri içintekillerharitasına bakıyor; başka her değer “her seferinde yeniden kur” anlamına geliyor, ayrı bir liste ya da sayaç tutulmuyor. - CK12. Kimlik yalnız
==ile okunuyor; hiçbir nesnenin kimlik karması ya da bellek adresi basılmıyor. - CK13. Kurulum yöntemi (
Kuraçıklaması) yapıcı çalıştıktan ve ayar alanları dolduktan sonra çağrılıyor; nesne zaten tamamen kurulmuş durumda. - CK14. Bir kap bileşeninde en çok bir kurulum yöntemi bulunuyor; bu ölçüm birden çok
Kurişaretli yöntemi olan bir sınıfı denemiyor. - CK15. Tanınmayan bir kapsam adı (
"tanimsiz-kapsam"gibi) derleme zamanında yakalanmıyor —kapsam()bir dizgi döndürüyor, kap onu yalnız çalışma zamanında"tekil"’e eşitliyor.
// Kap.java — bir kap bileseninin kapsamini ve kurulum sirasini kaynakta ne dogurdu: bir aciklama mi, kabin varsayilani mi
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Kap {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Bilesen { String kapsam() default "tekil"; }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Kur {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface Ayar { String ad(); String varsayilan(); }
static int aciklama = 0, varsayilan = 0;
static int sessiz = 0;
static void gozle(String davranis, String kaynak) {
if (kaynak.equals("varsayilan")) varsayilan++; else aciklama++;
System.out.printf("%-38s %s%n", davranis, kaynak);
}
static void kural(String ad, String sonuc) {
if (sonuc.equals("sessiz")) sessiz++;
System.out.printf("%-38s %s%n", ad, sonuc);
}
static void ozet() {
System.out.printf("davranis: %d aciklama, %d varsayilan%n", aciklama, varsayilan);
System.out.printf("kusur : %d sessiz%n", sessiz);
}
final Map<Class<?>, Object> tekiller = new HashMap<>();
final Map<String, String> ayarlar = new LinkedHashMap<>();
final List<String> gunluk = new ArrayList<>();
final Map<Class<?>, Class<?>> baglar = new HashMap<>();
Kap ayar(String ad, String deger) { ayarlar.put(ad, deger); return this; }
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());
boolean tekil = b.kapsam().equals("tekil");
if (tekil && tekiller.containsKey(tip)) return tekiller.get(tip);
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]);
Object ornek = yapici.newInstance(arg);
for (Field f : tip.getDeclaredFields()) {
Ayar a = f.getAnnotation(Ayar.class);
if (a == null) continue;
f.setAccessible(true);
f.set(ornek, ayarlar.getOrDefault(a.ad(), a.varsayilan()));
}
for (Method m : tip.getDeclaredMethods()) {
if (m.isAnnotationPresent(Kur.class)) { m.setAccessible(true); m.invoke(ornek); gunluk.add("kur:" + tip.getSimpleName()); }
}
if (tekil) tekiller.put(tip, ornek);
return ornek;
}
public static void main(String[] args) throws Exception {
Kap kap = new Kap().bagla(Depo.class, BellekDepo.class).ayar("depo.baslangic", "dolu");
Servis servis = (Servis) kap.al(Servis.class);
Kap.gozle("ayni ornegin donmesi", kap.al(Servis.class) == servis ? "varsayilan" : "aciklama");
Kap.gozle("her istekte yeni ornek", kap.al(Istek.class) != kap.al(Istek.class) ? "aciklama" : "varsayilan");
Kap.gozle("kurulum yonteminin cagrilmasi", kap.gunluk.contains("kur:BellekDepo") ? "aciklama" : "varsayilan");
BellekDepo elle = new BellekDepo();
System.out.println("kapla kurulanin durumu : " + ((BellekDepo) servis.depo).durum);
System.out.println("elle kurulanin durumu : " + elle.durum);
Kap k2 = new Kap();
Object y1 = k2.al(YanlisKapsam.class), y2 = k2.al(YanlisKapsam.class);
Kap.kural("kapsam adi taninmiyor", y1 == y2 ? "etkisiz" : "sessiz");
Kap.ozet();
}
}
interface Depo { String oku(); }
@Kap.Bilesen
class BellekDepo implements Depo {
@Kap.Ayar(ad = "depo.baslangic", varsayilan = "bos") String baslangic;
String durum = "kurulmadi";
@Kap.Kur void hazirla() { durum = "hazir:" + baslangic; }
public String oku() { return durum; }
}
@Kap.Bilesen
class Servis {
final Depo depo;
Servis(Depo depo) { this.depo = depo; }
String rapor() { return depo.oku(); }
}
@Kap.Bilesen(kapsam = "her-istekte")
class Istek { }
@Kap.Bilesen(kapsam = "tanimsiz-kapsam")
class YanlisKapsam { }
ayni ornegin donmesi varsayilan her istekte yeni ornek aciklama kurulum yonteminin cagrilmasi aciklama kapla kurulanin durumu : hazir:dolu elle kurulanin durumu : kurulmadi kapsam adi taninmiyor sessiz davranis: 2 aciklama, 1 varsayilan kusur : 1 sessiz
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 kapsam ve yaşam döngüsü diliminde kalıyor — bagla ile enjekte edilen
bağımlılık ve dene’nin ölçtüğü bağlanmamış arayüz burada yeniden ölçülmüyor.
Tekil, Varsayılanın Kendisi
ayni ornegin donmesi satırı varsayilan çıkıyor çünkü Servis’in kaynak dosyasında
kapsamı belirten hiçbir sözcük yok. @Kap.Bilesen çıplak yazılıyor, parantezsiz —
kapsam()’ın default "tekil" değeri devreye giriyor ve bu değeri veren satır Servis.java
değil, Kap.java’daki açıklama tanımı. Bu payın birinci dersinde de görülen bir örüntü
burada tam ortasına oturuyor: kaynağı okuyan biri Servis’in tekil olduğunu bilemez,
çünkü kaynak bunu hiç söylemiyor; yalnız kabın kuralını (varsayılan kapsamın tekil olduğunu)
önceden bilen biri bu sonucu tahmin edebilir.
her istekte yeni ornek satırı ise tam tersi: açıklama çıkıyor, çünkü Istek sınıfı
kapsam = "her-istekte" yazarak varsayılan değeri açıkça bastırıyor. Bu, bir Java
yöntem yeniden tanımlaması değil, bir varsayılan değerin bir açıklama bağımsız
değişkeniyle üzerine yazılması — kap.al(Istek.class) != kap.al(Istek.class) iki ayrı
çağrının iki ayrı nesne döndürdüğünü doğruluyor, ve bu fark kaynakta gerçekten yazan bir
sözcükten geliyor: "her-istekte".
Kurulum Yöntemi: Nesne Hazır Olduktan Sonra
BellekDepo sınıfının hazirla() yöntemi @Kap.Kur taşıyor ve gövdesi tek satır: durum = "hazir:" + baslangic;. Bu yöntemin ne zaman çağrılacağı al yönteminin sırasından
belirleniyor: önce yapıcı çalışıyor (ornek oluşuyor), sonra Ayar işaretli alanlar
dolduruluyor (baslangic alanına değer yazılıyor), sonra Kur işaretli yöntemler
çağrılıyor. Bu sıra rastgele değil — hazirla() çağrıldığında baslangic alanı zaten dolu
olmasaydı, durum değeri "hazir:null" gibi anlamsız bir şey taşırdı. Kap bu sırayı
zorluyor, ve kaynakta bu sırayı belirten hiçbir sayı ya da açıklama parametresi yok — sıra
al yönteminin kod satırlarının kendi sırası.
Kurulum yönteminin gerçekten çağrıldığı kap.gunluk listesiyle doğrulanıyor:
gunluk.add("kur:" + tip.getSimpleName()) satırı her çağrıdan sonra bir kayıt bırakıyor, ve
kurulum yonteminin cagrilmasi satırı bu kaydın varlığını arıyor. Burada gerçek bir saat
ya da sıra numarası tutulmuyor — yalnız “bu yöntem çağrıldı mı” sorusuna cevap veren bir
dizgi listesi. Elle kurulan BellekDepo bu kaydı hiç almıyor: elle.durum alanı sonsuza
kadar "kurulmadi" kalıyor, çünkü hazirla()’yı çağıracak taraf yalnız kap, ve elle kurulan
nesne kabın hiç görmediği bir nesne.
Kapsam ile kurulum yöntemi birbirinden bağımsız görünen iki mekanizma, ama aralarındaki
ilişki al yönteminin kendi sırasından geliyor: kurulum bloğu, tekil önbelleğe yazmadan
önce çalışıyor, ve bu blok kapsamdan habersiz — hangi kapsamda olursa olsun her yeni
kurulan örnek için bir kez koşuyor. Istek sınıfının hiçbir Kur işaretli yöntemi
olmadığı için bu ölçüm bunu doğrudan göstermiyor, ama düzenek şunu ima ediyor: her-istekte
kapsamlı bir bileşenin bir kurulum yöntemi olsaydı, o yöntem her istekte yeniden çağrılırdı
— çünkü her-istekte kapsamı her isteği yeni bir kurulmaya çeviriyor, ve her kurulma kendi
kurulum çağrısını tetikliyor. Tekil bir bileşende kurulum yöntemi ömür boyu tam bir kez
çalışıyor; her-istekte bir bileşende (varsayımsal olarak) her istekte yeniden çalışırdı.
Kapsam ve kurulum yöntemi kaynakta iki ayrı açıklama olarak duruyor, ama aralarındaki bu
ilişki hiçbir yerde yazmıyor — yalnız al yönteminin kod sırasını okuyarak çıkarılabiliyor.
Neden Ayrı Bir Adım
Kurulum yöntemini yapıcının kendisine koymak neden işe yaramazdı? BellekDepo’nun yapıcısı
yok — Java, hiçbir yapıcı yazılmadığında derleyicinin ürettiği parametresiz varsayılan
yapıcıyı kullanıyor. hazirla()‘yı bu yapıcıya taşımak mümkün değil, çünkü Java’da bir
yapıcının gövdesi yalnız kendi sınıfının yazdığı koddur; kabın Ayar alanlarını yapıcıdan
sonra doldurduğu düzenekte, yapıcı çalıştığı anda baslangic alanı henüz null.
Kurulum mantığını yapıcıya koysaydık, o mantık her zaman boş bir değer görürdü.
Bu, kap bileşenlerinin yaşam döngüsünü iki ayrı aşamaya bölmenin gerekçesi: kurma
(yapıcının çalışması, nesnenin bellekte var olması) ve hazırlama (ayarların
yerleşmesi, bağımlı kaynakların açılması, hazırlık mantığının çalışması). Yapıcı yalnız
birinci aşamayı garanti ediyor; ikinci aşama için ayrı bir kanca gerekiyor, ve bu kanca
tam olarak Kur açıklamasının işi. Elle kurulan bir nesnede bu ikinci aşama hiç
gerçekleşmiyor — elle nesnesi birinci aşamadan geçti (yapıcı çalıştı, nesne var), ama
ikinci aşamaya hiç girmedi.
Istek sınıfının hiçbir alanı ve hiçbir kurulum yöntemi yok; her istekte yeni bir örnek
kurulduğunda kap onu tekiller haritasına hiç yazmıyor, dolayısıyla hiçbir referans
tutmuyor. Bu ölçümde Istek örneklerinin sayısı ayrıca sayılmıyor, çünkü ölçülen şey süre
ya da bellek değil — yalnız iki isteğin aynı nesneyi mi, ayrı nesneleri mi aldığı.
Sınırlayıcı Ölçüm: Tanınmayan Bir Kapsam Adı
YanlisKapsam sınıfı @Kap.Bilesen(kapsam = "tanimsiz-kapsam") taşıyor — kabın tanıdığı
tek değer "tekil", ama açıklama bağımsız değişkeni bir dizgi, ve derleyici bu dizginin
kabın anladığı bir değer olup olmadığını denetlemiyor. Sonuç al yönteminin boolean tekil = b.kapsam().equals("tekil"); satırında ortaya çıkıyor: "tanimsiz-kapsam".equals("tekil")
false döndürüyor, tekil değişkeni false oluyor, ve kod tekil denetimini hiç yapmadan
her seferinde yeni bir örnek kuruyor.
Object y1 = k2.al(YanlisKapsam.class), y2 = k2.al(YanlisKapsam.class);
Kap.kural("kapsam adi taninmiyor", y1 == y2 ? "etkisiz" : "sessiz");
y1 == y2 false çıkıyor — iki istek iki ayrı nesne. Bu, kural satırının "sessiz"
sınıfına düşmesinin nedeni: hiçbir istisna fırlamıyor, hiçbir günlük satırı “bilinmeyen
kapsam” demiyor, program sorunsuz çalışmaya devam ediyor. Yalnız beklenen davranış
(tekil kalmak) sessizce kayboluyor. Bu kusuru fark etmenin tek yolu, iki isteğin kimliğini
== ile karşılaştırmak — değer eşitliğine bakan bir denetim (iki nesnenin alanları aynı mı)
hiçbir şey yakalamaz, çünkü YanlisKapsam’ın hiç alanı yok, ikisi de “eşit” görünür. Kusur
ancak kimliğe bakan bir gözle görünüyor, ve bu ders boyunca kimlik hep == ile okundu,
hiçbir zaman bir kimlik karması basılmadı.
Bu, önceki dersin bağlanmamış arayüz gözlemiyle aynı ailede bir tasarım tercihi: kap orada
da bağımlılık grafiğini kurulurken taramıyor, yalnız istendiğinde denetliyordu. Burada da
kapsam adı Bilesen açıklamasına yazıldığı anda değil, al çağrıldığı anda okunuyor, ve
tanınan tek değer bir dizgi eşitliği ile karşılaştırılıyor — küçük bir yazım farkı ("tekil"
yerine "Tekil" ya da "tanimsiz-kapsam") derleyiciye hiç görünmeden geçiyor. İki ders de
aynı sonuca varıyor: kabın erken uyarı vermemesi bir eksiklik değil, bu modelin bilinçli
sınırı — doğrulama, kaynağı okumakla değil, kapsanan davranışı gerçekten koşturmakla
mümkün.
Özet
- Tekil kapsam kabın varsayılanı; kaynakta bunu belirten hiçbir sözcük olmasa bile
Bilesen’inkapsam()değeri devreye giriyor. - Bir kap bileşenini her istekte yeniden kurdurmak açık bir
kapsam = "her-istekte"bağımsız değişkeni gerektiriyor — bu, kaynakta gerçekten yazan bir sözcükten doğan bir davranış. - Kurulum yöntemi (yaşam döngüsü kancası) yapıcı ve ayar alanları tamamlandıktan sonra, tam
bir kez çağrılıyor; sırayı belirleyen
alyönteminin kendi kod sırası. - Elle kurulan bir nesne kurulum yöntemini hiç almıyor; alan hep başlangıç değerinde kalıyor.
- Tanınmayan bir kapsam adı hiçbir hata vermiyor — bileşen sessizce tekil olmaktan çıkıyor
ve bu ancak
==ile yapılan bir kimlik karşılaştırmasıyla görünüyor.
Sonraki Adım
hazirla() yöntemi baslangic alanına yazılmış değeri okuyarak durum alanını dolduruyor,
ve bu baslangic alanı da kendi başına bir açıklamayla besleniyor: @Kap.Ayar(ad = "depo.baslangic", varsayilan = "bos"). Bu ders o değerin nereden geldiğini hiç sormadı —
yalnız kurulum sırasında dolu olduğunu varsaydı. Sıradaki ders tam bu soruyu açar: bir kap
bileşeninin ayar değeri, kaynakta hiç görünmeyen bir öncelik sırasından hangi kaynağı
kazanıyor, ve bu sıra neden dışarıdan bakan biri için görünmez.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.