İçeriğe geç
academia.sh

Ders 17 / 34

Bağlantı Odaklı Yanıtlar

Yanıtın kendi içinde gezinme ve işlem bağlantıları taşıması: sayfaların yalnızca sonraki bağlantısı izlenerek dolaşılması, kaynağın durumuna göre açılıp kapanan işlem bağlantıları ve istemcinin adres kurma yükünün ölçülmesi.

İçindekiler

Dokuz ders boyunca istemcinin adresleri kendisi kurduğu varsayıldı. Koleksiyon adresini, sayfa imlecini, alan listesini, hatta yeni oluşturulan kaydın adresini istemci biliyordu. Bunun bir bedeli var: sözleşmenin biçimi istemcinin içine gömülür. İmleç parametresinin adı değişirse, iade işleminin hangi yöntemle yapıldığı değişirse ya da bir işlem koşula bağlanırsa, her istemci ayrı ayrı güncellenmek zorundadır.

Bu ders bağımlılığı gevşetir. Yanıt, istemcinin bir sonraki isteğinde kullanacağı adresleri kendi içinde taşır; istemci yalnızca giriş adresini bilir, gerisini okur. İkinci ve daha önemli sonuç şudur: kaynağın hangi işlemlere açık olduğu da yanıttan okunur. İade edilmiş bir ödünç kaydı artık iade bağlantısı sunmaz.

Bağlantının Yapısı

Bir bağlantı iki bilgiyi taşımalıdır: hangi adrese ve hangi yöntemle gidileceği. Yalnızca adres taşıyan bir bağlantı, okuma ile yazmayı ayırt edilemez kılar. İşlem bağlantıları için üçüncü bir bilgi daha gerekir: gövdede hangi alanların beklendiği.

Yanıtın hangi bağlantıları taşıyacağı kaynağın türüne göre değişir. Koleksiyon yanıtı gezinme bağlantıları taşır: kendisi ve varsa sonraki sayfa. Tekil kaynak yanıtı ise kendi adresini, bağlı kaynakların adreslerini ve o an uygulanabilir işlemleri taşır.

// baglanti-sunucusu.mjs — yanitlarin icinde gezinme ve islem baglantilari tasidigi servis
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(JSON.stringify(nesne));
};

// Kaydin o anki durumu, hangi islemlerin acik oldugunu belirler.
const kayitGosterimi = (o) => ({
  id: o.id, uye: o.uye, isbn: o.isbn, verilis: o.verilis, iade: o.iade,
  baglantilar: {
    kendisi: { yol: `/oduncler/${o.id}`, yontem: "GET" },
    uye:     { yol: `/uyeler/${o.uye}`, yontem: "GET" },
    ...(o.iade === null
      ? { iade: { yol: `/oduncler/${o.id}`, yontem: "PATCH", alanlar: ["iade"] } }
      : {}),
  },
});

