İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat