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.