İçeriğe geç
academia.sh

Ders 08 / 19

Denetleyiciler ve Yönlendirme

Dört uç noktanın dördü de kaynakta hiçbir çağrı olmadan yalnız bir açıklamayla bildiriliyor. Eşleşmeyen bir yol istendiğinde nesne kabı hata vermiyor, sabit bir varsayılan yanıt döndürüyor; aynı yolu bildiren iki bileşen tarandığında ise kazanan tarama sırasına bağlı kalıyor.

İçindekiler

Önceki ders bir isteğin kaptan geçerken hangi halkalardan geçtiğini ölçtü, ama halkaların sonunda isteğin hangi işi karşıladığı hiç belirlenmedi. Bu ders o boşluğu kapatıyor: bir uç noktanın kaynakta hangi çağrıyla değil hangi açıklamayla bildirildiğini, ve eşleşmeyen bir isteğe kabın ne yanıt verdiğini ölçüyor. Kaynak tasarımı, bir yolun hangi kaynağı ve hangi eylemi temsil ettiği M16/K02 API Tasarımı kursunda kavram olarak kuruldu ve burada tekrarlanmıyor; bu dersin sorduğu soru ondan ayrı — bir yolun kaynakta nasıl var olduğu.

Denetleyici burada da modellenir: gerçek bir HTTP yöntemi ayrıştırılmaz, gerçek bir istek gövdesi çözülmez. Bir “yol” yalnız bir dizgedir ("/urun/liste" gibi), ve bir uç nokta bu dizgeyi bir Java yöntemine bağlayan bir eşleştirmedir. Önceki dersteki halkalar isteği değiştiriyordu, yönlendirmiyordu; bu dersin denetleyicileri ise isteği hiç değiştirmeden doğru yönteme yönlendiriyor — kursun terim kararında routing karşılığı budur.

Bir denetleyici burada sıradan bir Java sınıfıdır; onu denetleyici yapan tek şey, yöntemlerinden bazılarının üzerinde @UcNokta açıklamasının bulunmasıdır. Sınıfın kendisi hiçbir arayüz uygulamıyor, hiçbir üst sınıftan türemiyor — kabın onu “denetleyici” olarak tanıması, sınıfın tipinden değil, tara çağrısına elle verilmesinden geliyor. Bu, kursun web katmanındaki ikinci modelleme kararı: gerçek çerçevelerde sınıf taraması genellikle sınıf yolunun tamamını dolaşır, burada ise tara’ya hangi nesnelerin denetleyici olarak değerlendirileceği açıkça bir dizi olarak veriliyor. Modelleme farkı sonucu değiştirmiyor — her iki durumda da kaynakta denetleyici sınıfını çağıran bir satır yok, yalnız üzerinde duran bir açıklama var.

Dört Yolun Dördü de Açıklamadan Doğuyor

  • WV6. @UcNokta yalnız bir yöntemin üzerine konur ve tek bir alan taşır: yol. Bu açıklamayı okuyan taraf, aşağıdaki tara yöntemidir.
  • WV7. Yönlendirme tablosu bir Map<String, Method> olarak tutulur; anahtar yol dizgesi, değer yansımayla çağrılacak yöntemdir. Gerçek bir istek gövdesi ayrıştırma ya da içerik türü pazarlığı burada yoktur.

İki denetleyici sınıfı, dördü de @UcNokta taşıyan dört yöntem tanımlıyor. Hiçbir yerde kap.ekle("/urun/liste", ...) biçiminde bir çağrı yok — önceki dersin çağrı ile eklenen halkalarından farklı olarak, burada yönlendirme tablosunun tamamı tara çağrısının sınıfların üzerindeki açıklamaları okumasıyla kuruluyor. Bu, önceki dersin karışık kaynak dağılımından (iki varsayılan, iki açıklama, iki çağrı) ayrılan bir uç: dört uç noktanın dördü de açıklamadan doğuyor, ne bir varsayılan uç nokta var ne de elle eklenen bir çağrı.

