İçeriğe geç
academia.sh

Ders 19 / 19

Bağımlılık Çizgesi Sağlığı

İthal grafiğinin kaynak dosyalardan çıkarılması ve bileşen düzeyinde denetlenmesi: derine aramayla döngülerin adlarıyla listelenmesi, katman sıralarına aykırı kenarların sayılması, döngünün çalışma zamanındaki karşılığının gösterilmesi ve onarımdan sonra iki sayının da sıfıra inmesi.

İçindekiler

Bir önceki ders yayın sırasının bozulmasından döngünün var olduğunu okudu ama yerini söyleyemedi: sıralanamayan üç bileşenin hangisi döngünün içinde, hangisi ona bağlı olduğu için listede kaldı, çıktıdan anlaşılmıyordu. Ayrıca oradaki bütün ölçüler elle yazılmış bir bileşen çizgesi üzerindeydi.

Gerçek bir kod tabanında çizge elle yazılmaz, ithal satırlarından çıkarılır. Bu ders o çıkarımı yapan, döngüleri adlarıyla listeleyen ve katman sırasına aykırı kenarları sayan bir araç yazar; aracı önce bozuk bir sürümde koşturur, sonra onarılmış sürümde iki sayının da sıfıra indiğini gösterir.

Denetlenecek Kaynak Ağacı

İthal grafiğinin dosya düzeyinde çıkarılması ve katman kuralının denetlenmesi Veri Erişim Katmanı ve İş Mantığı kursundaki Katman Sorumlulukları dersinde kurulmuştu. Buradaki iş farklıdır: aynı grafik bileşen düzeyine indirgenir ve üzerinde döngü aranır. Katman denetimi de bileşen sıralarına göre yapılır, dosya yollarına göre değil.

Aşağıdaki blok kitaplığın bozuk sürümünü kurar. Dokuz dosya, beş bileşen ve dört katman sırası var: para en altta (0), tarife ile ag bir üstte (1), ticari-kural onların üstünde (2), giriş dosyası en üstte (3).

mkdir -p bozuk
cat > bozuk/para.mjs <<'EOF'
import { KADEMELER } from "./tarife-tablosu.mjs";
export const kurus = (tl) => Math.round(tl * 100);
export const ENUST_AGIRLIK = KADEMELER.at(-1).ustAgirlik;
EOF
cat > bozuk/tarife-tablosu.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const KADEMELER = [{ ustAgirlik: 5, ucret: kurus(84.9) }, { ustAgirlik: 30, ucret: kurus(249.9) }];
EOF
cat > bozuk/katsayi.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
export const ASGARI = kurus(39.9);
EOF
cat > bozuk/ucret-kurali.mjs <<'EOF'
import { KADEMELER } from "./tarife-tablosu.mjs";
import { KATSAYI, ASGARI } from "./katsayi.mjs";
export const ucret = (agirlik, bolge) =>
  Math.max((KADEMELER.find((k) => agirlik <= k.ustAgirlik)?.ucret ?? 0) * KATSAYI[bolge], ASGARI);
EOF
cat > bozuk/indirim.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const indirimli = (tutar, oran) => Math.max(Math.round(tutar * (1 - oran)), kurus(39.9));
EOF
cat > bozuk/secim.mjs <<'EOF'
import { ucret } from "./ucret-kurali.mjs";
import { indirimli } from "./indirim.mjs";
import { TASIYICILAR } from "./tasiyici-listesi.mjs";
export const enUcuz = (agirlik, bolge, oran) => TASIYICILAR
  .map((t) => ({ ad: t.ad, tutar: indirimli(ucret(agirlik, bolge) * t.carpan, oran) }))
  .sort((a, b) => a.tutar - b.tutar)[0];
EOF
cat > bozuk/tasiyici-listesi.mjs <<'EOF'
import { enUcuz } from "./secim.mjs";
export const TASIYICILAR = [{ ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 }];
export const varsayilan = (agirlik) => enUcuz(agirlik, "orta", 0).ad;
EOF
cat > bozuk/rota-kurma.mjs <<'EOF'
import { TASIYICILAR } from "./tasiyici-listesi.mjs";
export const rota = (bolge) => [bolge, TASIYICILAR[0].ad];
EOF
cat > bozuk/dene.mjs <<'EOF'
import { enUcuz } from "./secim.mjs";
console.log("enUcuz(3, 'orta', 0) =", JSON.stringify(enUcuz(3, "orta", 0)));
EOF
ls bozuk | tr '\n' ' '
dene.mjs indirim.mjs katsayi.mjs para.mjs rota-kurma.mjs secim.mjs tarife-tablosu.mjs tasiyici-listesi.mjs ucret-kurali.mjs 

İki ithal satırı fazladan konmuş durumda: para.mjs tarife tablosunu ithal ediyor ve tasiyici-listesi.mjs seçim modülünü ithal ediyor. İkisi de tek başına bakıldığında masum görünür — para modülü en üst ağırlık kademesini bilmek istiyor, taşıyıcı listesi varsayılan taşıyıcıyı hesaplamak istiyor.

Döngünün Çalışma Zamanındaki Karşılığı

Bu iki satırın bedeli çizge üzerinde kalmıyor. Giriş dosyası çalıştırıldığında modüller birbirini bekliyor.

node bozuk/dene.mjs 2>&1 | grep -m1 "^ReferenceError"
ReferenceError: Cannot access 'KADEMELER' before initialization

Hata, döngüdeki modüllerden birinin gövdesi çalışırken diğerinin dışa açtığı sabitin henüz tanımlanmamış olmasından çıkıyor. Hangi modülün önce değerlendirileceği giriş noktasına bağlı olduğu için, aynı döngü başka bir giriş noktasında başka bir adla ya da hiç hata vermeden geçebilir. Döngünün en zor tarafı budur: belirtisi kararlı değildir.

Döngü Tespiti ve Katman Denetimi

Araç üç adım yapar. Önce her dosyanın ithal satırlarını okuyup bileşen kenarlarını çıkarır; aynı bileşen içindeki ithaller elenir. Sonra bu kenarlar üzerinde derine arama yapar — Veri Yapıları kursundaki Derine Arama dersinde kurulan üç renkli düzen: gri düğüm hâlen işlenmektedir, siyah düğüm bitmiştir, gri bir düğüme geri gelinmesi döngüdür. Son olarak her kenarın katman sıralarını karşılaştırır.

// cizge-sagligi.mjs — ithal grafiginden bilesen dongusunu ve katman ihlalini cikarir
import { readdirSync, readFileSync } from "node:fs";

const BILESEN = {
  "para.mjs": "para", "tarife-tablosu.mjs": "tarife", "katsayi.mjs": "tarife",
  "ucret-kurali.mjs": "ticari-kural", "indirim.mjs": "ticari-kural", "secim.mjs": "ticari-kural",
  "tasiyici-listesi.mjs": "ag", "rota-kurma.mjs": "ag", "dene.mjs": "giris",
};
const KATMAN = { para: 0, tarife: 1, ag: 1, "ticari-kural": 2, giris: 3 };

function bilesenKenarlari(dizin) {
  const kenar = new Set();
  for (const dosya of readdirSync(dizin).filter((d) => d.endsWith(".mjs")).sort()) {
    const metin = readFileSync(`${dizin}/${dosya}`, "utf8");
    for (const m of metin.matchAll(/^import\s.*?from\s+"\.\/([\w.-]+)"/gm)) {
      const [a, b] = [BILESEN[dosya], BILESEN[m[1]]];
      if (a !== b) kenar.add(`${a} ${b}`);
    }
  }
  return [...kenar].map((k) => k.split(" "));
}

function dongular(kenarlar) {
  const komsu = new Map();
  for (const [a, b] of kenarlar) komsu.set(a, [...(komsu.get(a) ?? []), b]);
  const durum = new Map(), yol = [], bulunan = [];
  const derineAra = (d) => {
    durum.set(d, "gri"); yol.push(d);
    for (const k of komsu.get(d) ?? []) {
      if (durum.get(k) === "gri") bulunan.push([...yol.slice(yol.indexOf(k)), k]);
      else if (durum.get(k) !== "siyah") derineAra(k);
    }
    yol.pop(); durum.set(d, "siyah");
  };
  for (const d of [...new Set(kenarlar.flat())].sort()) if (!durum.has(d)) derineAra(d);
  return bulunan;
}

for (const dizin of process.argv.slice(2)) {
  const kenarlar = bilesenKenarlari(dizin);
  const dongu = dongular(kenarlar);
  const ihlal = kenarlar.filter(([a, b]) => KATMAN[a] < KATMAN[b]);
  console.log(`${dizin}/  bilesen kenari = ${kenarlar.length}`);
  console.log(`  dongu = ${dongu.length}`);
  for (const d of dongu) console.log(`    ${d.join(" -> ")}`);
  console.log(`  katman ihlali = ${ihlal.length}`);
  for (const [a, b] of ihlal) console.log(`    ${a}(${KATMAN[a]}) -> ${b}(${KATMAN[b]})`);
}
node cizge-sagligi.mjs bozuk
bozuk/  bilesen kenari = 7
  dongu = 2
    para -> tarife -> para
    ag -> ticari-kural -> ag
  katman ihlali = 2
    para(0) -> tarife(1)
    ag(1) -> ticari-kural(2)

Bir önceki dersin yayın sırası üç bileşeni “sıralanamayan” diye vermişti; buradaki çıktı döngünün içindeki bileşenleri ayrı ayrı adlandırıyor ve ticari-kural’ın döngüde değil, döngüye bağlı olduğunu ortaya koyuyor.

İki sayı aynı iki kenarı işaret ediyor ama aynı şeyi ölçmüyor. Katman ihlali bir kural ihlalidir: sıralar önceden ilan edilmiştir, kenar onlara uymamıştır. Döngü ise kuraldan bağımsızdır; hiçbir katman ilan edilmemiş olsa da döngü yine bir kusurdur, çünkü yayın sırası ve değerlendirme sırası ondan bağımsız olarak bozulur. Katman kuralı olmayan bir kitaplıkta ikinci sayı hesaplanamaz, birincisi hesaplanır.

Onarım

İki ithal iki farklı hareketle kaldırılıyor. para.mjs en üst ağırlık kademesini ithal etmek yerine kademeleri parametre olarak alıyor — birinci dersteki hareketin aynısı, kural tabloyu ithal etmez. tasiyici-listesi.mjs içindeki varsayılan taşıyıcı hesabı ise seçim modülüne taşınıyor; hesap zaten seçime bağlıydı, listeye değil.

cp -r bozuk duzeltilmis
cat > duzeltilmis/para.mjs <<'EOF'
export const kurus = (tl) => Math.round(tl * 100);
export const enUstAgirlik = (kademeler) => kademeler.at(-1).ustAgirlik;
EOF
cat > duzeltilmis/tasiyici-listesi.mjs <<'EOF'
export const TASIYICILAR = [{ ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 }];
EOF
cat >> duzeltilmis/secim.mjs <<'EOF'
export const varsayilan = (agirlik) => enUcuz(agirlik, "orta", 0).ad;
EOF
node duzeltilmis/dene.mjs
node cizge-sagligi.mjs duzeltilmis
enUcuz(3, 'orta', 0) = {"ad":"ekonomik","tutar":9742}
duzeltilmis/  bilesen kenari = 5
  dongu = 0
  katman ihlali = 0

Giriş dosyası artık hata vermeden çalışıyor ve bir sonuç üretiyor. Bileşen kenarı yediden beşe, döngü ikiden sıfıra, katman ihlali ikiden sıfıra indi. Kaybedilen bir yetenek yok: varsayılan taşıyıcı hesabı hâlâ var, yalnız başka bir dosyada.

Sayının Eşik Olması

Bu iki sayının değeri, tek seferlik bir ölçüm olmamalarındadır. Döngüler tek bir büyük kararla değil, tek tek masum görünen ithal satırlarıyla birikir; bozuk sürümdeki iki satırın ikisi de yazıldıkları anda makul bir gerekçeye sahipti. Denetim her değişiklikte koşturulduğunda ve döngü sayısı için eşik sıfır kabul edildiğinde, gerekçe tartışması satır yazılmadan önce yapılır.

Aracın çıkardığı çizge, bir önceki dersin kararsızlık ölçüsünün de girdisidir. Aynı ithal grafiğinden üç sayı okunur: döngü sayısı, katman ihlali sayısı ve bileşen başına kararsızlık. Üçü birden sıfıra ya da beklenen yöne oturmuşsa bileşen sınırları çalışıyor demektir.

Özet

  • Bileşen çizgesi elle yazılmaz; ithal satırlarından çıkarılır ve aynı bileşen içindeki ithaller elenerek bileşen kenarlarına indirgenir.
  • Döngü, derine aramanın üç renkli düzeniyle bulunur: gri bir düğüme geri dönülmesi döngüdür ve yığıttaki yol döngünün üyelerini adlarıyla verir.
  • Bozuk sürümde iki ithal satırı iki döngü ve iki katman ihlali üretti; aynı döngü çalışma zamanında giriş noktasına bağlı bir başlatma hatasına dönüştü.
  • Katman ihlali ilan edilmiş bir kuralın ihlalidir, döngü ise kuraldan bağımsız bir kusurdur; katman kuralı olmayan bir kitaplıkta da döngü sayılabilir.
  • Onarımdan sonra bileşen kenarı 7’den 5’e, döngü ve katman ihlali 2’den 0’a indi; taşınan hesap kaybolmadı, yalnız bağımlılığın zaten var olduğu tarafa geçti.

Kurs Kapanışı

Kurs bir tasarımın iyi olup olmadığını birimin içine değil, birimler arasındaki bağa bakarak ölçtü ve her bölümde ölçüyü bir düzey yukarı taşıdı.

SOLID bölümü sınıf ve modül düzeyinde beş ölçüt kurdu: sorumluluğun iş miktarı değil değişim nedeni sayısı olarak sayılması, davranışın var olan kodu düzenlemeden genişletilmesi, bir alt tipin üst tipin sözleşmesini bozmadan yerine geçmesi, arayüzlerin istemcinin fiilen kullandığı kadarıyla sınırlanması ve bağımlılığın somut ayrıntıya değil soyutlamaya kurulması.

Bağlaşım ve Uyum bölümü modül çiftlerine geçti: iki modül arasındaki bağın gücünün ölçülmesi, bir modül içindeki adların birbiriyle ilgililiği, bir çağrının kaç nesne üzerinden geçtiğinin sınırlanması, veriyi çekip karar vermek yerine kararı veriye söylemek, durum değiştiren işlemlerle okuyanların ayrılması, değişim eksenlerinin ayrı ayrı kapsüllenmesi, somut tipe bağlanmanın maliyeti ve denetimin çağrılan tarafa devredilmesi.

Sınırlar ve Bileşen İlkeleri bölümü ölçeği dosyadan bileşene çıkardı: kararlı politikanın değişken ayrıntıdan değişim sıklığıyla ayrılması, sözleşme yüzeyinin ad ve alan sayısıyla ölçülüp daraltılması, sözleşmesi başkasının denetiminde olan bir aracın bağdaştırıcı arkasına alınması, bileşen sınırının birlikte değişen dosyalardan çıkarılması, bağların kararlı yöne akıp akmadığının sayılması ve bağımlılık çizgesinde döngü ile katman ihlalinin bulunması.

Kursun baştan sona koruduğu bir varsayım var ve kapanışta adlandırılması gerekiyor: ilkeler bir tasarımın nasıl olması gerektiğini söyler, ne yapılacağını değil. “Sınır dar olsun”, “bağ kararlı yöne aksın”, “birlikte değişenler bir arada dursun” cümlelerinin hiçbiri bir çözüm vermez; ölçüt verir. Oysa aynı sınır problemi farklı kod tabanlarında defalarca aynı biçimde çözülmüşse, o çözümün bir adı, bilinen bir uygulanış biçimi ve bilinen sonuçları vardır.

Sonraki kurs Tasarım Kalıpları bunu ele alır: klasik kalıpların çözdükleri probleme göre sınıflandırılması, bir kalıbın gereksiz karmaşıklık üretmeden — yani ilkelerin ölçüleri kötüleşmeden — uygulanması ve kurumsal uygulama kalıplarının veri erişim tasarımında kullanılması. Bu kursun ölçüleri orada da geçerlidir; kalıp, ölçüyü iyileştirdiği ölçüde kalıptı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