İçeriğe geç
academia.sh

Ders 03 / 21

Depo Kalıbı

Kalıcılık ayrıntısını tek bir arayüzün arkasına toplayan depo kalıbı: iş kurallarının sahte depoyla veritabanı olmadan sınanması, aynı sözleşmenin iki uygulamaya uygulanması ve kalıcılık sızıntısının modül başına ölçülmesi.

İçindekiler

Önceki ders nesne modeliyle ilişkisel modelin dört noktada örtüşmediğini gösterdi ve her uyumsuzluğu kapatan kararı adlandırdı. Bu kararlar bir yere yazılmadıkça sorguyu yazan her çağrı noktasında yeniden verilir: kalıtımın hangi stratejiyle indirildiği, ilişkinin sahibinin hangi taraf olduğu, hangi alanın gömüldüğü bilgisi kodun içine dağılır.

Bu dersin konusu o bilgiyi tek bir yerde toplayan arayüzdür. Depo (repository), çağıran tarafa yalnız alan kavramlarıyla konuşan, kalıcılığın nasıl gerçekleştiğini gizleyen bir sınırdır. Sınırın gerçekten kapalı olup olmadığı iddia edilerek değil, sınanarak gösterilir: iş kuralları veritabanı olmadan çalışıyorsa sınır kapalıdır.

Deponun Vaadi

Depo, sorgu yazma aracı değildir. Çağıran taraf ondan tablo, sütun veya birleştirme istemez; ödünç kaydı ister. Aşağıdaki beş işlem ödünç servisinin veriye ihtiyaç duyduğu noktaların tamamıdır.

İşlem Anlamı
bul(oduncId) Kimliği verilen ödünç kaydını döndürür, yoksa null
kitabinAcikKaydi(kitapId) Kitabın iade edilmemiş kaydı varsa döndürür
uyeninAcikKayitlari(uyeId) Üyenin iade edilmemiş kayıtlarının listesi
ekle(kayit) Yeni kaydı saklar ve kimliğini döndürür
iadeIsaretle(oduncId, tarih) Kaydı kapalı duruma geçirir

Listede SELECT yok, bağlantı yok, satır yok. Bu, deponun tanımıdır: arayüz alan diliyle yazılır, uygulama kalıcılık diliyle.

Kurallar Depoya Bağlı Değildir

Ödünç verme iki kural taşır: ödünçte olan kitap yeniden verilemez ve bir üye aynı anda üçten çok kitap tutamaz. Bu kurallar hiçbir kalıcılık ayrıntısı içermez.

// odunc-servisi.mjs — is kurallari; hicbir SQL, hicbir surucu adi icermez
export const UYE_SINIRI = 3;

export class OduncServisi {
  constructor(depo) { this.depo = depo; }

  ver(uyeId, kitapId, tarih) {
    if (this.depo.kitabinAcikKaydi(kitapId) !== null) {
      return { durum: "reddedildi", sebep: "kitap zaten odunc verilmis" };
    }
    const acik = this.depo.uyeninAcikKayitlari(uyeId);
    if (acik.length >= UYE_SINIRI) {
      return { durum: "reddedildi", sebep: `uye siniri asildi (${UYE_SINIRI})` };
    }
    const id = this.depo.ekle({ kitapId, uyeId, alisTarihi: tarih, iadeTarihi: null });
    return { durum: "verildi", oduncId: id };
  }

  iadeAl(oduncId, tarih) {
    const kayit = this.depo.bul(oduncId);
    if (kayit === null) return { durum: "bulunamadi" };
    if (kayit.iadeTarihi !== null) return { durum: "zaten iade edilmis" };
    this.depo.iadeIsaretle(oduncId, tarih);
    return { durum: "iade alindi" };
  }
}

Servis, deposunu yapıcı üzerinden alır. Kendisi bir depo kurmaz, hangi deponun geleceğini bilmez. Bu ayrıntı bütün dersi taşır: depoyu dışarıdan almak, sınama sırasında başka bir depo vermeyi mümkün kılar.

Sahte Depo

Sözleşmeyi karşılayan en küçük uygulama bir dizidir.

