İçeriğe geç
academia.sh

Ders 07 / 10

Uygunluk İşlevleri

Bir mimari kuralın koşturulabilir bir sayıya çevrilmesi: iki uygunluk işlevinin gerçek modül dosyaları üzerinde yazılması, bilinen sızıntı kümesinde yakalanan ve kaçan ihlal ile yanlış alarmın sayılması, eşiğin baştan sona taranması, kuralın satır ve dosya okuma maliyeti ve yanlış alarmın kuralı devre dışı bıraktırması.

İçindekiler

Önceki ders dokuz yönetişim kuralının beşinin bir denetime çevrilebileceğini yazdı ama çevirinin bedelini saymadı. Çeviri şu demektir: kural bir sayıya ve bir eşiğe indirilir. Sayıyı üreten işleve uygunluk işlevi denir; kaynağı, yapılandırmayı ya da bir grafiği okur ve tek bir nicelik döndürür. Eşik ise kararın kendisidir — bu sayının üstü ihlal, altı uygundur.

Eşik seçildiği anda iki hata birden doğar. Eşik gevşekse gerçek ihlaller altından geçer; sıkıysa kurallara uygun modüller ihlal diye işaretlenir. Bu ikili, kalite göstergelerindeki kaçan hata ile yanlış geçme olgusunun aynısıdır; buradaki fark, hatayı yapanın bir sınama değil bir mimari kural olmasıdır. Bu ders bir uygunluk işlevini gerçekten yazar, bilinen bir sızıntı kümesinde koşturur ve üç sayıyı birden sayar: yakalanan, kaçan, yanlış alarm. Through-line bölgesel kütüphane ağıdır ve kurgudur.

Ölçülecek Modül Kümesi

Denetlenecek kural şudur: bir modül, dışarıya işini yapmak için gerekenden fazlasını açmaz. Cümle hâliyle denetlenemez; denetlenebilmesi için “fazlasının” bir sayıya bağlanması gerekir. Aşağıdaki blok ölçülecek kod tabanını diske yazar: on iki modül, her biri kendi dışa açık sembollerini dışa aktarır ve başkalarının sembollerini ithal eder.

YN4 — bölgesel kütüphane ağının kod tabanı on iki modülden oluşur ve her modülün dışa açtığı sembol sayısı ile bu sembolleri kimin ithal ettiği aşağıdaki haritadan üretilir. Gerekçe: uygunluk işlevi gerçek dosyaları okumalıdır; harita, okunacak dosyaları üreten girdidir. Sayılar değişirse aşağıdaki eşik taraması da değişir, işlevin kendisi değişmez.

// kur.mjs — bolgesel kutuphane agi modullerini disa acilan sembolleriyle diske yazar
import { mkdirSync, writeFileSync } from "node:fs";

// acik: disa acilan sembol sayisi | tuketici: hangi modul hangi sembol sirasini ithal ediyor
const MODUL = {
  ortakBicim:      { acik: 9, tuketici: { raporlama: [0, 1, 2], oduncAkisi: [0, 3, 4],
                                          subeUcu: [1, 5], ayarlar: [2] } },
  subeUcu:         { acik: 11, tuketici: { raporlama: [0, 1, 2] } },
  katalogKoprusu:  { acik: 7, tuketici: { oduncAkisi: [0, 1, 2, 3, 4, 5, 6] } },
  oduncAkisi:      { acik: 8, tuketici: { subeUcu: [0, 1, 2, 3], raporlama: [4, 5],
                                          ucretHesabi: [6] } },
  ucretHesabi:     { acik: 4, tuketici: { subeUcu: [0] } },
  uyelikKaydi:     { acik: 3, tuketici: { subeUcu: [0, 1], oduncAkisi: [2] } },
  kimlikKoprusu:   { acik: 2, tuketici: { subeUcu: [0], uyelikKaydi: [1] } },
  bildirimKuyrugu: { acik: 5, tuketici: { oduncAkisi: [0] } },
  raporlama:       { acik: 6, tuketici: { subeUcu: [0, 1, 2, 3, 4, 5] } },
  depoErisimi:     { acik: 3, tuketici: { oduncAkisi: [0] } },
  olayGunlugu:     { acik: 4, tuketici: { oduncAkisi: [0] } },
  ayarlar:         { acik: 4, tuketici: { subeUcu: [0], oduncAkisi: [1], raporlama: [2],
                                          ucretHesabi: [3] } },
};

mkdirSync("modul", { recursive: true });
for (const ad of Object.keys(MODUL)) {
  const ithal = Object.entries(MODUL)
    .filter(([, m]) => m.tuketici[ad])
    .map(([hedef, m]) => `import { ${m.tuketici[ad].map((i) => `${hedef}${i + 1}`).join(", ")} }` +
      ` from "./${hedef}.mjs";`);
  const disa = Array.from({ length: MODUL[ad].acik },
    (_, i) => `export const ${ad}${i + 1} = (x) => x;`);
  writeFileSync(`modul/${ad}.mjs`, `${ithal.join("\n")}\n${disa.join("\n")}\n`);
}

const kenar = Object.values(MODUL).reduce((s, m) => s + Object.keys(m.tuketici).length, 0);
const sembol = Object.values(MODUL).reduce((s, m) => s + m.acik, 0);
console.log(`${Object.keys(MODUL).length} modul dosyasi yazildi; ` +
  `${sembol} disa acilan sembol, ${kenar} ithal kenari`);
console.log(`\n${"modul".padEnd(17)}${"disa acilan".padStart(12)}   ithal eden modul`);
for (const [ad, m] of Object.entries(MODUL)) {
  console.log(`${ad.padEnd(17)}${String(m.acik).padStart(12)}   ` +
    `${Object.keys(m.tuketici).join(", ") || "-"}`);
}
12 modul dosyasi yazildi; 66 disa acilan sembol, 22 ithal kenari

modul             disa acilan   ithal eden modul
ortakBicim                  9   raporlama, oduncAkisi, subeUcu, ayarlar
subeUcu                    11   raporlama
katalogKoprusu              7   oduncAkisi
oduncAkisi                  8   subeUcu, raporlama, ucretHesabi
ucretHesabi                 4   subeUcu
uyelikKaydi                 3   subeUcu, oduncAkisi
kimlikKoprusu               2   subeUcu, uyelikKaydi
bildirimKuyrugu             5   oduncAkisi
raporlama                   6   subeUcu
depoErisimi                 3   oduncAkisi
olayGunlugu                 4   oduncAkisi
ayarlar                     4   subeUcu, oduncAkisi, raporlama, ucretHesabi

Kuralı Sayıya Çeviren İşlev

Aynı kural iki ayrı sayıya çevrilebilir. F1 — yüzey büyüklüğü en ucuz çeviridir: modülün dışa açtığı sembolleri sayar ve tek dosya okur. F2 — ölü yüzey oranı daha pahalıdır: dışa açılan sembollerin kaç tanesinin hiçbir modül tarafından ithal edilmediğini bulmak için bütün kod tabanını okur. İkisi de aynı kuralın adayıdır; hangisinin daha iyi olduğu ancak bilinen bir ihlal kümesinde sınanarak söylenir.

// uygunluk.mjs — bir mimari kurali sayiya ceviren iki uygunluk islevi ve esik taramasi
import { readFileSync, readdirSync } from "node:fs";

const DIZIN = "modul";
const dosya = readdirSync(DIZIN);

// >>> F1  yuzey buyuklugu: modulun kendi dosyasi yeter
function F1(ad) {
  const t = readFileSync(`${DIZIN}/${ad}.mjs`, "utf8");
  return [...t.matchAll(/^export const (\w+)/gm)].length;
}
// <<< F1

// >>> F2  olu yuzey orani: disa acilan sembollerin kac tanesi hic ithal edilmiyor
function F2(ad) {
  const acik = new Set([...readFileSync(`${DIZIN}/${ad}.mjs`, "utf8")
    .matchAll(/^export const (\w+)/gm)].map((m) => m[1]));
  const kullanilan = new Set();
  for (const d of dosya) {
    for (const m of readFileSync(`${DIZIN}/${d}`, "utf8")
      .matchAll(/^import \{ ([^}]+) \} from "\.\/(\w+)\.mjs"/gm)) {
      if (m[2] === ad) for (const s of m[1].split(", ")) kullanilan.add(s);
    }
  }
  return (acik.size - kullanilan.size) / acik.size;
}
// <<< F2

// YN5: mimar on iki modulu elle inceledi ve besini gercek yuzey sizintisi olarak isaretledi.
// subeUcu ve ucretHesabi olu sembol tasiyor; bildirimKuyrugu ve depoErisimi ic ayrintiyi aciyor;
// katalogKoprusu yedi sembolunu tek tuketiciye acarak ic temsilini disari tasiriyor.
const SIZINTI = new Set(["subeUcu", "katalogKoprusu", "ucretHesabi", "bildirimKuyrugu", "depoErisimi"]);
const ad = dosya.map((d) => d.replace(".mjs", ""));

console.log(`${"modul".padEnd(17)}${"F1 yuzey".padStart(10)}${"F2 olu oran".padStart(13)}${"mimar etiketi".padStart(15)}`);
for (const a of ad) {
  console.log(`${a.padEnd(17)}${String(F1(a)).padStart(10)}${F2(a).toFixed(2).padStart(13)}` +
    `${(SIZINTI.has(a) ? "sizinti" : "uygun").padStart(15)}`);
}

function tara(islev, esikler, etiket) {
  console.log(`\n${etiket.padEnd(12)}${"isaretlenen".padStart(13)}${"yakalanan".padStart(11)}` +
    `${"kacan".padStart(7)}${"yanlis alarm".padStart(14)}`);
  for (const e of esikler) {
    const isaret = ad.filter((a) => islev(a) > e);
    const y = isaret.filter((a) => SIZINTI.has(a)).length;
    console.log(`${`esik > ${e}`.padEnd(12)}${String(isaret.length).padStart(13)}` +
      `${`${y}/${SIZINTI.size}`.padStart(11)}${String(SIZINTI.size - y).padStart(7)}` +
      `${String(isaret.length - y).padStart(14)}`);
  }
}
tara(F1, [2, 3, 4, 5, 6, 7, 8, 10], "F1 yuzey");
tara(F2, [0.2, 0.4, 0.5, 0.6, 0.7], "F2 olu oran");

// kuralin kendi maliyeti: satir, dosya okuma, kosum
const kaynak = readFileSync("uygunluk.mjs", "utf8").split("\n");
const satir = (m) => {
  const b = kaynak.findIndex((l) => l.startsWith(`// >>> ${m}`));
  return kaynak.findIndex((l) => l.startsWith(`// <<< ${m}`)) - b - 1;
};
const toplamSatir = ad.reduce((s, a) => s + readFileSync(`${DIZIN}/${a}.mjs`, "utf8").split("\n").length, 0);
console.log(`\n${"islev".padEnd(12)}${"kural satiri".padStart(14)}${"dosya okuma".padStart(13)}` +
  `${"taranan satir".padStart(15)}`);
console.log(`${"F1".padEnd(12)}${String(satir("F1")).padStart(14)}${String(ad.length).padStart(13)}` +
  `${String(toplamSatir).padStart(15)}`);
console.log(`${"F2".padEnd(12)}${String(satir("F2")).padStart(14)}` +
  `${String(ad.length * (1 + dosya.length)).padStart(13)}${String((1 + ad.length) * toplamSatir).padStart(15)}`);
console.log("kosum suresi ortama baglidir; burada ortamdan bagimsiz nicelikler basilir " +
  `(F2 / F1 okuma orani ${(ad.length * (1 + dosya.length)) / ad.length}x)`);
modul              F1 yuzey  F2 olu oran  mimar etiketi
ayarlar                   4         0.00          uygun
bildirimKuyrugu           5         0.80        sizinti
depoErisimi               3         0.67        sizinti
katalogKoprusu            7         0.00        sizinti
kimlikKoprusu             2         0.00          uygun
oduncAkisi                8         0.13          uygun
olayGunlugu               4         0.75          uygun
ortakBicim                9         0.33          uygun
raporlama                 6         0.00          uygun
subeUcu                  11         0.73        sizinti
ucretHesabi               4         0.75        sizinti
uyelikKaydi               3         0.00          uygun

