Ders 01 / 18
Katmanlı Mimari
Katmanlı biçemin ilan edilmiş bir izinli kenar kümesi olarak yazılması: gönderi ücretlendirme kitaplığının dört katmana ayrılması, bir isteğin dokunduğu katman ile geçtiği sınır sayısının ölçülmesi, hiçbir şey değiştirmeden geçen katmanların sayılması ve açık katman ilanı ile ilan edilmemiş sızıntının ihlal sayısıyla ayrılması.
İçindekiler
Alan Odaklı Tasarım kursu bir soruyu açık bıraktı: buraya kadarki bütün kararlar bir uygulamanın içindeydi. Bağlam haritası birden çok bağlamı adlandırdı, ama hepsi aynı çalışan programın parçasıydı. Bağlamlar ayrı programlara dağıtıldığında ya da tek bir programda tutulmaya karar verildiğinde ölçülen şeyler değişir: bir isteğin dokunduğu birim sayısı, geçtiği modül sınırı sayısı, bir sürümün dağıtılabilmesi için birlikte yayınlanması gereken parça sayısı.
Bu kurs mimari düzenleri bu ölçülerle karşılaştırır ve her ders aynı kitaplığın — gönderi ücretlendirme ve yönlendirme kitaplığının — o düzende kurulmuş hâlini ölçer. İlk düzen birimleri sorumluluklarına göre sıralı yığınlara ayırır ve aralarındaki çağrı yönünü bir kuralla sınırlar.
Biçem Olarak Katman
Katmanların sorumlulukları ve aralarındaki bağımlılık yönü Veri Erişim Katmanı ve İş Mantığı kursundaki Katman Sorumlulukları dersinde kurulmuştu. İthal satırlarından katman kenarı çıkarıp ilan edilmiş bir kurala göre ihlal sayma yordamı da Tasarım İlkeleri kursundaki Bağımlılık Çizgesi Sağlığı dersinde yazılmıştı. Burada ikisi de yeniden anlatılmıyor; kullanılıyor.
Yeni olan biçemin kendisidir. Katmanlı mimari üç şeyi birlikte söyler. Birincisi, her birim tam bir katmana aittir; ikisine ait olan birim yoktur. İkincisi, katmanlar sıralıdır. Üçüncüsü ve belirleyici olanı, hangi katmanın hangisini çağırabileceği önceden ilan edilmiş bir kenar kümesidir. Üçüncü madde olmadan ilk ikisi bir dizin düzeninden fazlası değildir: kural yazılmamışsa ihlal de tanımlı değildir.
Kenar kümesinin biçimi bir ayrım getirir. Bir katman kapalı (closed) ise üstündeki katman onu atlayamaz, isteğin ondan geçmesi zorunludur. Açık (open) ise atlanmasına izin verilmiştir. Açık katman kuralın ihlali değil, kuralın bir parçasıdır: izinli kenar kümesine yazılır. Aynı çağrının izinli mi ihlal mi olduğu koda bakılarak anlaşılmaz, kenar kümesine bakılarak anlaşılır.
Kitaplığın Katmanlı Düzeni
Ücretlendirme bağlamı dört katmana ayrılıyor. Bağımlılıklar ithalle değil parametreyle kuruluyor; birimleri birbirine bağlayan tek yer bileşim köküdür.
mkdir -p katmanli/altyapi katmanli/alan katmanli/uygulama katmanli/sunum
// katmanli/altyapi/tarife-deposu.mjs — altyapi katmani: tarife satirlari ve posta kodu esdegeri const KADEMELER = [ { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 }, { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 }, ]; const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 }; const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" }; export const tarifeDeposu = { kademeler: () => KADEMELER.map((k) => ({ ...k })), katsayi: (postaKodu) => KATSAYI[POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak"], };
// katmanli/alan/ucret-kurali.mjs — alan katmani: ucret kurali, hicbir katmani ithal etmiyor const ASGARI_UCRET = 3990; const YUVARLAMA_ADIMI = 50; export const ucretKurali = { hesapla: (gonderi, kademeler, katsayi) => { const kademe = kademeler.find((k) => gonderi.agirlik <= k.ustAgirlik); if (kademe === undefined) throw new RangeError("agirlik kademesi yok"); const ham = kademe.ucret * katsayi; return Math.max(Math.round(ham / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI, ASGARI_UCRET); }, };
// katmanli/uygulama/teklif-ver.mjs — uygulama katmani: senaryonun sirasi export const uygulamaKatmani = (alan, altyapi) => ({ teklifVer: (gonderi) => { const kademeler = altyapi.kademeler(); const katsayi = altyapi.katsayi(gonderi.postaKodu); return { tutar: alan.hesapla(gonderi, kademeler, katsayi) }; }, kademeListele: () => altyapi.kademeler(), });
// katmanli/sunum/masa.mjs — sunum katmani: govde cozumleme ve tutar bicimleme export const sunumKatmani = (uygulama) => ({ teklifIstegi: (govde) => { const gonderi = { agirlik: Number(govde.agirlik), postaKodu: String(govde.postaKodu) }; const { tutar } = uygulama.teklifVer(gonderi); return { durum: 200, tutar: `${(tutar / 100).toFixed(2)} TL` }; }, kademeIstegi: () => uygulama.kademeListele(), }); // acik katman: kural bu kenari ilan ettiginde okuma istegi uygulama katmanini atlar export const acikKademeMasasi = (altyapi) => ({ kademeIstegi: () => altyapi.kademeler(), });
Sunum modülünde iki masa var. Birincisi kapalı düzenin masası, uygulama katmanını çağırır. İkincisi açık katman ilanının karşılığıdır ve yalnız okuma isteği için altyapıyı doğrudan çağırır. Aynı dosyada durmaları rastlantı değil: ikisi de aynı kuralın kapsamındadır ve fark kuralın hangisini izinli saydığındadır.
Bir İsteğin Yolu
Bağımlılıklar parametreyle geldiği için sınırlara sayaç konabilir. Ölçüm betiği bileşim kökünün işini yapar ve her katman nesnesini bir sarmalayıcıdan geçirir; sarmalayıcı çağrı ağacını kurar. Ağaçtan üç sayı çıkar: isteğin dokunduğu katman sayısı, katmanlar arasındaki çağrı sayısı ve geçirgen katman sayısı. Bir düğüm tek çocuklu ise, aldığı argümanlar ile çocuğa verdiği argümanlar aynıysa ve çocuktan aldığı değeri değiştirmeden döndürüyorsa o katman geçirgendir: isteğe hiçbir şey eklememiştir.
// sinir-say.mjs — bir istegin dokundugu katman, gectigi sinir ve gecirgen katman sayisi import { tarifeDeposu } from "./katmanli/altyapi/tarife-deposu.mjs"; import { ucretKurali } from "./katmanli/alan/ucret-kurali.mjs"; import { uygulamaKatmani } from "./katmanli/uygulama/teklif-ver.mjs"; import { sunumKatmani, acikKademeMasasi } from "./katmanli/sunum/masa.mjs"; const kokler = []; const yigit = []; function sar(katman, nesne) { const sarili = {}; for (const [ad, fn] of Object.entries(nesne)) { sarili[ad] = (...args) => { const dugum = { katman, ad, args, cocuk: [], deger: null }; (yigit.at(-1)?.cocuk ?? kokler).push(dugum); yigit.push(dugum); try { return (dugum.deger = fn(...args)); } finally { yigit.pop(); } }; } return sarili; } function dolas(dugum, gorulen = new Set(), gecis = [], gecirgen = []) { gorulen.add(dugum.katman); for (const c of dugum.cocuk) { gecis.push(`${dugum.katman}->${c.katman}`); dolas(c, gorulen, gecis, gecirgen); } const tek = dugum.cocuk.length === 1 ? dugum.cocuk[0] : null; if (tek !== null && JSON.stringify(dugum.args) === JSON.stringify(tek.args) && JSON.stringify(dugum.deger) === JSON.stringify(tek.deger)) { gecirgen.push(dugum.katman); } return { gorulen, gecis, gecirgen }; } const alan = sar("alan", ucretKurali); const altyapi = sar("altyapi", tarifeDeposu); const uygulama = sar("uygulama", uygulamaKatmani(alan, altyapi)); const sunum = sar("sunum", sunumKatmani(uygulama)); const acikSunum = sar("sunum", acikKademeMasasi(altyapi)); const ISTEKLER = [ ["teklif / kapali ", () => sunum.teklifIstegi({ agirlik: "3", postaKodu: "06800" })], ["kademe / kapali ", () => sunum.kademeIstegi()], ["kademe / acik katman", () => acikSunum.kademeIstegi()], ]; for (const [ad, cagir] of ISTEKLER) { kokler.length = 0; const sonuc = cagir(); const { gorulen, gecis, gecirgen } = dolas(kokler[0]); const ozet = Array.isArray(sonuc) ? `${sonuc.length} kademe` : JSON.stringify(sonuc); console.log(`${ad} sonuc = ${ozet}`); console.log(` dokunulan katman = ${gorulen.size}, sinir gecisi = ${gecis.length}, gecirgen katman = ${gecirgen.length}`); console.log(` yol = ${gecis.join(", ")}`); if (gecirgen.length > 0) console.log(` gecirgen = ${gecirgen.join(", ")}`); }
node sinir-say.mjs
teklif / kapali sonuc = {"durum":200,"tutar":"114.50 TL"}
dokunulan katman = 4, sinir gecisi = 4, gecirgen katman = 0
yol = sunum->uygulama, uygulama->altyapi, uygulama->altyapi, uygulama->alan
kademe / kapali sonuc = 4 kademe
dokunulan katman = 3, sinir gecisi = 2, gecirgen katman = 2
yol = sunum->uygulama, uygulama->altyapi
gecirgen = uygulama, sunum
kademe / acik katman sonuc = 4 kademe
dokunulan katman = 2, sinir gecisi = 1, gecirgen katman = 1
yol = sunum->altyapi
gecirgen = sunum
Geçirgen Katman
İlk istek biçemin işini gösteriyor. Teklif isteği dört katmana dokundu, dört sınır geçti ve geçirgen katman sayısı 0 çıktı: sunum gövdeyi çözümledi ve tutarı biçimledi, uygulama çağrı sırasını kurdu, altyapı iki tabloyu verdi, alan kuralı hesapladı. Her katman isteğe bir şey ekledi.
İkinci istek aynı biçemin bedelini gösteriyor. Kademe listesi isteği üç katmana dokundu ama iki katman geçirgen çıktı: uygulama katmanı aldığı boş argüman listesini olduğu gibi aşağı geçirdi ve dönen listeyi değiştirmeden yukarı verdi; sunum katmanı da aynısını yaptı. Üç katmanın ikisi yalnız bir aktarma noktasıydı. Katmanlı biçemin bu belirtisine katman atlama deme eğilimi yanlıştır: burada atlanan bir şey yok, tersine hiçbir şey yapmayan iki durak var.
Üçüncü koşum ölçünün ne kadar düştüğünü söylüyor. Aynı sonuç, açık katman ilanıyla iki katmana dokunularak ve bir sınır geçilerek üretildi: dokunulan katman 3’ten 2’ye, sınır geçişi 2’den 1’e, geçirgen katman 2’den 1’e indi. Kazanç bedava değil; iki masa arasındaki fark, aynı okuma isteğinin iki ayrı yerden karşılanabilmesidir ve okumaya bir iş kuralı eklendiği gün o kural iki yere yazılır ya da açık kenar geri alınır.
Sızıntının İthal Grafiğinde Görünmesi
Açık katman ile sızıntı çalışma zamanında birbirine benzer: ikisinde de bir katman atlanır. Fark ilan edilip edilmediğidir ve bu fark ithal grafiğinde ölçülür. Aşağıdaki blok kapalı düzenin bir kopyasını alıp iki dosyayı değiştiriyor.
cp -r katmanli sizintili cat > sizintili/sunum/masa.mjs <<'EOF' // sizintili/sunum/masa.mjs — okuma istegi uygulama katmanini atlayip altyapiyi cagiriyor import { tarifeDeposu } from "../altyapi/tarife-deposu.mjs"; export const sunumKatmani = (uygulama) => ({ teklifIstegi: (govde) => { const gonderi = { agirlik: Number(govde.agirlik), postaKodu: String(govde.postaKodu) }; const { tutar } = uygulama.teklifVer(gonderi); return { durum: 200, tutar: `${(tutar / 100).toFixed(2)} TL` }; }, kademeIstegi: () => tarifeDeposu.kademeler(), }); EOF cat > sizintili/alan/bolge-cozumleyici.mjs <<'EOF' // sizintili/alan/bolge-cozumleyici.mjs — alan katmanindaki yardimci altyapi tablosuna uzaniyor import { tarifeDeposu } from "../altyapi/tarife-deposu.mjs"; export const enPahaliKademe = () => tarifeDeposu.kademeler().reduce((a, b) => (a.ucret >= b.ucret ? a : b)); EOF find sizintili -name '*.mjs' | sort
sizintili/alan/bolge-cozumleyici.mjs sizintili/alan/ucret-kurali.mjs sizintili/altyapi/tarife-deposu.mjs sizintili/sunum/masa.mjs sizintili/uygulama/teklif-ver.mjs
İki değişiklik de tek başına bakıldığında masum görünür. Sunum masası bir liste isteği için araya bir katman koymaya gerek görmüyor; alan katmanındaki yardımcı en pahalı kademeyi öğrenmek istiyor. Denetim aracı her dosyanın bulunduğu dizinden katmanını okuyor, ithal satırlarını katman kenarına indirgiyor ve iki ayrı kenar kümesine karşı sayıyor.
// katman-ihlali.mjs — ithal grafigini katman kenarina indirger, ilan edilmis kurala gore ihlali sayar import { readdirSync, readFileSync } from "node:fs"; import { basename, dirname, join, normalize } from "node:path"; const KAPALI = new Set(["sunum->uygulama", "uygulama->alan", "uygulama->altyapi"]); const ACIK = new Set([...KAPALI, "sunum->altyapi"]); function katmanKenarlari(kok) { const kenar = new Set(); for (const katman of readdirSync(kok).sort()) { for (const dosya of readdirSync(join(kok, katman)).sort()) { const metin = readFileSync(join(kok, katman, dosya), "utf8"); for (const m of metin.matchAll(/^import\s.*?from\s+"(\.[^"]+)"/gm)) { const hedef = basename(dirname(normalize(join(kok, katman, m[1])))); if (hedef !== katman) kenar.add(`${katman}->${hedef}`); } } } return [...kenar]; } for (const kok of ["katmanli", "sizintili"]) { const kenarlar = katmanKenarlari(kok); console.log(`${kok}/ katman kenari = ${kenarlar.length} (${kenarlar.join(", ") || "yok"})`); for (const [ad, kural] of [["kapali kural", KAPALI], ["acik katman kurali", ACIK]]) { const ihlal = kenarlar.filter((k) => kural.has(k) === false); console.log(` ${ad.padEnd(19)} izinli kenar = ${kural.size}, ihlal = ${ihlal.length}${ihlal.length ? ` (${ihlal.join(", ")})` : ""}`); } }
node katman-ihlali.mjs
katmanli/ katman kenari = 0 (yok) kapali kural izinli kenar = 3, ihlal = 0 acik katman kurali izinli kenar = 4, ihlal = 0 sizintili/ katman kenari = 2 (alan->altyapi, sunum->altyapi) kapali kural izinli kenar = 3, ihlal = 2 (alan->altyapi, sunum->altyapi) acik katman kurali izinli kenar = 4, ihlal = 1 (alan->altyapi)
Kapalı düzenin katman kenarı 0 çıktı. Bu bir eksiklik değil, bileşim kökü düzeninin sonucudur: hiçbir katman modülü başka bir katmanı ithal etmiyor, bağlar tek bir dosyada kuruluyor. Böyle bir düzende katman kuralının denetlendiği yer ithal grafiği değil, o tek dosyadır — ve sızıntı tam olarak o dosyanın atlandığı anda ithal grafiğinde görünür hâle gelir.
Sızıntılı düzenin iki kenarı iki ayrı cinsten. sunum->altyapi kenarı açık katman kuralında
izinlidir, kapalı kuralda ihlaldir: aynı satır, iki kurala göre iki farklı yargı. Buna karşılık
alan->altyapi kenarı iki kuralda da ihlal, çünkü hiçbir kenar kümesi alan katmanından dışa
çıkan bir kenar ilan etmiyor. Sızıntının iki cinsi arasındaki fark budur: birincisi kuralın
gözden geçirilmesi gereken bir yerdir, ikincisi biçemin taşıyıcı kuralını bozar. Alan
katmanının bir tablo modülüne uzanması, o katmanın tek başına ithal edilebilmesini de bitirir;
kuralı sınamak için tabloyu da yüklemek gerekir.
Ödünleşimin Sayıları
Biçem bir kalite niteliğini iyileştirir, başka birini kötüleştirir. Sürdürülebilirlik tarafında kazanç ölçüldü: ücret kuralı tek dosyada, dışa çıkan kenarı 0, kapalı düzende katman ihlali 0. Kural değiştiğinde dokunulan dosya bir tanedir ve tabloların biçimi kurala hiç girmez.
Bedel de aynı ölçülerle yazılıyor. Aynı biçem, iş kuralı gerektirmeyen bir okuma isteğinde 3 katmana dokunuyor ve bu üçün 2’si isteğe hiçbir şey eklemiyor. Katman sayısı arttıkça bu oran büyür: her yeni katman, kural içermeyen isteklerde bir geçirgen durak daha demektir. Biçemin kendisi bu iki sayı arasındaki ödünleşimdir — ilan edilmiş kenar kümesi ne kadar sıkıysa kuralın yeri o kadar kesin, geçirgen durak sayısı o kadar yüksektir.
Özet
- Katmanlı biçem üç şeyi birlikte söyler: her birim tam bir katmana aittir, katmanlar sıralıdır ve izinli çağrı kenarları önceden ilan edilmiştir; üçüncüsü olmadan ihlal tanımlı değildir.
- Teklif isteği 4 katmana dokundu, 4 sınır geçti ve geçirgen katman sayısı 0 çıktı; kademe listesi isteği 3 katmana dokundu ve 3 katmanın 2’si isteğe hiçbir şey eklemedi.
- Açık katman ilanı aynı okuma isteğini 2 katmana ve 1 sınıra indirdi; bedeli, aynı isteğin iki ayrı yerden karşılanabilir hâle gelmesidir.
- Kapalı düzende katman kenarı 0 çıktı, çünkü bağlar bileşim kökünde kuruluyor; sızıntı o kökün atlandığı anda ithal grafiğinde görünür.
- Sızıntılı düzendeki iki kenardan
sunum->altyapiaçık kuralda izinli, kapalı kuralda ihlal;alan->altyapiher iki kuralda da ihlal ve alan katmanının tek başına yüklenmesini bitiriyor.
Sonraki Adım
Kapalı düzenin izinli kenar kümesinde bir kenar dikkat çekiyor: uygulama->altyapi. Kural, iş
akışını kuran katmanın tabloya doğrudan uzanmasına izin veriyor; yani sıralamada altyapı,
alanın yanında bir başka aşağı katmandır. Sıra bu biçimde kurulduğunda kitaplığın merkezinde
alan modeli değil, isteğin geçtiği yol durur. Sonraki ders sıralamayı tersine çeviren iki
düzeni ele alır: bütün bağların içe, alana doğru baktığı halkalı yerleşimleri. Altıgen
mimarinin dışında sırasız bir bağdaştırıcı kuşağı vardır, soğan mimaride ise halkalar
sıralıdır; iki düzen aynı üç değişiklik senaryosunda düzenlenen ve eklenen dosya sayısıyla
karşılaştırılır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.