Ders 17 / 21
Katman Sorumlulukları
Sunum, uygulama, alan ve altyapı katmanlarının ayrımı: bağımlılık yönü kuralının ithal grafiği üzerinden denetlenmesi, ihlalli sürümdeki bulguların sayılması ve sözleşmeyi alan katmanına taşıyarak bağımlılığın tersine çevrilmesi.
İçindekiler
Başarım Sorunları konusu veri erişim katmanının nasıl çalıştığını ölçtü: sorgu sayısı, aktarılan bayt, deyim sayısı, sorgu planı, sayfalama maliyeti. Hiçbiri katmanın ne yapması gerektiğini söylemedi. Ödünç verme kuralı nerede duruyor, doğrulama hangi katmanda yapılıyor, dışarıya dönen kayıt ile alan modeli aynı şey mi?
Bu konu o soruları ele alıyor ve ilki en temeldeki soru: kod hangi parçalara ayrılır ve bu parçalar birbirini hangi yönde çağırabilir? Ayrımın kâğıt üzerinde iddia edilmesi yeterli değildir; bir kural olarak yazılıp ithal grafiği (import graph) üzerinden denetlenmesi gerekir.
Dört Katman
Yerleşim dört sorumluluğa ayrılır. Adları değişse de ayrımın gerekçesi aynıdır: her katman farklı bir nedenle değişir.
| Katman | Sorumluluk | Değişme nedeni |
|---|---|---|
| Sunum | Dış dünyanın biçimi: yol, gövde çözümleme, durum kodu | Arayüz ya da protokol değişince |
| Uygulama | İş akışının sırası: kimi çağır, işlemi nerede aç, sonucu kime ver | Kullanım senaryosu değişince |
| Alan | İş kuralları: neyin geçerli, neyin yasak olduğu | Kurum kuralı değişince |
| Altyapı | Dış kaynaklara erişim: veritabanı, dosya, ağ | Araç ya da şema değişince |
Katman sayısı kadar önemli olan, aralarındaki bağımlılık yönüdür. Kural tek cümleyle yazılabilir: alan katmanı hiçbir katmana bağımlı olamaz. Kalan yönler bu kuraldan türer.
sunum → uygulama → alan ← altyapı
Altyapının oku ters yöne bakıyor. Veritabanı erişimi alan katmanını çağırmaz; alan katmanının tanımladığı bir sözleşmeyi karşılar. Bu ters yön dersin asıl konusu.
İhlalli Sürüm
Aşağıdaki dört modül ödünç verme isteğini karşılar. Dizin adları katmanı verir; kod önce akla gelen sırayla, doğrudan çağrılarla yazılmıştır.
mkdir -p ihlalli/sunum ihlalli/uygulama ihlalli/alan ihlalli/altyapi
// ihlalli/alan/odunc-kurallari.mjs — is kurallari, ama iki alt katmani dogrudan cagiriyor import { SqliteOduncDeposu } from "../altyapi/sqlite-odunc-deposu.mjs"; import { yanit } from "../sunum/yanit.mjs"; export const UYE_SINIRI = 3; export function oduncVer(db, uyeId, kitapId, tarih) { const depo = new SqliteOduncDeposu(db); if (depo.kitabinAcikKaydi(kitapId) !== null) return yanit(409, "kitap zaten odunc verilmis"); if (depo.uyeninAcikKayitlari(uyeId).length >= UYE_SINIRI) { return yanit(409, `uye siniri asildi (${UYE_SINIRI})`); } return yanit(201, String(depo.ekle({ kitapId, uyeId, alisTarihi: tarih }))); }
// ihlalli/altyapi/sqlite-odunc-deposu.mjs — SQL ve surucu burada import { DatabaseSync } from "node:sqlite"; export const baglan = (dosya) => new DatabaseSync(dosya); export class SqliteOduncDeposu { constructor(db) { this.db = db; } kitabinAcikKaydi(kitapId) { return this.db.prepare( "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId) ?? null; } uyeninAcikKayitlari(uyeId) { return this.db.prepare( "SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").all(uyeId); } ekle(k) { return Number(this.db.prepare( "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)") .run(k.kitapId, k.uyeId, k.alisTarihi).lastInsertRowid); } }
// ihlalli/sunum/yanit.mjs — HTTP yaniti bicimleyen sunum yardimcisi export const yanit = (durum, govde) => ({ durum, govde });
// ihlalli/uygulama/odunc-uygulama.mjs — istegi karsilayan uygulama servisi import { oduncVer } from "../alan/odunc-kurallari.mjs"; import { baglan } from "../altyapi/sqlite-odunc-deposu.mjs"; export function oduncIstegi(istek) { const db = baglan("kutuphane.db"); return oduncVer(db, istek.uyeId, istek.kitapId, istek.tarih); }
// ihlalli/sunum/http-ucu.mjs — govdeyi cozup uygulama servisini cagirir import { oduncIstegi } from "../uygulama/odunc-uygulama.mjs"; export const POST_odunc = (govde) => oduncIstegi(JSON.parse(govde));
Kod çalışır. Sorun davranışta değil, bağımlılık yönünde: iş kuralı hem SQL sınıfını hem HTTP biçimleyicisini ithal ediyor.
Kuralın Denetlenmesi
Bağımlılık yönü bir belge maddesi olarak kaldığı sürece ihlal edilir. Denetlenebilmesi için ölçülebilir olması gerekir. Aşağıdaki betik dizin ağacındaki bütün modülleri okur, ithal deyimlerini çıkarır, her modülü dizinine göre bir katmana yerleştirir ve izin verilmeyen her kenarı bir bulgu olarak bildirir.
// katman-denetle.mjs — ithal grafigini cikarir ve bagimlilik yonunu denetler import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative, dirname, resolve } from "node:path"; // Her katmanin ithal edebilecegi katmanlar. Liste disindaki her kenar bir ihlaldir. const IZIN = { sunum: ["sunum", "uygulama"], uygulama: ["uygulama", "alan"], altyapi: ["altyapi", "alan", "standart"], alan: ["alan", "standart"], kok: ["kok", "sunum", "uygulama", "alan", "altyapi", "standart"], }; const katman = (yol) => yol.split("/")[0] in IZIN ? yol.split("/")[0] : "kok"; const disKatman = (ad) => (ad === "node:sqlite" ? "altyapi" : "standart"); function dosyalar(kok, dizin = kok) { return readdirSync(dizin).flatMap((ad) => { const tam = join(dizin, ad); if (statSync(tam).isDirectory()) return dosyalar(kok, tam); return ad.endsWith(".mjs") ? [relative(kok, tam)] : []; }); } const ITHAL = /^\s*(?:import|export)[^;'"]*from\s+["']([^"']+)["']/gm; function ithaller(kok, dosya) { const metin = readFileSync(join(kok, dosya), "utf8"); return [...metin.matchAll(ITHAL)].map((m) => m[1]).map((h) => h.startsWith(".") ? relative(kok, resolve(kok, dirname(dosya), h)) : h); } export function denetle(kok) { const bulgular = []; for (const d of dosyalar(kok)) { for (const h of ithaller(kok, d)) { const kaynak = katman(d); const hedef = h.startsWith("node:") ? disKatman(h) : katman(h); if (!IZIN[kaynak].includes(hedef)) bulgular.push({ d, h, kaynak, hedef }); } } return bulgular; } // Bir modulden baslayarak ulasilan butun modullerin kumesi. export function kapanis(kok, giris) { const gorulen = new Set(); const yigit = [giris]; while (yigit.length > 0) { const su = yigit.pop(); if (gorulen.has(su) || su.startsWith("node:")) { gorulen.add(su); continue; } gorulen.add(su); for (const h of ithaller(kok, su)) yigit.push(h); } return gorulen; } const kok = process.argv[2]; const bulgular = denetle(kok); for (const b of bulgular) console.log(`ihlal: ${b.d} -> ${b.h} (${b.kaynak} -> ${b.hedef})`); console.log(`bulgu sayisi = ${bulgular.length}`); const k = kapanis(kok, "alan/odunc-kurallari.mjs"); console.log(`alan kapanisi = ${k.size} modul, node:sqlite iceride = ${k.has("node:sqlite") ? "evet" : "hayir"}`);
Sürücü ithali de bir katman sayılır: node:sqlite altyapı, kalan standart kitaplık
modülleri her yerden ithal edilebilen “standart” olarak işaretlenir. Ağacın kökündeki
dosyalar hiçbir katmana ait değildir; bunlar bileşim kökü (composition root) sayılır ve
her katmanı ithal edebilir.
node katman-denetle.mjs ihlalli
ihlal: alan/odunc-kurallari.mjs -> altyapi/sqlite-odunc-deposu.mjs (alan -> altyapi) ihlal: alan/odunc-kurallari.mjs -> sunum/yanit.mjs (alan -> sunum) ihlal: uygulama/odunc-uygulama.mjs -> altyapi/sqlite-odunc-deposu.mjs (uygulama -> altyapi) bulgu sayisi = 3 alan kapanisi = 4 modul, node:sqlite iceride = evet
Üç bulgu ve bir ölçü. Son satır ihlallerin sonucunu gösteriyor: iş kuralını içeren modüle dokunmak için dört modülün tamamı ve veritabanı sürücüsü gerekiyor.
Bağımlılığın Tersine Çevrilmesi
Bulguların ikisi aynı nedene dayanıyor: alan katmanı ihtiyaç duyduğu şeyi kendisi kurmaya çalışıyor. Çözüm, ihtiyacın alan katmanında sözleşme olarak yazılması, gerçekleştirimin altyapıda kalmasıdır. Depo Kalıbı dersinde kurulan depo arayüzü bunun bir örneğiydi; burada sözleşme açıkça bir modüle taşınıyor.
mkdir -p duzeltilmis/sunum duzeltilmis/uygulama duzeltilmis/alan duzeltilmis/altyapi
// duzeltilmis/alan/odunc-deposu.mjs — alan katmaninin ihtiyac duydugu depo sozlesmesi export const DEPO_SOZLESMESI = ["kitabinAcikKaydi", "uyeninAcikKayitlari", "ekle"]; export function depoyuDogrula(depo) { const eksik = DEPO_SOZLESMESI.filter((ad) => typeof depo?.[ad] !== "function"); if (eksik.length > 0) throw new TypeError(`depo sozlesmesi eksik: ${eksik.join(", ")}`); return depo; }
// duzeltilmis/alan/odunc-kurallari.mjs — yalniz is kurallari; HTTP yok, SQL yok import { depoyuDogrula } from "./odunc-deposu.mjs"; export const UYE_SINIRI = 3; export function oduncVer(depo, uyeId, kitapId, tarih) { depoyuDogrula(depo); if (depo.kitabinAcikKaydi(kitapId) !== null) { return { sonuc: "reddedildi", sebep: "kitap_oduncte" }; } if (depo.uyeninAcikKayitlari(uyeId).length >= UYE_SINIRI) { return { sonuc: "reddedildi", sebep: "uye_siniri", sinir: UYE_SINIRI }; } return { sonuc: "verildi", oduncId: depo.ekle({ kitapId, uyeId, alisTarihi: tarih }) }; }
Dönüş değeri de değişti. İhlalli sürüm HTTP durum kodu üretiyordu; bu sürüm alan diliyle konuşuyor. Durum kodu eşlemesi sunum katmanının işidir.
// duzeltilmis/altyapi/sqlite-odunc-deposu.mjs — sozlesmenin SQL ile gerceklestirimi import { DatabaseSync } from "node:sqlite"; import { depoyuDogrula } from "../alan/odunc-deposu.mjs"; export const baglan = (dosya) => new DatabaseSync(dosya); export class SqliteOduncDeposu { constructor(db) { this.db = db; depoyuDogrula(this); } kitabinAcikKaydi(kitapId) { return this.db.prepare( "SELECT odunc_id FROM odunc WHERE kitap_id = ? AND iade_tarihi IS NULL").get(kitapId) ?? null; } uyeninAcikKayitlari(uyeId) { return this.db.prepare( "SELECT odunc_id FROM odunc WHERE uye_id = ? AND iade_tarihi IS NULL").all(uyeId); } ekle(k) { return Number(this.db.prepare( "INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi) VALUES (?,?,?,NULL)") .run(k.kitapId, k.uyeId, k.alisTarihi).lastInsertRowid); } }
Altyapı modülü artık alan katmanını ithal ediyor. Ok tersine döndü ve bu, izinli yöndür.
// duzeltilmis/uygulama/odunc-uygulama.mjs — depoyu disaridan alir, kurali cagirir import { oduncVer } from "../alan/odunc-kurallari.mjs"; export class OduncUygulamasi { constructor(depo) { this.depo = depo; } ver(istek) { return oduncVer(this.depo, istek.uyeId, istek.kitapId, istek.tarih); } }
// duzeltilmis/sunum/http-ucu.mjs — alan sonucunu HTTP yanitina cevirir const DURUM = { verildi: 201, kitap_oduncte: 409, uye_siniri: 409 }; export function POST_odunc(uygulama, govde) { const s = uygulama.ver(JSON.parse(govde)); return s.sonuc === "verildi" ? { durum: DURUM.verildi, govde: { oduncId: s.oduncId } } : { durum: DURUM[s.sebep], govde: { sebep: s.sebep } }; }
Hiçbir katman kendi bağımlılığını kurmuyor. Bağlantıyı, depoyu ve uygulama servisini bir araya getiren tek yer bileşim köküdür.
// duzeltilmis/kur.mjs — bilesim koku: katmanlari yalniz burasi birbirine baglar import { baglan, SqliteOduncDeposu } from "./altyapi/sqlite-odunc-deposu.mjs"; import { OduncUygulamasi } from "./uygulama/odunc-uygulama.mjs"; import { POST_odunc } from "./sunum/http-ucu.mjs"; const uygulama = new OduncUygulamasi(new SqliteOduncDeposu(baglan(process.argv[2]))); for (const govde of ['{"uyeId":6,"kitapId":1,"tarih":"2025-07-15"}', '{"uyeId":6,"kitapId":7,"tarih":"2025-07-15"}']) { console.log(govde, "->", JSON.stringify(POST_odunc(uygulama, govde))); }
Denetim yeniden çalıştırılır.
node katman-denetle.mjs duzeltilmis
bulgu sayisi = 0 alan kapanisi = 2 modul, node:sqlite iceride = hayir
Bulgu sıfır. İkinci satır ilkinden daha çok şey söylüyor: iş kuralına ulaşmak için gereken modül sayısı dörtten ikiye indi ve veritabanı sürücüsü kapanışın dışında kaldı.
Bu sürüm gerçek veriyle de çalışır. Aşağıdaki komut SQL Temelleri kursundaki kütüphane şemasının ödünç bağıntısını kurar ve bileşim kökünü çalıştırır.
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 duzeltilmis/kur.mjs kutuphane.db
{"uyeId":6,"kitapId":1,"tarih":"2025-07-15"} -> {"durum":409,"govde":{"sebep":"kitap_oduncte"}}
{"uyeId":6,"kitapId":7,"tarih":"2025-07-15"} -> {"durum":201,"govde":{"oduncId":13}}
Kapanışın Pratik Karşılığı
Modül sayısı soyut bir ölçü gibi görünür. Karşılığı somuttur: kapanış küçüldükçe iş kuralı başka bir şey vermeden çağrılabilir hâle gelir. İki sürüme de sözleşmeyi karşılayan sahte bir nesne verildiğinde fark görünür.
// sahteyle-dene.mjs — ayni is kurali, veritabani olmadan, sahte bir depoyla import { oduncVer as ihlalli } from "./ihlalli/alan/odunc-kurallari.mjs"; import { oduncVer as duzeltilmis } from "./duzeltilmis/alan/odunc-kurallari.mjs"; const sahteDepo = { kitabinAcikKaydi: () => null, uyeninAcikKayitlari: () => [], ekle: () => 42, }; for (const [ad, kural] of [["ihlalli", ihlalli], ["duzeltilmis", duzeltilmis]]) { try { console.log(`${ad.padEnd(12)} ->`, JSON.stringify(kural(sahteDepo, 6, 7, "2025-07-15"))); } catch (e) { console.log(`${ad.padEnd(12)} -> ${e.constructor.name}: ${e.message}`); } }
node sahteyle-dene.mjs
ihlalli -> TypeError: this.db.prepare is not a function
duzeltilmis -> {"sonuc":"verildi","oduncId":42}
İhlalli sürüm sahte depoyu kabul etmiyor, çünkü verilen nesneyi bir bağlantı sanıp kendi SQL sınıfını kuruyor. Düzeltilmiş sürüm sözleşmeye baktığı için sahteyi kabul ediyor. Bağımlılık yönü kuralının sınanabilirlikle ilişkisi budur: kural bir üslup tercihi değil, alan katmanının bağımsız çalışabilmesinin ön koşuludur.
Uygulama ile Alan Arasındaki Sınır
Dört katman içinde en çok karışan ikisi uygulama ile alandır. Ayrım şu soruyla yapılır: kural, kurumun kuralı mı yoksa bu senaryonun akışı mı?
“Ödünçteki kitap yeniden verilemez” kurumun kuralıdır; kütüphane bunu ödünç verme ekranında da, toplu içe aktarımda da uygular. Alan katmanına aittir. “Önce üyeyi bul, sonra kuralı çalıştır, sonra işlemi kesinleştir, sonra bildirim gönder” ise bu senaryonun sırasıdır; başka bir senaryoda sıra değişir. Uygulama katmanına aittir.
İşlem sınırı da uygulama katmanının kararıdır. İşlem Sınırları dersinde iş biriminin
sınırının servis katmanında tutulması gerektiği gösterilmişti; bu ders o kararı katman
adıyla yerleştiriyor. Alan katmanı BEGIN yazmaz, çünkü bir kuralın işlem içinde mi yoksa
dışında mı değerlendirildiğini bilmesi gerekmez.
Sunum ile uygulama arasındaki sınır daha kolaydır. Sunum katmanı protokolün biçimini bilir: hangi yolun hangi işleve karşılık geldiği, gövdenin nasıl çözüleceği, sonucun hangi durum koduna eşleneceği. Bu bilginin uygulama katmanına sızması, aynı senaryonun ikinci bir giriş noktasından (zamanlanmış iş, kabuk komutu, kuyruk tüketicisi) çağrılmasını engeller.
Özet
- Dört katman farklı değişme nedenlerine göre ayrılır; ayrımın çekirdeği katman sayısı değil, aralarındaki bağımlılık yönüdür.
- Bağımlılık yönü ithal grafiğinden denetlenebilir: ihlalli sürümde üç bulgu çıktı, sözleşme alan katmanına taşınınca bulgu sıfıra indi.
- Alan katmanının ithal kapanışı dört modülden iki modüle indi ve veritabanı sürücüsü kapanışın dışında kaldı.
- İhlalli sürümdeki iş kuralı sahte bir depoyla çağrılamadı; düzeltilmiş sürüm aynı sahteyle çalıştı. Bağımlılık yönü kuralı, alan katmanının bağımsız sınanabilmesinin ön koşuludur.
- İş kuralı alan katmanına, senaryo sırası ve işlem sınırı uygulama katmanına, protokol biçimi sunum katmanına aittir; bağımlılıkları birbirine bağlayan tek yer bileşim köküdür.
Sonraki Adım
Düzeltilmiş sürümde alan katmanı { sonuc, sebep } biçiminde bir nesne döndürdü, sunum
katmanı bunu HTTP gövdesine çevirdi. Bu çeviri şimdilik üç alanlıydı ve neredeyse birebirdi;
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? Sonraki ders aynı alan nesnesinden iki farklı dış sözleşme
üretir, iç modele alan eklendiğinde dış sözleşmenin değişmediğini sınamayla gösterir ve
alan nesnesini doğrudan döndürmenin kaç iç alanı dışarıya sızdırdığını sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.