F1 yuzey      isaretlenen  yakalanan  kacan  yanlis alarm
esik > 2               11        5/5      0             6
esik > 3                9        4/5      1             5
esik > 4                6        3/5      2             3
esik > 5                5        2/5      3             3
esik > 6                4        2/5      3             2
esik > 7                3        1/5      4             2
esik > 8                2        1/5      4             1
esik > 10               1        1/5      4             0

F2 olu oran   isaretlenen  yakalanan  kacan  yanlis alarm
esik > 0.2              6        4/5      1             2
esik > 0.4              5        4/5      1             1
esik > 0.5              5        4/5      1             1
esik > 0.6              5        4/5      1             1
esik > 0.7              4        3/5      2             1

islev         kural satiri  dosya okuma  taranan satir
F1                       4           12            106
F2                      12          156           1378
kosum suresi ortama baglidir; burada ortamdan bagimsiz nicelikler basilir (F2 / F1 okuma orani 13x)

Eşiğin Okunması

F1 taraması eşiğin ne olduğunu çıplak gösterir. En gevşek uçta (> 2) beş sızıntının beşi de yakalanır ve hiçbiri kaçmaz — ama on iki modülün on biri işaretlenir ve bunların altısı yanlış alarmdır. En sıkı uçta (> 10) yanlış alarm sıfırlanır ve dört sızıntı kaçar. Arada hiçbir eşik yoktur ki hem kaçırma hem yanlış alarm birlikte düşük olsun: > 4 eşiği üç yakalar, iki kaçırır ve üç yanlış alarm verir. F1 kaçırma ile yanlış alarmı birbirinden ayıramaz, çünkü ölçtüğü sayı kuralın sorduğu şeye yaklaşık bile değildir — dokuz sembolünün altısı kullanılan ortakBicim ile üç sembolünün ikisi ölü olan depoErisimi aynı ekseni paylaşır.

F2 taraması aynı ihlal kümesinde farklı bir yerde durur. > 0.4 ile > 0.6 arasında sonuç değişmez: dört yakalanan, bir kaçan, bir yanlış alarm. Bu düzlük iyi bir işaret sayılır — eşiğin tam değeri sonucu belirlemiyorsa kural eşiğe aşırı duyarlı değildir. F2’nin tek kaçırdığı katalogKoprusudur ve nedeni tanımın kendisindedir: yedi sembolünün yedisi de ithal edilir, yani ölü yüzey sıfırdır; mimarın sızıntı demesinin nedeni sembollerin kullanılmaması değil, hepsinin tek tüketiciye açılmış olmasıdır. F2’nin tek yanlış alarmı olayGunlugudur: dört sembolünün üçü grafiğin dışındaki işletim araçları için açıktır ve denetleyici o araçları görmez.

Kural kodun içinde tanımlanır, kuralın kendisi ise koddan çıkarılamaz. F2, mimarın etiketine F1’den çok daha yakındır ama ikisi de yaklaşıktır; hiçbir eşik katalogKoprusu ile olayGunlugu çiftini birden doğru sınıflandırmaz.

Kuralın Kendi Maliyeti

Son tablo kuralın bedelidir. F1 dört satırdır ve tam koşumda on iki dosya, 106 satır okur. F2 on iki satırdır ve tam koşumda 156 dosya, 1.378 satır okur — her modül için bütün dizini taradığından okuma on üç katına çıkar. Koşum süresi ortama bağlı olduğu için basılan nicelikler ortamdan bağımsız olanlardır: okunan dosya ve taranan satır. Modül sayısı arttığında F2’nin okuması karesel büyür, F1’inki doğrusal.

Buradan çıkan sonuç, kuralı ucuzlatmanın yolu ölçüyü bozmadan okuma sayısını düşürmektir: F2 bütün dosyaları modül başına değil bir kez okuyup ithal haritasını bellekte kurabilir. Ama bu ders için sayılması gereken asıl bedel koşum değil, bakımdır: F2 ithal biçimini ayrıştırdığı için ithal söz diziminin her yeni biçimi kuralı bozar; F1’in ayrıştırdığı tek şey export satırıdır.

Yanlış Alarmın Ürettiği Davranış

Yanlış alarm bir sayı değil, bir davranıştır. Bir kural yanlış alarm verdiğinde ekip önce inceler, sonra istisna ekler, sonunda kuralı kapatır. Aşağıdaki blok bunu modeller.

// devre.mjs — yanlis alarmin ekipte urettigi davranis: kural ucuncu alarmda devre disi kalir
import { readFileSync, readdirSync } from "node:fs";

const DIZIN = "modul";
const dosya = readdirSync(DIZIN);
const acikSembol = (a) => [...readFileSync(`${DIZIN}/${a}.mjs`, "utf8")
  .matchAll(/^export const (\w+)/gm)].map((m) => m[1]);
const F1 = (a) => acikSembol(a).length;
const F2 = (a) => {
  const acik = new Set(acikSembol(a));
  const kullanilan = new Set();
  for (const d of dosya) {
    for (const m of readFileSync(`${DIZIN}/${d}`, "utf8")
      .matchAll(/^import \{ ([^}]+) \} from "\.\/(\w+)\.mjs"/gm)) {
      if (m[2] === a) for (const s of m[1].split(", ")) kullanilan.add(s);
    }
  }
  return (acik.size - kullanilan.size) / acik.size;
};
const SIZINTI = new Set(["subeUcu", "katalogKoprusu", "ucretHesabi", "bildirimKuyrugu", "depoErisimi"]);

// YN6: yirmi yayimin her biri tek bir module dokunur ve kural yalniz dokunulan modulde kosar.
// YN7: ekip ucuncu yanlis alarmdan sonra kurali devre disi birakir (esik gevsetme yerine kapatma).
const YAYIM = ["ayarlar", "subeUcu", "oduncAkisi", "raporlama", "ucretHesabi", "ortakBicim",
  "katalogKoprusu", "olayGunlugu", "depoErisimi", "oduncAkisi", "bildirimKuyrugu", "uyelikKaydi",
  "raporlama", "subeUcu", "ayarlar", "kimlikKoprusu", "ucretHesabi", "ortakBicim",
  "depoErisimi", "katalogKoprusu"];

function kosu(islev, esik) {
  let alarm = 0, acik = true, yakalanan = 0, kacan = 0, kapandi = 0, kapandiktanSonra = 0;
  YAYIM.forEach((m, i) => {
    if (!acik) { if (SIZINTI.has(m)) { kacan += 1; kapandiktanSonra += 1; } return; }
    const isaret = islev(m) > esik;
    if (isaret && SIZINTI.has(m)) yakalanan += 1;
    else if (!isaret && SIZINTI.has(m)) kacan += 1;
    else if (isaret) {
      alarm += 1;
      if (alarm === 3) { acik = false; kapandi = i + 1; }
    }
  });
  return { alarm, yakalanan, kacan, kapandi, kapandiktanSonra };
}

const ihlalliYayim = YAYIM.filter((m) => SIZINTI.has(m)).length;
console.log(`${YAYIM.length} yayim, ${ihlalliYayim} tanesi sizintili bir module dokunuyor`);
console.log(`\n${"kural".padEnd(16)}${"yanlis alarm".padStart(14)}${"kapandigi yayim".padStart(17)}` +
  `${"yakalanan".padStart(11)}${"kacan".padStart(7)}${"kapandiktan sonra kacan".padStart(25)}`);
for (const [etiket, islev, esik] of [["F1 esik > 3", F1, 3], ["F2 esik > 0.5", F2, 0.5]]) {
  const r = kosu(islev, esik);
  console.log(`${etiket.padEnd(16)}${String(r.alarm).padStart(14)}` +
    `${(r.kapandi ? String(r.kapandi) : "kapanmadi").padStart(17)}` +
    `${`${r.yakalanan}/${ihlalliYayim}`.padStart(11)}${String(r.kacan).padStart(7)}` +
    `${String(r.kapandiktanSonra).padStart(25)}`);
}

// YN8: cevrilemeyen kural icin yerine konan sey her ucuncu sembolun elle okunmasidir
const sembol = dosya.flatMap((d) => acikSembol(d.replace(".mjs", "")));
const ornek = sembol.filter((_, i) => i % 3 === 2);
console.log(`\ncevrilemeyen kural ("disa acilan ad modulun isini anlatir"): ` +
  `${sembol.length} sembolun ${ornek.length} tanesi elle okunur, ${sembol.length - ornek.length} tanesi okunmaz`);
20 yayim, 9 tanesi sizintili bir module dokunuyor