const sunucu = createServer(async (istek, yanit) => {
  const adres = new URL(istek.url, "http://127.0.0.1");
  const tekil = /^\/oduncler\/(\d+)$/.exec(adres.pathname);
  const uye = /^\/uyeler\/([\w-]+)$/.exec(adres.pathname);

  if (istek.method === "GET" && uye) {
    const u = db.prepare("SELECT * FROM uye WHERE kod = ?").get(uye[1]);
    return u ? yanitla(yanit, 200, u) : yanitla(yanit, 404, { hata: "uye_yok" });
  }

  if (istek.method === "GET" && adres.pathname === "/oduncler") {
    const boyut = Math.min(Number(adres.searchParams.get("boyut") ?? 3), 50);
    const imlec = adres.searchParams.get("imlec");
    const satirlar = imlec === null
      ? db.prepare("SELECT * FROM odunc ORDER BY id LIMIT ?").all(boyut)
      : db.prepare("SELECT * FROM odunc WHERE id > ? ORDER BY id LIMIT ?").all(Number(imlec), boyut);
    const son = satirlar.at(-1);
    // Sonraki sayfa yalnizca gercekten varsa bildirilir.
    const kalan = son
      ? db.prepare("SELECT COUNT(*) AS n FROM odunc WHERE id > ?").get(son.id).n : 0;
    return yanitla(yanit, 200, {
      veri: satirlar.map(kayitGosterimi),
      baglantilar: {
        kendisi: { yol: `/oduncler?boyut=${boyut}`, yontem: "GET" },
        ...(kalan > 0
          ? { sonraki: { yol: `/oduncler?boyut=${boyut}&imlec=${son.id}`, yontem: "GET" } }
          : {}),
      },
    });
  }

  if (istek.method === "GET" && tekil) {
    const o = db.prepare("SELECT * FROM odunc WHERE id = ?").get(Number(tekil[1]));
    return o ? yanitla(yanit, 200, kayitGosterimi(o)) : yanitla(yanit, 404, { hata: "odunc_yok" });
  }

  if (istek.method === "PATCH" && tekil) {
    const g = await govdeOku(istek);
    const id = Number(tekil[1]);
    if ("iade" in g) db.prepare("UPDATE odunc SET iade = ? WHERE id = ?").run(g.iade, id);
    return yanitla(yanit, 200, kayitGosterimi(db.prepare("SELECT * FROM odunc WHERE id = ?").get(id)));
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8484, "127.0.0.1", () => console.log("baglanti sunucusu 127.0.0.1:8484"));

İki ayrıntı özellikle önemlidir. sonraki bağlantısı yalnızca gerçekten kalan kayıt varsa üretiliyor; istemcinin “boş sayfa gelirse dur” gibi bir kural yazmasına gerek kalmıyor. iade bağlantısı ise yalnızca kaydın iade alanı boşken üretiliyor; iş kuralı gösterimin içine yerleşmiş oluyor.

Yalnızca Giriş Adresini Bilen İstemci

Karşı taraftaki istemci tek bir adres yazıyor ve başka hiçbir adres kurmuyor.

// gezgin.mjs — yalnizca giris adresini bilen, geri kalanini yanittan okuyan istemci
const TABAN = "http://127.0.0.1:8484";
const GIRIS = "/oduncler?boyut=3";
const kurulanAdres = 1;   // yalnizca giris adresi istemcide yaziliyor

const cagir = async (baglanti, govde) => {
  const secenek = { method: baglanti.yontem };
  if (govde) {
    secenek.headers = { "content-type": "application/json" };
    secenek.body = JSON.stringify(govde);
  }
  return (await fetch(TABAN + baglanti.yol, secenek)).json();
};

// 1) Sayfalar yalnizca "sonraki" baglantisi izlenerek dolasilir.
let sayfa = await (await fetch(TABAN + GIRIS)).json();
let sayfaSayisi = 1;
const kayitlar = [];
while (true) {
  kayitlar.push(...sayfa.veri);
  console.log(`sayfa ${sayfaSayisi}: ${sayfa.veri.map((k) => k.id).join(", ")}` +
              `   sonraki: ${sayfa.baglantilar.sonraki?.yol ?? "yok"}`);
  if (!sayfa.baglantilar.sonraki) break;
  sayfa = await cagir(sayfa.baglantilar.sonraki);
  sayfaSayisi++;
}
console.log(`toplam ${kayitlar.length} kayit, ${sayfaSayisi} sayfa`);

// 2) Islem baglantisi acik olan ilk kayit bulunur ve o baglanti izlenir.
const acik = kayitlar.find((k) => k.baglantilar.iade);
console.log(`\nislem oncesi  id=${acik.id} baglantilar: ${Object.keys(acik.baglantilar).join(", ")}`);
const guncel = await cagir(acik.baglantilar.iade, { iade: "2026-03-15" });
console.log(`islem sonrasi id=${guncel.id} baglantilar: ${Object.keys(guncel.baglantilar).join(", ")}`);

// 3) Kaydin kendi adresi de yanittan okunur.
const yeniden = await cagir(guncel.baglantilar.kendisi);
console.log(`yeniden okuma id=${yeniden.id} iade=${yeniden.iade} ` +
            `baglantilar: ${Object.keys(yeniden.baglantilar).join(", ")}`);
console.log(`\nistemcide kurulan adres sayisi: ${kurulanAdres}`);
# Yedi odunc kaydi; istemci yalnizca giris adresini bilerek butun akisi yurutur.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql && sqlite3 kutuphane.db < katalog.sql
sqlite3 kutuphane.db <<'SQL'
DELETE FROM odunc;
INSERT INTO odunc (id, uye, isbn, verilis, iade) VALUES
  (1,'U-1001','K-01','2026-02-01','2026-02-14'),
  (2,'U-1002','K-02','2026-02-05','2026-02-19'),
  (3,'U-1001','K-03','2026-03-01',NULL),
  (4,'U-1002','K-04','2026-03-02',NULL),
  (5,'U-1001','K-05','2026-03-03',NULL),
  (6,'U-1002','K-06','2026-03-04',NULL),
  (7,'U-1001','K-07','2026-03-05',NULL);
SQL
node baglanti-sunucusu.mjs & sunucu=$!
sleep 0.4

node gezgin.mjs
echo "--- tek kaydin ham gosterimi ---"
curl -sS http://127.0.0.1:8484/oduncler/4; echo

kill $sunucu
baglanti sunucusu 127.0.0.1:8484
sayfa 1: 1, 2, 3   sonraki: /oduncler?boyut=3&imlec=3
sayfa 2: 4, 5, 6   sonraki: /oduncler?boyut=3&imlec=6
sayfa 3: 7   sonraki: yok
toplam 7 kayit, 3 sayfa

islem oncesi  id=3 baglantilar: kendisi, uye, iade
islem sonrasi id=3 baglantilar: kendisi, uye
yeniden okuma id=3 iade=2026-03-15 baglantilar: kendisi, uye

istemcide kurulan adres sayisi: 1
--- tek kaydin ham gosterimi ---
{"id":4,"uye":"U-1002","isbn":"K-04","verilis":"2026-03-02","iade":null,"baglantilar":{"kendisi":{"yol":"/oduncler/4","yontem":"GET"},"uye":{"yol":"/uyeler/U-1002","yontem":"GET"},"iade":{"yol":"/oduncler/4","yontem":"PATCH","alanlar":["iade"]}}}

Üç sayfa, yedi kayıt, tek bir yazılı adres. İstemcinin kodunda imlec sözcüğü hiç geçmiyor; imlecin nasıl kodlandığı, hangi parametre adıyla taşındığı, hatta sayfalamanın imleç tabanlı mı atlama tabanlı mı olduğu istemciyi ilgilendirmiyor. Sunucu bu kararı değiştirdiğinde istemci değişmez.

İkinci bölüm daha ilginçtir. İade işleminden önce kaydın bağlantıları arasında iade vardı; işlemden sonra kayboldu. İstemci “bu kayıt iade edilebilir mi” sorusunu bir alanın değerine bakıp kendi kuralıyla yanıtlamıyor; bağlantının varlığına bakıyor. Kural sunucuda duruyor ve tek yerde duruyor.

Bunun arayüz tarafındaki karşılığı doğrudandır: iade düğmesinin görünürlüğü, iade bağlantısının varlığına bağlanır. İş kuralı değiştiğinde — örneğin cezalı üyelerin iade işlemini şubede yapması gerektiğinde — sunucu bağlantıyı üretmez ve düğme kendiliğinden kaybolur.

Ham Gösterim ve Sözleşmenin Sınırı

Çıktının son satırı tek bir kaydın ham gösterimini veriyor. Bağlantılar gövdenin ayrı bir alanında toplanmış; kaydın kendi alanlarıyla karışmıyor. Bu ayrım korunmalıdır, yoksa iade adı hem bir tarih alanı hem de bir bağlantı adı olur.

Bağlantı odaklı tasarımın da bir maliyeti vardır ve abartılmamalıdır.

Gövde büyür. Yedi kayıtlık bir listede her kaydın üç bağlantı taşıması, gövdenin önemli bir bölümünü bağlantılara ayırır. Kısmi yanıt dersindeki ölçüt burada da geçerlidir: bağlantı bloğu alan seçimiyle kapatılabilmelidir.

İstemci bağımlılığı yok olmaz, yer değiştirir. İstemci artık adres biçimine değil, bağlantı adlarına bağımlıdır. sonraki adının değişmesi, imleç parametresinin adının değişmesi kadar kırıcıdır. Kazanç, bağlantı adlarının adres biçimlerinden daha az değişmesindedir.

Bağlantı üretmek sunucuya iş çıkarır. Yukarıdaki koleksiyon uç noktası, sonraki sayfanın var olup olmadığını anlamak için fazladan bir sayım sorgusu çalıştırıyor. Büyük koleksiyonlarda bu sayım pahalıdır; yaygın çözüm, sayfa boyutundan bir fazla kayıt çekip fazlalık gelirse sonraki sayfanın var olduğuna karar vermektir.

Son olarak, bağlantıların hepsi aynı düzeyde zorunlu değildir. Gezinme bağlantıları olmadan istemci sayfalama biçimini bilmek zorunda kalır; işlem bağlantıları olmadan iş kurallarını yeniden yazmak zorunda kalır. İlki neredeyse her serviste karşılığını verir; ikincisi, iş kurallarının karmaşık ve değişken olduğu servislerde değerlidir.

Özet

  • Bağlantı en az iki bilgi taşır: adres ve yöntem; işlem bağlantıları ayrıca gövdede beklenen alanları bildirir.
  • Koleksiyon yanıtı gezinme bağlantıları, tekil kaynak yanıtı bağlı kaynak ve işlem bağlantıları taşır; bağlantılar gövdenin ayrı bir alanında toplanır.
  • Ölçümde istemci tek bir adres yazarak üç sayfayı dolaştı ve yedi kaydı topladı; imleç parametresinin adı istemcinin kodunda hiç geçmedi.
  • İşlem bağlantıları kaynağın o anki durumundan üretilir: iade edilmiş kayıtta iade bağlantısı yoktur, dolayısıyla iş kuralı tek yerde durur.
  • Sonraki sayfa bağlantısı yalnızca kalan kayıt varken üretilir; istemcinin boş sayfayla durma kuralı yazmasına gerek kalmaz.
  • Maliyeti gövdenin büyümesi, sunucunun bağlantı üretme yükü ve bağımlılığın adres biçiminden bağlantı adlarına kaymasıdır.

Sonraki Adım

Kaynak ve sözleşme tasarımı tamamlandı: kaynaklar belirlendi, adresleri yerleşti, yöntemler ve durum kodları eşlendi, gövde biçimi kararlaştırıldı, koleksiyonlar sayfalandı ve süzüldü, yazma istekleri yinelenen etkiye karşı korundu, yanıtlar kendi gezinme bilgisini taşır oldu. Bütün bunlar işler yolunda gittiğinde geçerlidir. Hata durumunda ise her uç nokta kendi gövdesini uyduruyor: bir yerde hata alanı, başka yerde alan ve neden, bir başka yerde yalnız bir kod. Sonraki konu buradan başlar ve ilk dersi hata yanıtlarının tek bir yapıya oturtulmasıdır: standart bir problem ayrıntısı biçimi, hangi alanları taşır ve istemci onu nasıl kullanır.

İ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