// bellek-deposu.mjs — deponun sahte uygulamasi; veritabani yok
export class BellekOduncDeposu {
  constructor(baslangic = []) {
    this.kayitlar = baslangic.map((k) => ({ ...k }));
    this.sonrakiId = this.kayitlar.length + 1;
  }
  bul(oduncId) {
    return this.kayitlar.find((k) => k.oduncId === oduncId) ?? null;
  }
  kitabinAcikKaydi(kitapId) {
    return this.kayitlar.find((k) => k.kitapId === kitapId && k.iadeTarihi === null) ?? null;
  }
  uyeninAcikKayitlari(uyeId) {
    return this.kayitlar.filter((k) => k.uyeId === uyeId && k.iadeTarihi === null);
  }
  ekle(kayit) {
    const oduncId = this.sonrakiId++;
    this.kayitlar.push({ oduncId, ...kayit });
    return oduncId;
  }
  iadeIsaretle(oduncId, tarih) {
    const k = this.bul(oduncId);
    if (k !== null) k.iadeTarihi = tarih;
  }
}

Sahte depoyla iş kuralları doğrudan sınanabilir. Sınama dosyası önceki iki bloğun tanımladığı odunc-servisi.mjs ve bellek-deposu.mjs modüllerini kullanır.

// servis.test.mjs — is kurallari sahte depoyla sinanir
import { test } from "node:test";
import assert from "node:assert/strict";
import { OduncServisi } from "./odunc-servisi.mjs";
import { BellekOduncDeposu } from "./bellek-deposu.mjs";

test("odunte olmayan kitap verilir", () => {
  const servis = new OduncServisi(new BellekOduncDeposu());
  assert.deepEqual(servis.ver(1, 5, "2025-07-01"), { durum: "verildi", oduncId: 1 });
});

test("odunte olan kitap reddedilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  ]);
  const sonuc = new OduncServisi(depo).ver(1, 5, "2025-07-01");
  assert.equal(sonuc.durum, "reddedildi");
  assert.match(sonuc.sebep, /zaten odunc/);
});

test("uye siniri asilinca reddedilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 1, uyeId: 4, alisTarihi: "2025-06-01", iadeTarihi: null },
    { oduncId: 2, kitapId: 2, uyeId: 4, alisTarihi: "2025-06-02", iadeTarihi: null },
    { oduncId: 3, kitapId: 3, uyeId: 4, alisTarihi: "2025-06-03", iadeTarihi: null },
  ]);
  const sonuc = new OduncServisi(depo).ver(4, 7, "2025-07-01");
  assert.equal(sonuc.durum, "reddedildi");
  assert.equal(sonuc.sebep, "uye siniri asildi (3)");
});