kural             yanlis alarm  kapandigi yayim  yakalanan  kacan  kapandiktan sonra kacan
F1 esik > 3                  3                4        1/9      8                        8
F2 esik > 0.5                1        kapanmadi        7/9      2                        0

cevrilemeyen kural ("disa acilan ad modulun isini anlatir"): 66 sembolun 22 tanesi elle okunur, 44 tanesi okunmaz

Sonuç, eşik taramasının tek başına anlattığından farklıdır. F1 eşiği > 3 seçilerek kurulduğunda tarama tablosuna göre dört sızıntıyı yakalıyordu; yirmi yayımlık koşumda ise dördüncü yayımda kapanır ve kalan on altı yayım boyunca hiçbir şeye bakmaz. Dokuz ihlalli yayımın yalnız biri yakalanır, sekizi kaçar ve sekizinin sekizi de kural kapandıktan sonradır. F2 aynı koşumda tek yanlış alarm verir, hiç kapanmaz ve dokuz ihlalli yayımın yedisini yakalar.

Aradaki fark eşik seçiminden değil, yanlış alarm oranından doğar. Bir kural kapatıldığı andan sonra ölçtüğü değer sıfırdır; bu yüzden kuralın kalitesi tek koşumdaki yakalama sayısıyla değil, ekipte kaç yayım boyunca açık kaldığıyla ölçülür. Kuralın çevrilemeyen kısmı ise adıyla durur: dışa açılan adın modülün işini anlatıp anlatmadığını hiçbir işlev söylemez ve yerine konan örneklem, 66 sembolün 22’sini okur; 44’ü hiç okunmaz.

Özet

  • Aynı mimari kural iki uygunluk işlevine çevrildi: F1 yüzey büyüklüğü (dışa açılan sembol sayısı), F2 ölü yüzey oranı (hiç ithal edilmeyen sembollerin payı); ikisi de gerçek modül dosyalarını okur.
  • F1’in eşik taramasında hiçbir eşik iyi değildir: > 2 beş sızıntının beşini yakalar ve 6 yanlış alarm verir, > 10 yanlış alarmı sıfırlar ve 4 sızıntıyı kaçırır.
  • F2 aynı ihlal kümesinde > 0.4 ile > 0.6 arasında değişmez (4 yakalanan, 1 kaçan, 1 yanlış alarm) ve eşiğe duyarsızlık kuralın sağlamlık işaretidir; iki hatası da tanımdan doğar — katalogKoprusunu kaçırır (yedi sembolü de kullanılıyor), olayGunluguna yanlış alarm verir (üç sembolü grafiğin dışındaki araçlar için açık).
  • Kuralın bedeli F1 için 4 satır, 12 dosya ve 106 satır okuma; F2 için 12 satır, 156 dosya ve 1.378 satır okumadır; fark 13 kattır ve modül sayısıyla karesel büyür.
  • Yirmi yayımlık koşumda F1 üçüncü yanlış alarmında dördüncü yayımda kapanır ve 9 ihlalli yayımın 8’ini kaçırır; F2 tek yanlış alarmla açık kalır ve 7’sini yakalar.
  • Çevrilemeyen kural adıyla yazıldı (“dışa açılan ad modülün işini anlatır”) ve yerine konan örneklem 66 sembolün 22’sini okur.

Sonraki Adım

Bu dersin uygunluk işlevi tek bir modülün yüzeyine baktı ve komşularını yalnız sembol sayarken kullandı. Mimari kuralların bir bölümü ise tek modülde hiç görünmez: hangi modülün hangi modülü ithal edebileceği, kod tabanının grafiğinde durur ve ancak grafiğin tamamı okunarak denetlenebilir. Sonraki ders bu kuralı yazar: gerçek bir ithal grafiği üzerinde koşan bir katman denetleyicisi kurulur ve asıl soru denetleyicinin değil kuralın kendisidir — aynı katman kuralı izin listesi biçiminde de yasak listesi biçiminde de yazılabilir. İki biçim aynı grafikte koşturulur ve üç sayı karşılaştırılır: hangisi daha az yanlış alarm veriyor, hangisi kaç ihlali kaçırıyor ve kod tabanına yeni bir modül eklendiğinde kural kaç yerde güncelleniyor.

İ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