İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat