İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat