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.
@UcNoktayalnız bir yöntemin üzerine konur ve tek bir alan taşır:yol. Bu açıklamayı okuyan taraf, aşağıdakitarayö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.
istekyö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,UrunDenetleyiciile aynı yolu (/urun/liste) bildiriyor. Kap, aynı anahtara ikinci kez yazılan girdiyi öncekinin üzerine yazıyor; hangi girdinin “ikinci” olduğutaraç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
@UcNoktaaçıklamasından doğdu; yönlendirme tablosunu kuran hiçbir çağrı kaynakta yok, tabloyutarayö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.