// Kap.java — uc noktalar aciklamadan doguyor, cakisma kayit sirasina bagli
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Kap {

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
    @interface UcNokta { String yol(); }

    final Map<String, Method> yollar = new LinkedHashMap<>();
    final Map<String, Object> sahibi = new LinkedHashMap<>();
    final List<String> kayitSirasi = new ArrayList<>();

    void tara(Object... denetleyiciler) {
        for (Object d : denetleyiciler) {
            for (Method m : d.getClass().getDeclaredMethods()) {
                UcNokta u = m.getAnnotation(UcNokta.class);
                if (u == null) continue;
                yollar.put(u.yol(), m);
                sahibi.put(u.yol(), d);
                kayitSirasi.add(u.yol() + " <- " + d.getClass().getSimpleName());
            }
        }
    }

    String istek(String yol) {
        Method m = yollar.get(yol);
        if (m == null) return "varsayilan-yanit: sayfa hazir degil";
        try {
            return (String) m.invoke(sahibi.get(yol));
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        UrunDenetleyici urun = new UrunDenetleyici();
        StokDenetleyici stok = new StokDenetleyici();

        Kap kap = new Kap();
        kap.tara(urun, stok);

        String[] dortYol = {"/urun/liste", "/urun/ekle", "/stok/durum", "/stok/uyari"};
        System.out.println("-- dort yolun tamami aciklamadan dogdu --");
        int aciklamadan = 0;
        for (String yol : dortYol) {
            String yanit = kap.istek(yol);
            System.out.printf("%-16s%s%n", yol, yanit);
            if (kap.yollar.containsKey(yol)) aciklamadan++;
        }
        System.out.println("aciklamadan dogan yol sayisi: " + aciklamadan + "/4");

        System.out.println("-- eslesmeyen yol --");
        System.out.println("/urun/sil       " + kap.istek("/urun/sil"));
        System.out.println("kusur turu: sessiz (istisna yok, sabit bir varsayilan yanit dondu)");

        System.out.println("-- cakisan uc nokta: iki bilesen ayni yolu bildiriyor --");
        YedekDenetleyici yedek = new YedekDenetleyici();

        Kap kapA = new Kap();
        kapA.tara(urun, yedek);
        System.out.println("tarama sirasi urun-yedek, /urun/liste yaniti: " + kapA.istek("/urun/liste"));

        Kap kapB = new Kap();
        kapB.tara(yedek, urun);
        System.out.println("tarama sirasi yedek-urun, /urun/liste yaniti: " + kapB.istek("/urun/liste"));

        System.out.println("iki tarama sirasi ayni yaniti mi veriyor: "
                + kapA.istek("/urun/liste").equals(kapB.istek("/urun/liste")));

        System.out.println("-- elle cagrilan denetleyici: kap kaydindan bagimsiz --");
        System.out.println("urun.liste() dogrudan cagrildi: " + urun.liste());
        System.out.println("kapA uzerinden /urun/liste (son kayit yedek): " + kapA.istek("/urun/liste"));
        System.out.println("elle cagri ile kap yaniti ayni mi: " + urun.liste().equals(kapA.istek("/urun/liste")));
    }
}

class UrunDenetleyici {
    @Kap.UcNokta(yol = "/urun/liste")
    public String liste() { return "3 urun"; }

    @Kap.UcNokta(yol = "/urun/ekle")
    public String ekle() { return "urun eklendi"; }
}

class StokDenetleyici {
    @Kap.UcNokta(yol = "/stok/durum")
    public String durum() { return "stok: 42"; }

    @Kap.UcNokta(yol = "/stok/uyari")
    public String uyari() { return "uyari yok"; }
}

class YedekDenetleyici {
    @Kap.UcNokta(yol = "/urun/liste")
    public String liste() { return "yedek liste"; }
}
-- dort yolun tamami aciklamadan dogdu --
/urun/liste     3 urun
/urun/ekle      urun eklendi
/stok/durum     stok: 42
/stok/uyari     uyari yok
aciklamadan dogan yol sayisi: 4/4
-- eslesmeyen yol --
/urun/sil       varsayilan-yanit: sayfa hazir degil
kusur turu: sessiz (istisna yok, sabit bir varsayilan yanit dondu)
-- cakisan uc nokta: iki bilesen ayni yolu bildiriyor --
tarama sirasi urun-yedek, /urun/liste yaniti: yedek liste
tarama sirasi yedek-urun, /urun/liste yaniti: 3 urun
iki tarama sirasi ayni yaniti mi veriyor: false
-- elle cagrilan denetleyici: kap kaydindan bagimsiz --
urun.liste() dogrudan cagrildi: 3 urun
kapA uzerinden /urun/liste (son kayit yedek): yedek liste
elle cagri ile kap yaniti ayni mi: false

Dört uç noktanın dördü de aciklamadan dogan yol sayisi: 4/4 satırında doğrulanıyor. Kaynağı okuyan biri UrunDenetleyici ve StokDenetleyici sınıflarına bakıp dört @UcNokta açıklaması görür, ama bu dört açıklamanın gerçekten bir yönlendirme tablosuna dönüştüğünü kaynaktan çıkaramaz — dönüşümü yapan tara yöntemi çağıran kodun hiç görmediği bir yerde duruyor. Bu, önceki dersle aynı okuma sorununu tekrar üretiyor: açıklama kaynakta bir işaret bırakıyor, ama işaretin gerçekten bir davranışa dönüştüğü yalnız kabın kendi kodunu okuyarak anlaşılıyor.

Bu noktada iki denetleyici sınıfının kendi başına hiçbir işlevi yok — UrunDenetleyici ile StokDenetleyici, kaba hiç tanıtılmadan da derlenir, örneklenebilir, yöntemleri çağrılabilir. Bir sınıfı “denetleyici” yapan şey onun kendi kodu değil, kabın onu tanıması. Bu, önceki derste @Halka açıklamasında da görülen kalıbın burada tekrarı: açıklama sınıfın üzerinde durduğu sürece bir istektir, isteği yerine getirecek bir okuyucu yoksa sınıf sıradan bir Java sınıfından farksız kalır. Fark şu: bu derste isteği yerine getiren okuyucu, sınıfı bir bileşen olarak kurmuyor, yalnız yöntemlerinden birini bir yol dizgesine bağlıyor. Aynı açıklama düzeneği, konuya göre tamamen farklı bir sonuç üretebiliyor — kap ne yapacağına, açıklamanın taşıdığı bilgiye ve kendi kuralına bakarak karar veriyor.

Eşleşmeyen Yol: Sessiz Bir Varsayılan

  • WV8. istek yöntemi eşleşme bulamadığında sabit bir dizge döndürüyor, hiçbir istisna fırlatmıyor. Bu sabit dizge normal bir yanıtla aynı tipte (String) olduğu için çağıran taraf onu ayırt etmek zorunda kalmadan bir “yanıt” alıyor.

/urun/sil hiçbir denetleyicide tanımlı değil, ve kap.istek("/urun/sil") çağrısı bir istisna fırlatmak yerine "varsayilan-yanit: sayfa hazir degil" döndürüyor. Bu, kursun dört kusur sınıfından sessiz olanının bir örneği: program çöküyor demiyor, bir hata mesajı basmıyor, yalnız beklenmeyen bir yanıtla devam ediyor. Bir geliştirici /urun/sill gibi bir yazım hatası yaparsa, kod hiçbir yerde patlamaz — yanıt gelir, biçimi doğrudur, ama içeriği hiçbir zaman gerçek bir uç noktanın ürettiği içerik değildir. Kusuru yakalayan tek şey, yanıtın içeriğini bilerek karşılaştırmaktır; yanıtın var olması hiçbir şey kanıtlamıyor.

Bu davranış bir tasarım kararı, bir eksiklik değil — kap eşleşmeyen her yol için aynı sabit yanıtı üretiyor ve bu tutarlı. Ama tutarlılık ile doğruluk aynı şey değil: eşleşmeyen bir yol her zaman aynı yanıtı üretmesi, o yanıtın çağıranın beklediği şey olduğu anlamına gelmiyor. Önceki derste kap eksik bir tip istendiğinde IllegalStateException fırlatıyordu — orada kap hiç bilmediği bir sınıfı kurmaya çalışıyordu ve bu gerçekten yürütülemez bir durumdu. Burada ise kap bir yol bulamadığında yürütülebilir bir durumdayken sessiz kalmayı seçiyor, çünkü “bu yol yok” bilgisini bir istisna yerine bir yanıt gövdesi olarak taşımak da kabın verdiği bir karar.

Bu kararın alternatifi kaptan değil, yöntemin imzasından da görülebilir: istek bir String yerine bir Optional<String> döndürseydi, çağıranın eşleşmeme durumunu kontrol etmesi zorunlu hâle gelirdi — tip sistemi eşleşmeyen yolu ayrı bir durum olarak taşırdı. Burada seçilen imza bunu yapmıyor; “bulundu” ile “bulunamadı” aynı tipte, aynı biçimde geri dönüyor. Sınırlayıcı ölçüm burada tam bu noktaya oturuyor: kusurun sessiz kalması kabın bir zayıflığı değil, dönüş tipinin eşleşme durumunu ayırt etmemesinin doğrudan sonucu. Bir yönlendirme kuralı yazan kişi bu ayrımı bilerek tasarlamak zorunda; aksi hâlde her yazım hatası, hatasız görünen ama yanlış bir yanıtla sonuçlanır.

Çakışan Uç Nokta: Kazanan Tarama Sırasına Bağlı

  • WV9. YedekDenetleyici, UrunDenetleyici ile aynı yolu (/urun/liste) bildiriyor. Kap, aynı anahtara ikinci kez yazılan girdiyi öncekinin üzerine yazıyor; hangi girdinin “ikinci” olduğu tara çağrısına verilen argüman sırasına bağlı.
  • WV10. Kap, çakışmayı denetlemiyor — ne bir uyarı basıyor ne bir istisna fırlatıyor. Kazananın kim olduğu yalnız iki kurulumu karşılaştırarak görülüyor.

kapA, urun ile yedeki bu sırayla tarıyor; kapB aynı iki denetleyiciyi ters sırayla. /urun/liste isteğine verilen yanıt iki kurulumda farklı: kapA’da yedek denetleyicinin yanıtı kazanıyor (son taranan o), kapB’de asıl denetleyicinin yanıtı kazanıyor (son taranan o). Kaynak metinde hiçbir “kazanan budur” ifadesi yok; kazananı belirleyen tek şey tara çağrısına verilen dizinin sırası.

Bu, önceki dersteki halka sıralamasıyla doğrudan karşıtlık kuruyor. Orada açıklamayla bulunan halkaların sırası oncelik alanına göre belirleniyordu ve tarama sırasından tamamen bağımsızdı — iki farklı yazım aynı sonucu üretiyordu. Burada aynı tara deseni kullanılıyor, ama yönlendirme tablosu bir Map üzerine düz bir put ile kuruluyor ve hiçbir eşitleyici kural yok. Fark, mekanizmanın kendisinde değil — ikisi de açıklama okuyan bir yansıma taramasıdır — kabın çakışmayı çözecek açık bir kuralı olup olmamasında. Halka sıralamasında oncelik alanı bu kuralı sağlıyordu; yönlendirme tablosunda böyle bir alan yok, bu yüzden çakışma kaynaktaki hiçbir ifadeye değil, tarama sırasına düşüyor. Bir açıklamanın sıralamadan bağımsız olması otomatik gelmiyor; kabın o sıralamayı çözecek bir kuralı olması gerekiyor, ve bu kural yoksa kazananı belirleyen şey kaynaktaki en son satır değil, kabın kendi kayıt sırasıdır.

Elle Çağrılan Denetleyici Kap Kaydından Bağımsızdır

urun.liste() doğrudan çağrıldığında her zaman "3 urun" döner — bu, UrunDenetleyici sınıfının kendi yönteminin davranışı, ve hiçbir kap kaydına bağlı değil. kapA üzerinden /urun/liste isteği ise "yedek liste" döndürüyor, çünkü o kurulumda son taranan YedekDenetleyici oldu. İki yanıt farklı, ve bu fark tam olarak dersin başındaki ayrımı gösteriyor: urun nesnesinin kendi yöntemleri her zaman kendi mantığını çalıştırır, kap üzerinden gelen bir yanıt ise kabın o anki kayıt durumuna bağlıdır. UrunDenetleyici sınıfının kendisi hiç değişmedi, liste() yöntemi hâlâ aynı dizgeyi döndürüyor — değişen şey kabın hangi yöntemi /urun/liste yoluna bağladığı. Bir denetleyici sınıfını birim testinde elle çağırmak bu yüzden yalnız o sınıfın kendi mantığını doğruluyor; o sınıfın hangi yola bağlı olduğunu, ve o yolun başka bir bileşenle çakışıp çakışmadığını doğrulamıyor. İki soru ayrı: yöntem doğru mu, ve kap onu doğru yola mı bağladı — birincisini elle çağrı yanıtlıyor, ikincisini yalnız kabın kendisi çalıştırılarak yanıtlanabiliyor.

Bu ayrımın kapsamı ilk derste görülenden biraz daha geniş: orada elle çağrı yalnız varsayılan halkaları (istek-kimligi, hata-yakalama) kaçırıyordu, çünkü o davranış kabın kendi kodunun bir parçasıydı. Burada kaçırılan şey bir varsayılan değil, kabın o an tuttuğu kayıt durumu — hangi bileşenin hangi yolu son taradığı. Elle çağrı bu durumu hiç görmüyor çünkü elle çağrı zaten kabı hiç sormuyor; nesnenin kendi belleğinde ne varsa onu çalıştırıyor. İki tür bağımlılık da aynı kökten geliyor — kabın araya girip iş üzerinde bir karar vermesi — ama biri kabın sabit davranışı, öteki kabın o an içinde bulunduğu duruma göre değişen bir davranış.

Özet

  • Dört uç noktanın dördü de @UcNokta açıklamasından doğdu; yönlendirme tablosunu kuran hiçbir çağrı kaynakta yok, tabloyu tara yönteminin okuduğu açıklamalar kuruyor.
  • Eşleşmeyen bir yol istendiğinde kap istisna fırlatmadı, sabit bir varsayılan yanıt döndürdü; bu, kursun sessiz kusur sınıfının bir örneği.
  • İki bileşen aynı yolu bildirdiğinde kap çakışmayı denetlemedi; kazanan, tarama çağrısına verilen sınıf sırasına göre değişti.
  • Bu, önceki dersteki halka sıralamasından farklı: orada açıklama sırası kabın kendi kuralıyla (öncelik) sabitti, burada yol çakışmasını çözecek bir kural olmadığı için sonuç kayıt sırasına bağlı kaldı.
  • Bir denetleyici sınıfını elle çağırmak yalnız o sınıfın kendi mantığını sınar; hangi yola bağlı olduğunu ve bir çakışmada kazanıp kazanmadığını sınamaz.

Sonraki Adım

Bu ders bir isteğin hangi yönteme ulaştığını ölçtü, ama o yönteme ulaşan verinin doğru biçimde olup olmadığı hiç sorulmadı. Sıradaki ders bunu ele alıyor: bir alanın doğrulama kuralının kaynakta bir çağrı mı yoksa bir açıklama mı olduğunu, ve kural bozulduğunda ortak hata yanıtının hangi alanları nereden topladığını ölçüyor.

İ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