İçeriğe geç
academia.sh

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 tekil değeri için tekiller haritası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 (Kur açı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 Kur iş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’in kapsam() 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 al yö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat