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:
> 2beş sızıntının beşini yakalar ve 6 yanlış alarm verir,> 10yanlış alarmı sıfırlar ve 4 sızıntıyı kaçırır. - F2 aynı ihlal kümesinde
> 0.4ile> 0.6arası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.