Ders 18 / 21
Veri Aktarım Nesneleri
Dış sözleşmenin iç modelden ayrılması: aynı alan nesnesinden iki farklı görünüm üretilmesi, iç modele alan eklendiğinde dış sözleşmenin değişmediğinin sınanması, alan nesnesini doğrudan döndürmenin sızdırdığı alanların sayılması ve gelen yönde aşırı atama.
İçindekiler
Önceki dersin düzeltilmiş sürümünde alan katmanı { sonuc, sebep } biçiminde bir nesne
döndürdü ve sunum katmanı bunu HTTP gövdesine çevirdi. Çeviri üç alanlıydı, neredeyse
birebirdi; ara katman gereksiz bir el değiştirme gibi görünüyordu.
Kayıt büyüdüğünde soru keskinleşir. Ödünç kaydının dışarıya dönen biçimi ile alan modelinin kendisi aynı şey midir? Alan modeline yeni bir alan eklendiğinde istemcinin gördüğü gövde değişmeli midir? Bu dersin konusu, iki tarafın arasına konan veri aktarım nesnesi (data transfer object): dışarıyla konuşmak için üretilen, alan davranışı taşımayan düz kayıt.
Aynı Kayıt, İki Sözleşme
Ödünç kaydının alan modeli veriyi ve o veriden türeyen kararları taşır. Aşağıdaki sınıf on bir alan tutar; ikisi türetilmiş bilgi üretir. Dersin dosyaları iki dizine yerleşir.
mkdir -p alan sunum
// alan/odunc.mjs — odunc kaydinin alan modeli export class Odunc { constructor(v) { this.oduncId = v.oduncId; this.kitapId = v.kitapId; this.kitapBaslik = v.kitapBaslik; this.uyeId = v.uyeId; this.uyeAd = v.uyeAd; this.uyeEposta = v.uyeEposta; this.alisTarihi = v.alisTarihi; this.sonTeslimTarihi = v.sonTeslimTarihi; this.iadeTarihi = v.iadeTarihi; this.personelNotu = v.personelNotu; this.kayitSurumu = v.kayitSurumu; } get acikMi() { return this.iadeTarihi === null; } gecikmeGunu(bugun) { const bitis = this.iadeTarihi ?? bugun; const fark = (Date.parse(bitis) - Date.parse(this.sonTeslimTarihi)) / 86400000; return Math.max(0, Math.round(fark)); } durum(bugun) { if (!this.acikMi) return "iade_edildi"; return this.gecikmeGunu(bugun) > 0 ? "gecikmis" : "acik"; } }
Üç alan dışarıya ait değildir. uyeEposta kişisel veridir ve yalnız belirli rollere
gösterilir; personelNotu iç yazışmadır; kayitSurumu İyimser ve Kötümser Kilitleme
dersindeki sürüm sütununun kendisidir, yani bir kalıcılık ayrıntısıdır.
Dışarısı ise tek bir biçim istemez. Liste ekranı satır başına dört alanla yetinir; ayrıntı ekranı ilişkileri de görmek ister.
// sunum/gorunumler.mjs — ayni alan nesnesinden iki farkli dis sozlesme export const listeGorunumu = (o, bugun) => ({ oduncId: o.oduncId, kitap: o.kitapBaslik, sonTeslimTarihi: o.sonTeslimTarihi, durum: o.durum(bugun), }); export const ayrintiGorunumu = (o, bugun) => ({ oduncId: o.oduncId, kitap: { kitapId: o.kitapId, baslik: o.kitapBaslik }, uye: { uyeId: o.uyeId, ad: o.uyeAd }, alisTarihi: o.alisTarihi, sonTeslimTarihi: o.sonTeslimTarihi, iadeTarihi: o.iadeTarihi, durum: o.durum(bugun), gecikmeGunu: o.gecikmeGunu(bugun), });
Eşleme iki iş yapıyor: iç alanları dışarıda bırakıyor ve türetilmiş alanları ekliyor.
İkincisi çoğu zaman gözden kaçar. durum ile gecikmeGunu alan modelinde saklanan değerler
değil, hesaplanan değerlerdir; istemcinin bunları kendisi hesaplaması, iş kuralının
istemciye kopyalanması demektir.
// gorunum.mjs — tek alan nesnesi, iki dis sozlesme import { Odunc } from "./alan/odunc.mjs"; import { listeGorunumu, ayrintiGorunumu } from "./sunum/gorunumler.mjs"; const BUGUN = "2025-06-20"; const kayit = new Odunc({ oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir", uyeEposta: "[email protected]", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15", iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3, }); console.log("liste :", JSON.stringify(listeGorunumu(kayit, BUGUN))); console.log("ayrinti:", JSON.stringify(ayrintiGorunumu(kayit, BUGUN)));
node gorunum.mjs
liste : {"oduncId":7,"kitap":"Körlük","sonTeslimTarihi":"2025-06-15","durum":"gecikmis"}
ayrinti: {"oduncId":7,"kitap":{"kitapId":5,"baslik":"Körlük"},"uye":{"uyeId":2,"ad":"Ayşe Demir"},"alisTarihi":"2025-06-01","sonTeslimTarihi":"2025-06-15","iadeTarihi":null,"durum":"gecikmis","gecikmeGunu":5}
Aynı nesne, iki gövde. Liste gövdesinde ilişkiler düzleştirildi, ayrıntı gövdesinde gruplandı. Bu bir biçim tercihi değil, iki farklı sözleşmedir: liste ekranı değişse ayrıntı sözleşmesi etkilenmez.
Sözleşmenin İç Değişimden Yalıtılması
Ayrımın asıl gerekçesi buradadır. Alan modeline yeni bir alan eklendiğinde ne olur? Aşağıdaki dosya, aynı modelin iki iç alan eklendikten sonraki hâlidir.
// alan/odunc-2.mjs — ayni modelin iki ic alan eklendikten sonraki hali export class Odunc { constructor(v) { this.oduncId = v.oduncId; this.kitapId = v.kitapId; this.kitapBaslik = v.kitapBaslik; this.uyeId = v.uyeId; this.uyeAd = v.uyeAd; this.uyeEposta = v.uyeEposta; this.alisTarihi = v.alisTarihi; this.sonTeslimTarihi = v.sonTeslimTarihi; this.iadeTarihi = v.iadeTarihi; this.personelNotu = v.personelNotu; this.kayitSurumu = v.kayitSurumu; this.yenilemeSayisi = v.yenilemeSayisi ?? 0; // yeni this.sonHatirlatmaTarihi = v.sonHatirlatmaTarihi ?? null; // yeni } get acikMi() { return this.iadeTarihi === null; } gecikmeGunu(bugun) { const bitis = this.iadeTarihi ?? bugun; const fark = (Date.parse(bitis) - Date.parse(this.sonTeslimTarihi)) / 86400000; return Math.max(0, Math.round(fark)); } durum(bugun) { if (!this.acikMi) return "iade_edildi"; return this.gecikmeGunu(bugun) > 0 ? "gecikmis" : "acik"; } }
Sınama, iki sürümün ürettiği gövdeleri karşılaştırır.
// gorunum.test.mjs — dis sozlesmenin ic model degisiminden etkilenmedigini sinar import { test } from "node:test"; import assert from "node:assert/strict"; import { Odunc as Odunc1 } from "./alan/odunc.mjs"; import { Odunc as Odunc2 } from "./alan/odunc-2.mjs"; import { listeGorunumu, ayrintiGorunumu } from "./sunum/gorunumler.mjs"; const BUGUN = "2025-06-20"; const TEMEL = { oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir", uyeEposta: "[email protected]", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15", iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3, }; // Surum 2 ayni kayda iki ic alan ekler. const TEMEL2 = { ...TEMEL, yenilemeSayisi: 2, sonHatirlatmaTarihi: "2025-06-18" }; const LISTE_SOZLESMESI = ["oduncId", "kitap", "sonTeslimTarihi", "durum"]; const AYRINTI_SOZLESMESI = ["oduncId", "kitap", "uye", "alisTarihi", "sonTeslimTarihi", "iadeTarihi", "durum", "gecikmeGunu"]; const IC_ALANLAR = ["uyeEposta", "personelNotu", "kayitSurumu", "yenilemeSayisi", "sonHatirlatmaTarihi"]; test("liste gorunumu sozlesmedeki alanlari uretir", () => { assert.deepEqual(Object.keys(listeGorunumu(new Odunc1(TEMEL), BUGUN)), LISTE_SOZLESMESI); }); test("ayrinti gorunumu sozlesmedeki alanlari uretir", () => { assert.deepEqual(Object.keys(ayrintiGorunumu(new Odunc1(TEMEL), BUGUN)), AYRINTI_SOZLESMESI); }); test("ic alanlar hicbir gorunumde gecmiyor", () => { const metin = JSON.stringify(listeGorunumu(new Odunc2(TEMEL2), BUGUN)) + JSON.stringify(ayrintiGorunumu(new Odunc2(TEMEL2), BUGUN)); for (const alan of IC_ALANLAR) assert.equal(metin.includes(alan), false, alan); }); test("ic modele alan eklenince liste gorunumu birebir ayni kalir", () => { assert.deepEqual(listeGorunumu(new Odunc2(TEMEL2), BUGUN), listeGorunumu(new Odunc1(TEMEL), BUGUN)); }); test("ic modele alan eklenince ayrinti gorunumu birebir ayni kalir", () => { assert.deepEqual(ayrintiGorunumu(new Odunc2(TEMEL2), BUGUN), ayrintiGorunumu(new Odunc1(TEMEL), BUGUN)); });
node --test gorunum.test.mjs
✔ liste gorunumu sozlesmedeki alanlari uretir (0.556ms) ✔ ayrinti gorunumu sozlesmedeki alanlari uretir (0.068083ms) ✔ ic alanlar hicbir gorunumde gecmiyor (1.237958ms) ✔ ic modele alan eklenince liste gorunumu birebir ayni kalir (0.062917ms) ✔ ic modele alan eklenince ayrinti gorunumu birebir ayni kalir (0.064041ms) ℹ tests 5 ℹ suites 0 ℹ pass 5 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 35.5965
Parantez içindeki süreler her çalıştırmada değişir; anlamlı olan pass 5 satırıdır. İki
sürüm arasında dış gövdeler deepEqual ile birebir eşit çıktı. İç model değişti, dış
sözleşme değişmedi. Bu, Web API Tasarımı kursundaki geriye dönük uyumluluk kuralının
uygulama içindeki karşılığıdır: sözleşmenin değişmesi bir karar olmalıdır, bir yan etki
değil.
Doğrudan Dönmenin Bedeli
Ters yön ölçülebilir. Uç nokta alan nesnesini doğrudan döndürürse dış gövde ile sözleşme arasında iki yönlü bir fark oluşur.
// sizinti-say.mjs — alan nesnesi dogrudan donduruldugunde dis sozlesme ne oluyor import { Odunc as Odunc1 } from "./alan/odunc.mjs"; import { Odunc as Odunc2 } from "./alan/odunc-2.mjs"; import { listeGorunumu } from "./sunum/gorunumler.mjs"; const BUGUN = "2025-06-20"; const TEMEL = { oduncId: 7, kitapId: 5, kitapBaslik: "Körlük", uyeId: 2, uyeAd: "Ayşe Demir", uyeEposta: "[email protected]", alisTarihi: "2025-06-01", sonTeslimTarihi: "2025-06-15", iadeTarihi: null, personelNotu: "telefonla arandi", kayitSurumu: 3, }; const TEMEL2 = { ...TEMEL, yenilemeSayisi: 2, sonHatirlatmaTarihi: "2025-06-18" }; // Istemcinin gordugu gövde, nesnenin JSON'a cevrilmis halidir. const disAlanlar = (nesne) => Object.keys(JSON.parse(JSON.stringify(nesne))).sort(); const fark = (a, b) => a.filter((x) => !b.includes(x)); for (const [ad, kayit] of [["surum 1", new Odunc1(TEMEL)], ["surum 2", new Odunc2(TEMEL2)]]) { const eslemeli = disAlanlar(listeGorunumu(kayit, BUGUN)); const dogrudan = disAlanlar(kayit); console.log(`${ad} eslemeli=${eslemeli.length} alan dogrudan=${dogrudan.length} alan`); console.log(` sizan (${fark(dogrudan, eslemeli).length}): ${fark(dogrudan, eslemeli).join(", ")}`); console.log(` eksilen (${fark(eslemeli, dogrudan).length}): ${fark(eslemeli, dogrudan).join(", ")}`); }
node sizinti-say.mjs
surum 1 eslemeli=4 alan dogrudan=11 alan sizan (9): alisTarihi, iadeTarihi, kayitSurumu, kitapBaslik, kitapId, personelNotu, uyeAd, uyeEposta, uyeId eksilen (2): durum, kitap surum 2 eslemeli=4 alan dogrudan=13 alan sizan (11): alisTarihi, iadeTarihi, kayitSurumu, kitapBaslik, kitapId, personelNotu, sonHatirlatmaTarihi, uyeAd, uyeEposta, uyeId, yenilemeSayisi eksilen (2): durum, kitap
Üç bulgu var. Birincisi sızıntının kendisi: liste sözleşmesinde dört alan varken doğrudan
dönen gövdede on bir alan çıktı, dokuzu sözleşmede yok. İçlerinden uyeEposta kişisel veri,
personelNotu iç yazışma, kayitSurumu kalıcılık ayrıntısıdır.
İkincisi büyümenin kendiliğinden olması: iki iç alan eklenince doğrudan dönen gövde on üç alana çıktı, eşlemeli gövde dört alanda kaldı. Doğrudan dönen uçta her model değişikliği bir sözleşme değişikliğidir ve bunun için kimse karar vermemiştir.
Üçüncüsü ters yöndeki kayıptır. Doğrudan dönen gövdede durum ve kitap alanları yok,
çünkü bunlar saklanan değil türetilen alanlardır. Aşırı Veri Çekme dersinde ölçülen fazla
bayt burada ikinci bir maliyet kazanıyor: gövde hem gereğinden büyük hem de istemcinin
ihtiyaç duyduğu bilgi bakımından eksik.
Gelen Yön
Aynı sınır ters yönde de gereklidir. İstek gövdesi doğrudan iç modele atanırsa istemci yazmaması gereken alanları belirler. Aşağıdaki karşılaştırma bunu sayıyor.
// gelen-yon.mjs — istek govdesi ic modele dogrudan atandiginda istemci neyi belirliyor const VARSAYILAN = { oduncId: null, kitapId: null, kitapBaslik: null, uyeId: null, uyeAd: null, uyeEposta: null, alisTarihi: null, sonTeslimTarihi: null, iadeTarihi: null, personelNotu: null, kayitSurumu: 1, }; // Istemcinin yolladigi govde: uc mesru alan ve uc fazlalik. const ISTEK = { kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", oduncId: 1, kayitSurumu: 99, personelNotu: "kaydi kapali gorun", }; const korumasiz = (istek) => ({ ...VARSAYILAN, ...istek }); const esleyerek = (istek) => ({ ...VARSAYILAN, kitapId: istek.kitapId, uyeId: istek.uyeId, alisTarihi: istek.alisTarihi, }); const istemcininBelirledigi = (nesne) => Object.keys(nesne).filter((a) => JSON.stringify(nesne[a]) !== JSON.stringify(VARSAYILAN[a])); for (const [ad, kur] of [["korumasiz", korumasiz], ["esleyerek", esleyerek]]) { const alanlar = istemcininBelirledigi(kur(ISTEK)); console.log(`${ad.padEnd(10)} istemcinin belirledigi alan = ${alanlar.length}: ${alanlar.join(", ")}`); }
node gelen-yon.mjs
korumasiz istemcinin belirledigi alan = 6: oduncId, kitapId, uyeId, alisTarihi, personelNotu, kayitSurumu esleyerek istemcinin belirledigi alan = 3: kitapId, uyeId, alisTarihi
Korumasız sürümde istemci kaydın kimliğini, sürüm sayacını ve personel notunu belirledi. Sürüm sayacının istemciden gelmesi iyimser kilitlemeyi işlevsiz kılar: çatışma denetimi, sunucunun bildiği sürüm numarasına dayanır. Bu davranışa aşırı atama (mass assignment) denir ve gövdeye alan eklendikçe sessizce genişler.
Eşlemeli sürümde istek gövdesi bir veri aktarım nesnesidir; adı geçmeyen alan iç modele ulaşamaz. Sorgu Nesneleri ve Belirtimler dersindeki tanıtıcı izin listesiyle aynı ilkedir: dışarıdan gelen adın iç yapıya çevrilmesi sabit bir listeye dayanır.
Eşlemenin Bedeli ve Sınırı
Ayrım bedelsiz değildir. Her görünüm ek kod, ek sınama ve model değiştiğinde ek bakım demektir. Alan sayısı arttıkça eşleme işlevleri de uzar ve bir kısmı alan adını olduğu gibi kopyalar.
Bedelin karşılığı üç durumda net olarak alınır: dış sözleşmenin iç modelden farklı bir hızda değişmesi gerekiyorsa, iç modelde dışarıya gösterilmeyecek alan varsa ve aynı kayıttan birden çok görünüm üretiliyorsa. Kimlik Doğrulama ve Yetkilendirme kursundaki rol ayrımı üçüncü maddeyi doğrudan besler: aynı ödünç kaydı üyeye, personele ve rapor arayüzüne farklı alan kümeleriyle görünür; bu farkı taşıyacak yer görünüm işlevleridir.
Karşı durum da vardır. İç modeli olmayan, tabloyu okuyup listeleyen bir yönetim ekranında ayrı bir aktarım nesnesi katmanı yalnızca kopya üretir. Ölçüt şudur: eşleme işlevi hiçbir alanı gizlemiyor, hiçbir alan türetmiyor ve hiçbir adı değiştirmiyorsa o eşleme henüz bir sözleşme taşımıyordur.
Özet
- Veri aktarım nesnesi dışarıyla konuşmak için üretilen düz kayıttır; alan modeli ise veriyi ve o veriden türeyen kararları taşır.
- Aynı alan nesnesinden liste ve ayrıntı olmak üzere iki ayrı sözleşme üretildi; eşleme hem iç alanları gizledi hem türetilmiş alanları ekledi.
- İç modele iki alan eklendiğinde iki görünüm de birebir aynı gövdeyi üretti; beş sınamanın tamamı geçti.
- Alan nesnesi doğrudan döndürüldüğünde liste sözleşmesine göre dokuz alan sızdı, iki türetilmiş alan eksildi; model büyüyünce sızan alan sayısı on bire çıktı.
- Gelen yönde istek gövdesinin iç modele doğrudan atanması istemcinin altı alanı belirlemesine yol açtı; adları sayılan bir eşlemede bu sayı üçe indi.
Sonraki Adım
Bu dersin gelen yön örneğinde istek gövdesindeki alanlar hiç denetlenmeden kabul edildi:
kitapId bir sayı mı, alisTarihi geçerli bir tarih mi, üye gerçekten var mı, o üye şu
anda ödünç almaya uygun mu? Bu soruların hepsi “doğrulama” başlığı altında toplanır ama
aynı yere ait değildir. Biri gövdenin biçimiyle ilgilidir ve veriye bakmadan yanıtlanır;
öteki kurumun kuralıdır ve veritabanına bakmadan yanıtlanamaz. Sonraki ders aynı hatalı
girdiyi iki katmanda çalıştırır, ürettikleri yanıtların farkını gösterir ve alan kuralının
neden girdi doğrulamasına indirgenemediğini ortaya koyar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.