Ders 07 / 18
Sınır Çizme Ölçütleri
Sınırın nereden geçeceğinin koddan ölçülmesi: modüller arası ithal kenarının bağlaşım ölçüsü olarak sayılması, her tablonun sahibinin belirlenip sahibi olmayan yazmaların bulunması, değişiklik kaydından çıkarılan değişim birlikteliği ve üç aday sınır çiziminin aynı ölçütlerle karşılaştırılması.
İçindekiler
Önceki konu tek parçadan sunucusuza kadar uzanan seçenekleri kurdu ve her birinin bedelini dağıtım birimi, süreç ve dağıtım adımı cinsinden saydı. Seçeneklerin hepsinin ortak bir varsayımı vardı: sınırın nereden geçtiği verili kabul edildi. Oysa bu listede en pahalı karar mimari seçeneğin adı değil, sınırın yeridir. Yanlış yerden geçen bir sınır, seçilen mimari ne olursa olsun her değişikliği iki tarafa birden dokundurur.
Bu ders sınırın yerini bir ölçüte bağlar ve ölçüt kodun içindedir. Alan modelleme, sınırlı bağlam ve bağlam haritası Alan Odaklı Tasarım kursunda kuruldu; burada alan modeli öğretilmez. Buradaki soru daha dar: elde çalışan bir modüler tek parça varken, hangi sayıya bakılarak sınır çizilir. Through-line kütüphane ödünç sistemidir ve beş alan modülüne sahiptir: katalog, üyelik, ödünç, bildirim, ücretlendirme.
Ölçülecek Uygulama
Aşağıdaki blok uygulamayı diske yazar ve bir kitabın ödünç verilmesi iş akışını koşturur. Modül dosyaları bir haritadan üretilir; her modül hangi modülleri ithal ettiğini, hangi tablolara yazdığını ve hangi tablonun sahibi olduğunu kendi kaynağında taşır.
// kur.mjs — kutuphane odunc uygulamasini modul dosyalari olarak yazar ve is akisini kosturur import { mkdirSync, writeFileSync } from "node:fs"; const MODUL = { // ithal: alan modulleri | yazar: tablolar | sahip: tablolar katalog: { ithal: [], yazar: ["kitap", "nusha"], sahip: ["kitap", "nusha"] }, uyelik: { ithal: [], yazar: ["uye"], sahip: ["uye"] }, odunc: { ithal: ["katalog", "uyelik"], yazar: ["odunc", "nusha"], sahip: ["odunc"] }, ucretlendirme: { ithal: ["odunc"], yazar: ["ucret", "odunc"], sahip: ["ucret"] }, bildirim: { ithal: ["uyelik", "odunc"], yazar: ["bildirim"], sahip: ["bildirim"] }, }; mkdirSync("uygulama", { recursive: true }); writeFileSync("uygulama/depo.mjs", "export const kayit = [];\n" + "export const yaz = (tablo, satir) => kayit.push({ tablo, ...satir });\n"); for (const [ad, m] of Object.entries(MODUL)) { writeFileSync(`uygulama/${ad}.mjs`, `// sahip: ${m.sahip.join(", ")}\n` + 'import { yaz, kayit } from "./depo.mjs";\n' + m.ithal.map((i) => `import { ${i}Oku } from "./${i}.mjs";\n`).join("") + `export const ${ad}Oku = () => kayit.filter((k) => k.tablo === "${m.sahip[0]}");\n` + `export function ${ad}Islem(veri) {\n` + m.ithal.map((i) => ` ${i}Oku();\n`).join("") + m.yazar.map((t) => ` yaz("${t}", { adim: "${ad}", ...veri });\n`).join("") + "}\n"); } writeFileSync("uygulama/uygulama.mjs", 'import { kayit } from "./depo.mjs";\n' + 'import { uyelikIslem } from "./uyelik.mjs";\n' + 'import { katalogIslem } from "./katalog.mjs";\n' + 'import { oduncIslem } from "./odunc.mjs";\n' + 'import { bildirimIslem } from "./bildirim.mjs";\n' + 'import { ucretlendirmeIslem } from "./ucretlendirme.mjs";\n' + 'const istek = { uye: 7, kitap: 412 };\n' + "uyelikIslem(istek); katalogIslem(istek); oduncIslem(istek);\n" + "bildirimIslem(istek); ucretlendirmeIslem(istek);\n" + 'export const yazilan = kayit.map((k) => `${k.adim}->${k.tablo}`);\n'); const { yazilan } = await import("./uygulama/uygulama.mjs"); console.log(`modul dosyasi: ${Object.keys(MODUL).length} alan modulu + depo + uygulama girisi`); console.log(`is akisi (bir kitabin odunc verilmesi) ${yazilan.length} yazma uretti:`); console.log(yazilan.join(" "));
modul dosyasi: 5 alan modulu + depo + uygulama girisi is akisi (bir kitabin odunc verilmesi) 8 yazma uretti: uyelik->uye katalog->kitap katalog->nusha odunc->odunc odunc->nusha bildirim->bildirim ucretlendirme->ucret ucretlendirme->odunc
Başlangıç noktası budur: tek dağıtım birimi, tek süreç, sekiz yazma. Bu hâliyle sistem çalışıyor ve hiçbir sınır yok. Sınır çizme kararı bundan sonra verilecek.
Koddan Okunan İki Ölçüt
İlk iki ölçüt kaynak dosyalardan doğrudan çıkarılır. Bağlaşım (coupling) burada bir sayıya indirgenir: bir modülün başka bir alan modülünden yaptığı ithal, bir kenardır. Veri sahipliği ikinci ölçüttür: her tablonun tek bir sahip modülü vardır ve sahibi olmayan bir modülün o tabloya yazması ayrı sayılır.
// olc.mjs — kur.mjs'in yazdigi uygulama/ dizinini okur; olcu koddan cikarilir import { readFileSync, readdirSync } from "node:fs"; const dosya = readdirSync("uygulama").filter((d) => d !== "depo.mjs" && d !== "uygulama.mjs"); const M = {}; for (const d of dosya) { const t = readFileSync(`uygulama/${d}`, "utf8"); M[d.replace(".mjs", "")] = { ithal: [...t.matchAll(/from "\.\/(\w+)\.mjs"/g)].map((m) => m[1]).filter((a) => a !== "depo"), yazar: [...t.matchAll(/yaz\("(\w+)"/g)].map((m) => m[1]), sahip: t.match(/^\/\/ sahip: (.+)$/m)[1].split(", "), }; } const ad = Object.keys(M); const sahibi = (tablo) => ad.find((a) => M[a].sahip.includes(tablo)); const kenar = ad.flatMap((a) => M[a].ithal.map((b) => [a, b])); const disYazma = ad.flatMap((a) => M[a].yazar.filter((t) => sahibi(t) !== a).map((t) => [a, t])); console.log(`${"modul".padEnd(15)}${"ithal ettigi".padEnd(18)}${"yazdigi".padEnd(15)}sahibi oldugu`); for (const a of ad) { console.log(`${a.padEnd(15)}${(M[a].ithal.join(",") || "-").padEnd(18)}` + `${M[a].yazar.join(",").padEnd(15)}${M[a].sahip.join(",")}`); } console.log(`toplam: ${ad.length} modul, ${kenar.length} ithal kenari, ` + `${ad.reduce((s, a) => s + M[a].yazar.length, 0)} yazma, ` + `${disYazma.length} sahibi olmayan yazma (${disYazma.map(([a, t]) => `${a}->${t}`).join(", ")})`);
modul ithal ettigi yazdigi sahibi oldugu bildirim uyelik,odunc bildirim bildirim katalog - kitap,nusha kitap,nusha odunc katalog,uyelik odunc,nusha odunc ucretlendirme odunc ucret,odunc ucret uyelik - uye uye toplam: 5 modul, 5 ithal kenari, 8 yazma, 2 sahibi olmayan yazma (odunc->nusha, ucretlendirme->odunc)
Beş kenar ve iki sahibi olmayan yazma. Bu iki yazma tek dağıtım birimi içinde yalnızca bir düzen kusurudur: ödünç modülü nüsha durumunu doğrudan değiştiriyor, ücretlendirme modülü ödünç kaydına gecikme işareti koyuyor. İkisi de aynı süreçte, aynı işlemin içinde olduğu için bugün bir bedel ödetmiyor. Sınır çizildiği anda bunlar sınır ihlaline dönüşür: bir servisin başka bir servisin tablosuna yazması artık ya bir uca ya da paylaşılan bir tabloya bağımlılık demektir.
Üçüncü Ölçüt: Değişim Birlikteliği
Üçüncü ölçüt kaynak dosyalarda görünmez, kod tabanının geçmişinde durur. Değişim birlikteliği (change coupling), iki modülün aynı değişiklikte birlikte değişme sıklığıdır. Ölçüt şudur: sık birlikte değişen iki modülü ayıran bir sınır, her değişikliği iki dağıtım birimine yayar.
SS1 — son çeyreğin değişiklik kaydı yirmi satırdır ve her satır bir değişikliğin dokunduğu modülleri verir. Gerekçe: sürüm geçmişindeki her değişiklik, dokunduğu dosyalardan modül kümesine indirgenebilir; bu ders o indirgemenin sonucunu girdi olarak sabitler. Sayı ve dağılım değişirse aşağıdaki sıralama da değişir; ölçütün kendisi değişmez.
// aday.mjs — ayni dizini okur, uc aday sinir cizimini ayni olcutlerle karsilastirir import { readFileSync, readdirSync } from "node:fs"; const M = {}; for (const d of readdirSync("uygulama").filter((x) => x !== "depo.mjs" && x !== "uygulama.mjs")) { const t = readFileSync(`uygulama/${d}`, "utf8"); M[d.replace(".mjs", "")] = { ithal: [...t.matchAll(/from "\.\/(\w+)\.mjs"/g)].map((m) => m[1]).filter((a) => a !== "depo"), yazar: [...t.matchAll(/yaz\("(\w+)"/g)].map((m) => m[1]), sahip: t.match(/^\/\/ sahip: (.+)$/m)[1].split(", "), }; } const ad = Object.keys(M); const sahibi = (tablo) => ad.find((a) => M[a].sahip.includes(tablo)); const kenar = ad.flatMap((a) => M[a].ithal.map((b) => [a, b])); const disYazma = ad.flatMap((a) => M[a].yazar.filter((t) => sahibi(t) !== a).map((t) => [a, t])); // SS1: son ceyregin degisiklik kaydi — her satir bir degisiklik ve dokundugu moduller const DEGISIM = [ "gecikme ucreti orani: odunc ucretlendirme", "odunc suresini uzatma: odunc ucretlendirme", "ucret muafiyeti: ucretlendirme", "hatirlatma metni: bildirim", "iletisim tercihi: uyelik bildirim", "uyelik dondurma: uyelik odunc", "tur etiketi: katalog", "arama alani: katalog", "nusha durumu: katalog odunc", "rezervasyon sirasi: katalog odunc", "ceza esigi: odunc ucretlendirme", "uye adres alani: uyelik", "bildirim kanali: bildirim", "yenileme hatirlatmasi: uyelik bildirim", "gecikme gunu hesabi: odunc ucretlendirme", "nusha kaybi: katalog odunc ucretlendirme", "kapak gorseli: katalog", "kimlik dogrulama: uyelik", "iade akisi: odunc", "ucret iadesi: ucretlendirme", ].map((s) => [s.split(": ")[0], s.split(": ")[1].split(" ")]); const cift = {}; for (const [, ms] of DEGISIM) { for (let i = 0; i < ms.length; i += 1) { for (let j = i + 1; j < ms.length; j += 1) { const c = [ms[i], ms[j]].sort().join(" + "); cift[c] = (cift[c] ?? 0) + 1; } } } console.log(`${DEGISIM.length} degisiklikte birlikte degisen ciftler (SS1):`); for (const [c, n] of Object.entries(cift).sort((a, b) => b[1] - a[1])) { console.log(` ${c.padEnd(30)}${n}`); } const ADAY = { "A referans/islem": [["katalog", "uyelik"], ["odunc", "ucretlendirme", "bildirim"]], "B varlik basina": [["katalog"], ["uyelik"], ["odunc"], ["ucretlendirme"], ["bildirim"]], "C degisim birlikteligi": [["odunc", "ucretlendirme"], ["katalog"], ["uyelik", "bildirim"]], }; console.log(`\n${"aday".padEnd(24)}${"birim".padStart(6)}${"sinir gecen ithal".padStart(19)}` + `${"sahiplik ihlali".padStart(17)}${"tek birimde kalan".padStart(19)}${"dagitim".padStart(9)}`); for (const [a, bolum] of Object.entries(ADAY)) { const birim = (m) => bolum.findIndex((b) => b.includes(m)); const dokunulan = DEGISIM.map(([, ms]) => new Set(ms.map(birim)).size); const toplam = dokunulan.reduce((s, n) => s + n, 0); console.log(`${a.padEnd(24)}${String(bolum.length).padStart(6)}` + `${String(kenar.filter(([x, y]) => birim(x) !== birim(y)).length).padStart(19)}` + `${String(disYazma.filter(([x, t]) => birim(x) !== birim(sahibi(t))).length).padStart(17)}` + `${`${dokunulan.filter((n) => n === 1).length}/${DEGISIM.length}`.padStart(19)}` + `${String(toplam).padStart(9)}`); } // Ayni degisiklik, dort gerceklestirim: SS1'in ilk satiri const [dad, dmod] = DEGISIM[0]; console.log(`\n"${dad}" degisikligi (${dmod.join(", ")}) dort gerceklestirimde:`); console.log(`${"gerceklestirim".padEnd(24)}${"dokunulan dosya".padStart(16)}${"dagitim birimi".padStart(16)}` + `${"aga donusen ithal".padStart(19)}${"sinir gecen yazma".padStart(19)}`); for (const [g, bolum] of Object.entries({ "moduler tek parca": [ad], ...ADAY })) { const birim = (m) => bolum.findIndex((b) => b.includes(m)); console.log(`${g.padEnd(24)}${String(dmod.length).padStart(16)}` + `${String(new Set(dmod.map(birim)).size).padStart(16)}` + `${String(kenar.filter(([x, y]) => dmod.includes(x) && dmod.includes(y) && birim(x) !== birim(y)).length).padStart(19)}` + `${String(disYazma.filter(([x, t]) => dmod.includes(x) && birim(x) !== birim(sahibi(t))).length).padStart(19)}`); }
20 degisiklikte birlikte degisen ciftler (SS1): odunc + ucretlendirme 5 katalog + odunc 3 bildirim + uyelik 2 odunc + uyelik 1 katalog + ucretlendirme 1 aday birim sinir gecen ithal sahiplik ihlali tek birimde kalan dagitim A referans/islem 2 3 1 14/20 26 B varlik basina 5 5 2 10/20 31 C degisim birlikteligi 3 3 1 16/20 24 "gecikme ucreti orani" degisikligi (odunc, ucretlendirme) dort gerceklestirimde: gerceklestirim dokunulan dosya dagitim birimi aga donusen ithal sinir gecen yazma moduler tek parca 2 1 0 0 A referans/islem 2 1 0 1 B varlik basina 2 2 1 2 C degisim birlikteligi 2 1 0 1
Üç Adayın Okunması
En sık birlikte değişen çift ödünç ile ücretlendirmedir: yirmi değişikliğin beşi ikisine birden dokunuyor. Katalog ile ödünç üç kez, üyelik ile bildirim iki kez birlikte değişiyor.
Aday B, her varlığa bir servis, en temiz görünen çizimdir ve ölçüde en pahalısıdır. Beş ithal kenarının beşi de sınırı geçiyor, iki sahiplik ihlalinin ikisi de sınır ihlaline dönüşüyor ve yirmi değişikliğin yalnız onu tek birimde kalıyor. Yirmi değişikliğin toplam dağıtım sayısı 31.
Aday A ile aday C bağlaşımda eşittir, ikisi de üç kenar ve bir ihlal veriyor. Ayrım üçüncü ölçütte çıkıyor: A değişikliklerin on dördünü, C on altısını tek birimde tutuyor; dağıtım toplamı 26’ya karşı 24. Fark, A’nın üyelik ile bildirimi ayırmasından geliyor — o çift iki kez birlikte değişiyor ve A her seferinde iki birime dokunuyor.
Buradan çıkan kural şudur: bağlaşım ve sahiplik ölçütleri aday sayısını daraltır, kararı değişim birlikteliği verir. İlk iki ölçüt kodun bugünkü hâlini, üçüncüsü kodun nasıl değiştiğini ölçer; sınır ikincisine göre çizilir çünkü sınırın bedeli her değişiklikte ödenir.
Aynı Değişiklik, Dört Gerçekleştirim
Son tablo tek bir değişikliği izliyor: gecikme ücreti oranı, ödünç ile ücretlendirmeye dokunuyor. Dokunulan dosya sayısı dört gerçekleştirimde de ikidir; sınır çizmek bir değişikliğin kod hacmini azaltmaz. Değişen şey o iki dosyanın nereye ait olduğudur.
Neyi ucuzlattı. Modüler tek parçada ve C çiziminde değişiklik tek dağıtım biriminde kalıyor: tek doğrulama, tek dağıtım, tek geri alma. C bunu tek parçadan farkı olmadan yaparken sistemi üç bağımsız birime bölmüş oluyor; katalog değişiklikleri ödünç birimine hiç dokunmuyor.
Neyi pahalılaştırdı. B çiziminde aynı değişiklik iki dağıtım birimine dokunuyor, bir ithal kenarı ağ çağrısına dönüşüyor ve iki yazma sınırı geçiyor. Ücretlendirmenin ödünç tablosuna yazması artık kendi verisine yazma değil, başka bir birimin verisine yazmadır; ya o birimde bir uç açılır ya da tablo iki birim tarafından paylaşılır. İkisi de yeni sözleşme demektir.
Hangi yeni arıza kipi doğdu. Tek parçada iki dosya birlikte yayımlanır ve arada tutarsız bir an olmaz. İki dağıtım birimine bölündüğünde iki yayım arasında bir pencere açılır: yeni oran ücretlendirmede yürürlüktedir ama ödünç eski alanı gönderiyordur. Bu, tek parçada bulunmayan bir arıza kipidir ve sınırın kendisinden doğar. A ile C’de kalan tek sahiplik ihlali de aynı sınıfa girer: ödünç modülünün nüsha durumunu yazması, katalog ayrı bir birime taşındığı anda ya bir çağrıya dönüşecek ya da iki birimin aynı satırı yazmasına yol açacaktır.
Özet
- Sınırın yeri üç ölçüyle sınanır ve üçü de koddan okunur: ithal kenarı (bağlaşım), tablo başına sahip modül (veri sahipliği), değişikliklerin modül kümesi (değişim birlikteliği).
- Ölçülen uygulamada 5 alan modülü, 5 ithal kenarı ve 8 yazma var; yazmaların ikisi sahibi olmayan modülden geliyor ve tek dağıtım biriminde bedelsizken sınır çizilince sınır ihlaline dönüşüyor.
- Her varlığa bir servis çizimi üç ölçütte de en kötüsüdür: 5 kenarın 5’i sınırı geçiyor, 2 ihlalin 2’si kalıyor, 20 değişikliğin yalnız 10’u tek birimde kalıyor ve dağıtım toplamı 31.
- Bağlaşımda eşit iki aday değişim birlikteliğiyle ayrılır: 14/20’ye karşı 16/20 ve 26’ya karşı 24 dağıtım; sık birlikte değişen çifti ayıran sınır her değişiklikte iki birime dokunur.
- Aynı değişiklik dört gerçekleştirimde de 2 dosyaya dokunuyor; sınır dosya sayısını değil dağıtım birimi, ağ çağrısı ve sınırı geçen yazma sayısını değiştiriyor.
Sonraki Adım
Sınır bir ölçüte bağlandı, ama sınırın çizilmesiyle birlikte tabloda sessizce duran bir sayı işlemeye başlıyor: ağa dönüşen ithal. Modüller arasında bir ithal kenarı, aynı süreçte bir işlev çağrısıdır ve maliyeti yığıt üzerinde ölçülür. İki dağıtım birimi arasında aynı kenar bir ağ çağrısıdır ve maliyeti tur cinsinden ölçülür. Sonraki ders bu dönüşümü gerçek süreçlerle kurar: aynı ödünç verme iş akışı önce tek süreçte, sonra ayrı süreçlerde koşturulur ve zincirin uzunluğu arttıkça uçtan uca turun nasıl büyüdüğü ölçülür. Aynı koşumda ikinci bir soru daha sorulur — bu süreçlerden biri ayakta değilken gelen isteklerin hangi payı yanıtsız kalır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.