test("iade edilen kitap yeniden verilebilir", () => {
  const depo = new BellekOduncDeposu([
    { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  ]);
  const servis = new OduncServisi(depo);
  assert.equal(servis.iadeAl(1, "2025-06-20").durum, "iade alindi");
  assert.equal(servis.ver(1, 5, "2025-07-01").durum, "verildi");
});
node --test servis.test.mjs
✔ odunte olmayan kitap verilir (0.593333ms)
✔ odunte olan kitap reddedilir (0.103167ms)
✔ uye siniri asilinca reddedilir (0.056542ms)
✔ iade edilen kitap yeniden verilebilir (0.063459ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 35.385792

Parantez içindeki süreler ve duration_ms alanı her çalıştırmada değişir; anlamlı olan pass 4 ve fail 0 satırlarıdır. Dört kural, veritabanı dosyası olmadan, şema kurulmadan ve tek bir bağlantı açılmadan sınandı. Sınamanın kurulum adımı bir dizi kayıt yazmaktan ibaret; kurallara giren başlangıç durumu doğrudan okunabiliyor.

Sahtenin Sadakati

Sahte depo bir risk taşır: gerçek uygulamadan farklı davranırsa, sınamalar geçer ama uygulama çalışmaz. Bu riski kapatan yöntem, aynı sınama kümesini iki uygulamaya da uygulamaktır. Önce gerçek uygulama.

// sqlite-deposu.mjs — deponun gercek uygulamasi; butun SQL burada toplanir
const NESNE = (s) => (s === undefined ? null : {
  oduncId: s.odunc_id, kitapId: s.kitap_id, uyeId: s.uye_id,
  alisTarihi: s.alis_tarihi, iadeTarihi: s.iade_tarihi,
});

export class SqliteOduncDeposu {
  constructor(db) { this.db = db; }
  bul(oduncId) {
    return NESNE(this.db.prepare("SELECT * FROM odunc WHERE odunc_id = ?").get(oduncId));
  }
  kitabinAcikKaydi(kitapId) {
    return NESNE(this.db.prepare(
      "SELECT * FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId));
  }
  uyeninAcikKayitlari(uyeId) {
    return this.db.prepare(
      "SELECT * FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL ORDER BY odunc_id")
      .all(uyeId).map(NESNE);
  }
  ekle(kayit) {
    const sonuc = this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,?)")
      .run(kayit.kitapId, kayit.uyeId, kayit.alisTarihi, kayit.iadeTarihi);
    return Number(sonuc.lastInsertRowid);
  }
  iadeIsaretle(oduncId, tarih) {
    this.db.prepare("UPDATE odunc SET iade_tarihi = ? WHERE odunc_id = ?").run(tarih, oduncId);
  }
}

Uygulama bağlantıyı da dışarıdan alıyor. Dosya adını kendi içinde tutsaydı, sınama sırasında bellek içi bir veritabanına yönlendirilemezdi.

// sozlesme.test.mjs — ayni sinama kumesi iki uygulamaya da uygulanir
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { DatabaseSync } from "node:sqlite";
import { BellekOduncDeposu } from "./bellek-deposu.mjs";
import { SqliteOduncDeposu } from "./sqlite-deposu.mjs";

const BASLANGIC = [
  { oduncId: 1, kitapId: 5, uyeId: 2, alisTarihi: "2025-06-01", iadeTarihi: null },
  { oduncId: 2, kitapId: 6, uyeId: 2, alisTarihi: "2025-06-02", iadeTarihi: "2025-06-25" },
];

const bellekKur = () => new BellekOduncDeposu(BASLANGIC);

function sqliteKur() {
  const db = new DatabaseSync(":memory:");
  db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
           uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT)`);
  for (const k of BASLANGIC) {
    db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)")
      .run(k.oduncId, k.kitapId, k.uyeId, k.alisTarihi, k.iadeTarihi);
  }
  return new SqliteOduncDeposu(db);
}

for (const [ad, kur] of [["bellek", bellekKur], ["sqlite", sqliteKur]]) {
  describe(`depo sozlesmesi: ${ad}`, () => {
    test("acik kayit bulunur, kapali kayit bulunmaz", () => {
      const d = kur();
      assert.equal(d.kitabinAcikKaydi(5).oduncId, 1);
      assert.equal(d.kitabinAcikKaydi(6), null);
    });
    test("uyenin acik kayitlari yalniz acik olanlardir", () => {
      assert.deepEqual(kur().uyeninAcikKayitlari(2).map((k) => k.oduncId), [1]);
    });
    test("eklenen kayit yeni kimlik alir ve bulunur", () => {
      const d = kur();
      const id = d.ekle({ kitapId: 7, uyeId: 3, alisTarihi: "2025-07-01", iadeTarihi: null });
      assert.equal(d.bul(id).kitapId, 7);
    });
    test("iade isaretlemesi acik kaydi kapatir", () => {
      const d = kur();
      d.iadeIsaretle(1, "2025-07-02");
      assert.equal(d.kitabinAcikKaydi(5), null);
    });
    test("olmayan kimlik null doner", () => {
      assert.equal(kur().bul(999), null);
    });
  });
}
node --test sozlesme.test.mjs
▶ depo sozlesmesi: bellek
  ✔ acik kayit bulunur, kapali kayit bulunmaz (0.537917ms)
  ✔ uyenin acik kayitlari yalniz acik olanlardir (0.262666ms)
  ✔ eklenen kayit yeni kimlik alir ve bulunur (0.05475ms)
  ✔ iade isaretlemesi acik kaydi kapatir (0.046084ms)
  ✔ olmayan kimlik null doner (0.039042ms)
✔ depo sozlesmesi: bellek (1.312042ms)
▶ depo sozlesmesi: sqlite
  ✔ acik kayit bulunur, kapali kayit bulunmaz (0.342959ms)
  ✔ uyenin acik kayitlari yalniz acik olanlardir (0.113333ms)
  ✔ eklenen kayit yeni kimlik alir ve bulunur (0.107ms)
  ✔ iade isaretlemesi acik kaydi kapatir (0.082458ms)
  ✔ olmayan kimlik null doner (0.083541ms)
✔ depo sozlesmesi: sqlite (0.824958ms)
ℹ tests 10
ℹ suites 2
ℹ pass 10
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 34.280709

On sınama, iki uygulama, sıfır başarısızlık. Bu sınama kümesi deponun sözleşmesidir; sahtenin gerçeğe sadık kaldığını her çalıştırmada yeniden doğrular. Yeni bir uygulama eklendiğinde (başka bir veri deposu, önbellek katmanı, kayıtları dosyada tutan bir sürüm) aynı küme ona da uygulanır.

Sözleşme sınamasının bir sınırı vardır. Yalıtım düzeyi, kilitlenme davranışı ve eşzamanlı güncelleme çatışması bellek içi bir dizide üretilemez; bunlar yalnız gerçek uygulama üzerinde sınanır. Sahte deponun kapsamı iş kurallarıdır, kalıcılık davranışı değil.

Sızıntının Ölçülmesi

Sınırın kapalılığı sayılabilir. Kalıcılığın izleri (SQL anahtar sözcükleri, sürücü tipi, deyim hazırlama çağrısı) hangi modülde kaç kez geçiyor? Karşılaştırma için, aynı işi depo olmadan yapan bir sürüm de yazalım.

// sizdiran-servis.mjs — deponun yerine dogrudan baglanti kullanan surum
export class SizdiranOduncServisi {
  constructor(db) { this.db = db; }
  ver(uyeId, kitapId, tarih) {
    const acikKitap = this.db.prepare(
      "SELECT 1 FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId);
    if (acikKitap !== undefined) return { durum: "reddedildi", sebep: "kitap zaten odunc verilmis" };
    const sayi = this.db.prepare(
      "SELECT count(*) AS n FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").get(uyeId).n;
    if (sayi >= 3) return { durum: "reddedildi", sebep: "uye siniri asildi (3)" };
    const sonuc = this.db.prepare(
      "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)")
      .run(kitapId, uyeId, tarih);
    return { durum: "verildi", oduncId: Number(sonuc.lastInsertRowid) };
  }
}
// sizinti-olc.mjs — hangi modulde kac tane kalicilik izi var
import { readFileSync } from "node:fs";

const IZLER = /\b(SELECT|INSERT|UPDATE|DELETE|prepare|DatabaseSync)\b/g;

for (const dosya of ["odunc-servisi.mjs", "sizdiran-servis.mjs", "sqlite-deposu.mjs", "bellek-deposu.mjs"]) {
  const iz = readFileSync(dosya, "utf8").match(IZLER) ?? [];
  console.log(`${dosya.padEnd(22)} kalicilik izi = ${iz.length}`);
}
node sizinti-olc.mjs
odunc-servisi.mjs      kalicilik izi = 0
sizdiran-servis.mjs    kalicilik izi = 6
sqlite-deposu.mjs      kalicilik izi = 10
bellek-deposu.mjs      kalicilik izi = 0

Sayılar dağılımı gösteriyor. Depo kullanan serviste iz sıfır; kalıcılığın tamamı tek bir modülde, on iz olarak toplanmış. Sızdıran serviste ise altı iz iş kurallarının arasına karışmış durumda. Bu altı iz, o servisin sahte bir depoyla sınanamamasının nedenidir: prepare çağrısı bir dizide karşılığı olmayan bir yeteneği ister.

Ölçüm kaba bir araçtır, kesin bir kanıt değil. Bir modül SQL yazmadan da kalıcılığa bağlanabilir; örneğin bul çağrısının dönüş sırasının veritabanının varsayılan sıralamasına güvenmesi de bir bağımlılıktır. Sayının işlevi, sınırın nereye çizildiğini gözle görünür kılmaktır.

Deponun Sınırı ve Sorgu Çoğalması

Aynı servisin gerçek depoyla çalıştırılması ek bir kod değişikliği istemez.

// gercekle-calistir.mjs — ayni servis, bu kez veritabanina bagli depoyla
import { DatabaseSync } from "node:sqlite";
import { OduncServisi } from "./odunc-servisi.mjs";
import { SqliteOduncDeposu } from "./sqlite-deposu.mjs";

const depo = new SqliteOduncDeposu(new DatabaseSync("kutuphane.db"));
const servis = new OduncServisi(depo);

console.log("kitap 1 (acik kaydi var):", JSON.stringify(servis.ver(6, 1, "2025-07-15")));
console.log("kitap 7 (bos):           ", JSON.stringify(servis.ver(6, 7, "2025-07-15")));
console.log("uye 4 acik kayit sayisi: ", depo.uyeninAcikKayitlari(4).length);

Bu blok, SQL Temelleri kursundaki kütüphane şemasını taşıyan kutuphane.db dosyasını bekler. Aşağıdaki komut şemayı ve dersin çalıştığı satırları kurar.

rm -f kutuphane.db
sqlite3 kutuphane.db <<'SQL'
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);
INSERT INTO odunc VALUES (1,1,1,'2025-01-10','2025-01-24'),(2,2,1,'2025-02-02','2025-02-20'),
  (3,1,2,'2025-02-11',NULL),(4,3,3,'2025-03-01','2025-03-15'),(5,4,3,'2025-03-18','2025-04-02'),
  (6,1,4,'2025-04-05','2025-04-19'),(7,5,4,'2025-04-21',NULL),(8,2,5,'2025-05-02','2025-05-30'),
  (9,7,1,'2025-05-14','2025-05-28'),(10,3,5,'2025-06-03',NULL),(11,6,2,'2025-06-11','2025-06-25'),
  (12,4,4,'2025-06-20','2025-07-04');
SQL
node gercekle-calistir.mjs
kitap 1 (acik kaydi var): {"durum":"reddedildi","sebep":"kitap zaten odunc verilmis"}
kitap 7 (bos):            {"durum":"verildi","oduncId":13}
uye 4 acik kayit sayisi:  1

Kitap 1’in açık kaydı bulunduğu için istek reddedildi, boştaki kitap 7 verildi ve on üçüncü kimliği aldı. Servis kodunda tek satır değişmedi.

Depo kalıbının bedeli burada belirir. Arayüz alan diliyle yazıldığı için, her yeni soru yeni bir yöntem ister: gecikmiş kayıtlar, belirli şubedeki açık kayıtlar, belirli tarih aralığında verilenler, hem gecikmiş hem belirli şubede olanlar. Ölçütler birleştirilebilir olduğundan yöntem sayısı ölçüt sayısının kombinasyonuyla artar. Bir noktadan sonra depo arayüzü, gizlemeye çalıştığı sorgu diline benzemeye başlar.

Özet

  • Depo, çağıran tarafa alan kavramlarıyla konuşan bir arayüzdür; sorguyu, bağlantıyı ve satır–nesne çevrimini kendi içinde tutar.
  • Servisin deposunu dışarıdan alması, sınama sırasında bellek içi bir sahte depo verilmesini mümkün kılar; dört iş kuralı veritabanı olmadan sınandı.
  • Aynı sözleşme sınaması iki uygulamaya da uygulandı ve on sınamanın tamamı geçti; bu, sahtenin gerçeğe sadık kaldığının sürekli doğrulanmasıdır.
  • Kalıcılık izleri sayıldığında depo kullanan serviste sıfır, sızdıran serviste altı iz çıktı; sınırın kapalılığı böyle görünür kılınır.
  • Yalıtım ve eşzamanlılık davranışı sahte depoda üretilemez; sahtenin kapsamı iş kurallarıdır.

Sonraki Adım

Deponun arayüzü kapandıkça sorulacak soru sayısı arttı ve her soru yeni bir yöntem istedi. Gecikmiş kayıtlar, şubeye göre süzülmüş kayıtlar, tarih aralığıyla sınırlanmış kayıtlar ve bunların birleşimleri ayrı ayrı yazıldığında depo şişer. Sorun ölçütlerin kendisinde değil, ölçütlerin arayüzde sabit yöntem adlarına gömülmesindedir. Sonraki ders ölçütü bir nesne hâline getirir: her ölçüt kendi SQL parçasını ve bağlı değişkenlerini üretir, ölçütler “ve”, “veya”, “değil” ile birleştirilir ve depo tek bir yöntemle bütün birleşimleri karşılar.

İ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