İçeriğe geç
academia.sh

Ders 02 / 18

Altıgen ve Soğan Mimari

Bağımlılığın içe doğru çevrildiği iki düzenin kural olarak karşılaştırılması: dört halkaya yerleşmiş ücretlendirme bağlamında on altı olası halka kenarının sayılması, altıgen ve soğan kurallarının yasakladığı kenar kümelerinin ayrı ayrı çıkarılması, aynı kod tabanındaki iki kusurdan her kuralın yalnız birini görmesi ve onarımdan sonra iki sayının da sıfıra inmesi.

İçindekiler

Katmanlı düzenin izinli kenar kümesinde bir kenar vardı: uygulama->altyapi. Kural, iş akışını kuran katmanın tabloya doğrudan uzanmasına izin veriyordu; sıralamada altyapı, alanın yanında bir başka aşağı katmandı. Bu biçimde kurulan bir sırada kitaplığın merkezinde alan modeli değil, isteğin geçtiği yol durur.

Bu ders sıralamayı tersine çeviren iki düzeni ele alır. İkisinin de ortak kuralı tek cümledir: bağımlılık okları daima içe, alana doğru bakar. Ayrıldıkları yer içerinin nasıl bölündüğüdür ve bu ayrım ölçülebilir bir sonuç verir — iki kuralın gördüğü kusurlar aynı değildir.

İki Düzenin Ayrıldığı Yer

Altıgen mimari, bağlantı noktası ve bağdaştırıcı Alan Odaklı Tasarım kursundaki Bağlantı Noktaları ve Bağdaştırıcılar dersinde kurulmuş ve ölçülmüştür: alanın dışa açıldığı yerler adlandırıldığında dış dünyaya giden doğrudan bağ sayısı 3’ten 0’a inmişti. Burada o ölçüm tekrarlanmıyor ve kavramlar yeniden tanımlanmıyor.

Yeni olan şudur. Altıgen düzen içeriyi bölmez: alan, bağlantı noktaları ve senaryolar tek bir içeri kümesidir, dışarıda ise bağdaştırıcılardan oluşan sırasız bir kuşak vardır. Bağdaştırıcılar birbirinin eşidir ve birbirini tanımaz. Soğan mimari (onion architecture) aynı içe bakma kuralını korur ama içeriyi sıralı halkalara böler: en içte alan modeli, onun üstünde alan servisleri, onun üstünde uygulama servisleri, en dışta bağdaştırıcılar. Kural halka sırasına göre yazılır: bir halka yalnız kendinden içteki halkaları ve kendi halkasını ithal edebilir.

İki kural aynı dört bölmeye uygulandığında farklı şeyler yasaklar, ve bu fark sayılabilir.

Halkalara Yerleşmiş Ücretlendirme Bağlamı

Ücretlendirme bağlamı dört dizine dağıtılıyor; dizin adı halka numarasını verir.

mkdir -p halkalar/alan-modeli halkalar/alan-servisleri halkalar/uygulama-servisleri halkalar/dis
// halkalar/alan-modeli/kademe.mjs — halka 0: agirlik kademesi degeri
export const kademe = (ustAgirlik, ucret) => ({ ustAgirlik, ucret: Math.round(ucret) });
// halkalar/alan-servisleri/indirim-servisi.mjs — halka 1: sozlesmeli indirim orani
const SOZLESME = { yok: 0, standart: 0.05, hacimli: 0.12 };

export const indirimServisi = {
  oran: (sozlesme, tutar) => (SOZLESME[sozlesme] ?? 0) + (tutar >= 20000 ? 0.03 : 0),
};
// halkalar/alan-modeli/ucret-kurali.mjs — halka 0: kural, indirim servisine uzaniyor
import { indirimServisi } from "../alan-servisleri/indirim-servisi.mjs";

const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;
const yuvarla = (t) => Math.round(t / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;

export const ucretKurali = {
  hesapla: (gonderi, kademeler, katsayi) => {
    const kademe = kademeler.find((k) => gonderi.agirlik <= k.ustAgirlik);
    if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
    const ham = kademe.ucret * katsayi;
    const oran = indirimServisi.oran(gonderi.sozlesme, ham);
    return Math.max(yuvarla(ham * (1 - oran)), ASGARI_UCRET);
  },
};
// halkalar/alan-servisleri/tarife-noktasi.mjs — halka 1: disa acilan iki nokta ve bekledigi adlar
export const NOKTALAR = { tarife: ["kademeler", "katsayi"], duyurular: ["gonder"] };

export const eksikAd = (cevre) => Object.entries(NOKTALAR)
  .flatMap(([n, adlar]) => adlar
    .filter((a) => typeof cevre?.[n]?.[a] !== "function")
    .map((a) => `${n}.${a}`));
// halkalar/uygulama-servisleri/teklif-akisi.mjs — halka 2: senaryonun sirasi
import { ucretKurali } from "../alan-modeli/ucret-kurali.mjs";
import { eksikAd } from "../alan-servisleri/tarife-noktasi.mjs";

export const teklifAkisi = (cevre) => {
  const eksik = eksikAd(cevre);
  if (eksik.length > 0) throw new TypeError(`karsilanmayan nokta: ${eksik.join(", ")}`);
  return {
    teklifVer: (gonderi) => {
      const tutar = ucretKurali.hesapla(gonderi, cevre.tarife.kademeler(), cevre.tarife.katsayi(gonderi.postaKodu));
      cevre.duyurular.gonder({ postaKodu: gonderi.postaKodu, tutar });
      return { tutar };
    },
  };
};
// halkalar/dis/tarife-bagdastiricisi.mjs — halka 3: dis satirlari halka 0 degerine cevirir
import { kademe } from "../alan-modeli/kademe.mjs";

const SATIRLAR = [[1, 49.9], [5, 84.9], [15, 149.9], [30, 249.9]];
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const tarifeBagdastiricisi = {
  kademeler: () => SATIRLAR.map(([agirlik, tl]) => kademe(agirlik, tl * 100)),
  katsayi: (postaKodu) => KATSAYI[POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak"],
};
// halkalar/dis/bildirim-bagdastiricisi.mjs — halka 3: duyuru noktasini karsilayan bagdastirici
const GIDEN = [];

export const bildirimBagdastiricisi = { gonder: (m) => GIDEN.push(m), sayi: () => GIDEN.length };
// halkalar/dis/masa.mjs — halka 3: liste istegini komsu bagdastiriciya soruyor
import { tarifeBagdastiricisi } from "./tarife-bagdastiricisi.mjs";

export const masa = (akis) => ({
  teklifIstegi: (govde) => akis.teklifVer({
    agirlik: Number(govde.agirlik), postaKodu: String(govde.postaKodu), sozlesme: govde.sozlesme,
  }),
  kademeIstegi: () => tarifeBagdastiricisi.kademeler(),
});

Yerleşimde iki satır bilinçli olarak yanlış konmuş durumda: ücret kuralı indirim servisini ithal ediyor ve masa, kademe listesini komşusu olan bağdaştırıcıdan istiyor. İkisi de tek başına bakıldığında masum görünür.

İki Kuralın Yasakladığı Kenarlar

Dört halka arasında 4×4=164 \times 4 = 16 olası kenar vardır. Araç önce iki kuralın bu on altı kenardan hangilerini yasakladığını çıkarır, sonra ağacın gerçek kenarlarını ithal satırlarından okuyup her iki kurala karşı sayar. İthal grafiğinden kenar çıkarma yordamı Tasarım İlkeleri kursundaki Bağımlılık Çizgesi Sağlığı dersinde kurulmuştu.

// halka-kurali.mjs — iki halka kuralinin yasakladigi kenarlari ve agactaki ihlalleri sayar
import { readdirSync, readFileSync } from "node:fs";
import { basename, dirname, join, normalize } from "node:path";

const HALKA = { "alan-modeli": 0, "alan-servisleri": 1, "uygulama-servisleri": 2, "dis": 3 };
const SOGAN = (i, j) => j <= i;
const ALTIGEN = (i, j) => j !== 3;
const KURALLAR = [["altigen", ALTIGEN], ["sogan", SOGAN]];

const tumKenarlar = [0, 1, 2, 3].flatMap((i) => [0, 1, 2, 3].map((j) => [i, j]));
const yasak = new Map(KURALLAR.map(([ad, izin]) =>
  [ad, new Set(tumKenarlar.filter(([i, j]) => izin(i, j) === false).map(([i, j]) => `${i}->${j}`))]));

console.log(`olasi halka kenari = ${tumKenarlar.length}`);
for (const [ad, kume] of yasak) console.log(`  ${ad.padEnd(8)} yasak kenar = ${kume.size}  ${[...kume].join(" ")}`);
const ortak = [...yasak.get("altigen")].filter((k) => yasak.get("sogan").has(k));
const birlesim = new Set([...yasak.get("altigen"), ...yasak.get("sogan")]);
console.log(`  ortak yasak = ${ortak.length}  ${ortak.join(" ")}`);
console.log(`  birlesik yasak = ${birlesim.size}`);

function halkaKenarlari(kok) {
  const kenar = new Map();
  for (const halka of readdirSync(kok).sort()) {
    for (const dosya of readdirSync(join(kok, halka)).sort()) {
      const metin = readFileSync(join(kok, halka, dosya), "utf8");
      for (const m of metin.matchAll(/^import\s.*?from\s+"(\.[^"]+)"/gm)) {
        const hedef = basename(dirname(normalize(join(kok, halka, m[1]))));
        const anahtar = `${HALKA[halka]}->${HALKA[hedef]}`;
        kenar.set(anahtar, `${halka}/${dosya} -> ${hedef}/${basename(m[1])}`);
      }
    }
  }
  return kenar;
}

for (const kok of process.argv.slice(2)) {
  const kenar = halkaKenarlari(kok);
  console.log(`\n${kok}/  halka kenari = ${kenar.size}  (${[...kenar.keys()].sort().join(" ")})`);
  for (const [ad, kume] of yasak) {
    const ihlal = [...kenar].filter(([k]) => kume.has(k));
    console.log(`  ${ad.padEnd(8)} ihlal = ${ihlal.length}`);
    for (const [k, kaynak] of ihlal) console.log(`    ${k}  ${kaynak}`);
  }
}
node halka-kurali.mjs halkalar
olasi halka kenari = 16
  altigen  yasak kenar = 4  0->3 1->3 2->3 3->3
  sogan    yasak kenar = 6  0->1 0->2 0->3 1->2 1->3 2->3
  ortak yasak = 3  0->3 1->3 2->3
  birlesik yasak = 7

halkalar/  halka kenari = 5  (0->1 2->0 2->1 3->0 3->3)
  altigen  ihlal = 1
    3->3  dis/masa.mjs -> dis/tarife-bagdastiricisi.mjs
  sogan    ihlal = 1
    0->1  alan-modeli/ucret-kurali.mjs -> alan-servisleri/indirim-servisi.mjs

Her Kuralın Kör Noktası

İlk üç satır iki kuralın büyüklüğünü veriyor. Altıgen kural on altı kenardan 4’ünü yasaklıyor: dış kuşağa giden her kenarı ve dış kuşağın kendi içindeki kenarı. Soğan kural 6’sını yasaklıyor: içeride de bir sıra ilan edildiği için dışa bakan her kenar yasaktır. Ortak yasak 3 kenar, yani iki kuralın anlaştığı şey yalnız “içeriden dışarıya bağ olmaz” cümlesidir. Birleşik yasak 7’ye çıkıyor; ikisinin yasakları iç içe değil, kesişiyor.

Bunun bedeli ağaçta görünüyor. Beş halka kenarından ikisi kusurluydu ve her kural yalnız birini gördü. Altıgen kural masa -> tarife-bagdastiricisi kenarını yakaladı: iki bağdaştırıcı birbirini tanıyor, oysa dış kuşak sırasızdır ve eşler birbirine bağlanmaz. Soğan kural bunu göremez, çünkü ikisi de aynı halkadadır ve halka içi kenar izinlidir. Buna karşılık soğan kural ucret-kurali -> indirim-servisi kenarını yakaladı: alan modeli, kendisinden dışta duran alan servisine uzanıyor. Altıgen kural bunu göremez, çünkü ikisi de “içeri” kümesindedir ve içerinin sırası yoktur.

Sonuç, iki kuralın hangisinin doğru olduğu değildir. İkisi de aynı yönü söyler ve ikisi de ötekinin gördüğü bir kusuru göremez.

İki Kenarın Kaldırılması

Onarım iki hareketten oluşuyor. Ücret kuralı indirim oranını ithal etmek yerine parametre olarak alıyor; oranı çağıran taraf, yani uygulama servisi hesaplıyor. Masa ise kademe listesini komşusundan değil akıştan istiyor.

cp -r halkalar onarilmis
cat > onarilmis/alan-modeli/ucret-kurali.mjs <<'EOF'
// onarilmis/alan-modeli/ucret-kurali.mjs — halka 0: indirim orani parametre olarak geliyor
const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;
const yuvarla = (t) => Math.round(t / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;

export const ucretKurali = {
  hamUcret: (gonderi, kademeler, katsayi) => {
    const kademe = kademeler.find((k) => gonderi.agirlik <= k.ustAgirlik);
    if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
    return kademe.ucret * katsayi;
  },
  hesapla: (ham, oran) => Math.max(yuvarla(ham * (1 - oran)), ASGARI_UCRET),
};
EOF
cat > onarilmis/uygulama-servisleri/teklif-akisi.mjs <<'EOF'
// onarilmis/uygulama-servisleri/teklif-akisi.mjs — halka 2: indirimi ve liste istegini kendisi yurutuyor
import { ucretKurali } from "../alan-modeli/ucret-kurali.mjs";
import { indirimServisi } from "../alan-servisleri/indirim-servisi.mjs";
import { eksikAd } from "../alan-servisleri/tarife-noktasi.mjs";

export const teklifAkisi = (cevre) => {
  const eksik = eksikAd(cevre);
  if (eksik.length > 0) throw new TypeError(`karsilanmayan nokta: ${eksik.join(", ")}`);
  return {
    teklifVer: (gonderi) => {
      const ham = ucretKurali.hamUcret(gonderi, cevre.tarife.kademeler(), cevre.tarife.katsayi(gonderi.postaKodu));
      const tutar = ucretKurali.hesapla(ham, indirimServisi.oran(gonderi.sozlesme, ham));
      cevre.duyurular.gonder({ postaKodu: gonderi.postaKodu, tutar });
      return { tutar };
    },
    kademeListele: () => cevre.tarife.kademeler(),
  };
};
EOF
cat > onarilmis/dis/masa.mjs <<'EOF'
// onarilmis/dis/masa.mjs — halka 3: liste istegini de akistan soruyor, komsusunu tanimiyor
export const masa = (akis) => ({
  teklifIstegi: (govde) => akis.teklifVer({
    agirlik: Number(govde.agirlik), postaKodu: String(govde.postaKodu), sozlesme: govde.sozlesme,
  }),
  kademeIstegi: () => akis.kademeListele(),
});
EOF
node halka-kurali.mjs onarilmis
olasi halka kenari = 16
  altigen  yasak kenar = 4  0->3 1->3 2->3 3->3
  sogan    yasak kenar = 6  0->1 0->2 0->3 1->2 1->3 2->3
  ortak yasak = 3  0->3 1->3 2->3
  birlesik yasak = 7

onarilmis/  halka kenari = 3  (2->0 2->1 3->0)
  altigen  ihlal = 0
  sogan    ihlal = 0

Halka kenarı 5’ten 3’e indi ve kalan üç kenar iki kuralda da izinli: uygulama servisi iki iç halkaya bakıyor, bağdaştırıcı en iç halkanın değerini üretiyor. Aynı ağaç artık iki kuralın birleşik yasağını da geçiyor.

Davranış ve Kapanış

Bir yeniden düzenlemenin davranışı koruduğu iddia edilmez, sınanır. Bileşim kökü halkaların dışındadır; aşağıdaki betik iki ağacı da kendisi kurar ve aynı gönderilerle çağırır. Ayrıca en iç halkadaki kural dosyasının ithal kapanışını iki ağaçta ölçer.

// esdeger-mi.mjs — bilesim koku halkalarin disindadir: iki agaci kurar, sonuc ve kapanis karsilastirir
import { readFileSync } from "node:fs";
import { dirname, join, normalize } from "node:path";
import { masa as bozukMasa } from "./halkalar/dis/masa.mjs";
import { teklifAkisi as bozukAkis } from "./halkalar/uygulama-servisleri/teklif-akisi.mjs";
import { tarifeBagdastiricisi as bozukTarife } from "./halkalar/dis/tarife-bagdastiricisi.mjs";
import { bildirimBagdastiricisi as bozukDuyuru } from "./halkalar/dis/bildirim-bagdastiricisi.mjs";
import { masa as onarilmisMasa } from "./onarilmis/dis/masa.mjs";
import { teklifAkisi as onarilmisAkis } from "./onarilmis/uygulama-servisleri/teklif-akisi.mjs";
import { tarifeBagdastiricisi as onarilmisTarife } from "./onarilmis/dis/tarife-bagdastiricisi.mjs";
import { bildirimBagdastiricisi as onarilmisDuyuru } from "./onarilmis/dis/bildirim-bagdastiricisi.mjs";

const GONDERILER = [
  { agirlik: "0.4", postaKodu: "34710", sozlesme: "yok" },
  { agirlik: "3", postaKodu: "06800", sozlesme: "standart" },
  { agirlik: "12", postaKodu: "65100", sozlesme: "hacimli" },
  { agirlik: "28", postaKodu: "35400", sozlesme: "yok" },
  { agirlik: "5", postaKodu: "34100", sozlesme: "hacimli" },
];

function kapanis(kok) {
  const gorulen = new Set(), yigit = [kok];
  while (yigit.length > 0) {
    const dosya = yigit.pop();
    if (gorulen.has(dosya)) continue;
    gorulen.add(dosya);
    for (const [, yol] of readFileSync(dosya, "utf8").matchAll(/^import[^"']*["'](\.[^"']+)["']/gm)) {
      yigit.push(normalize(join(dirname(dosya), yol)));
    }
  }
  return gorulen.size;
}

const bozuk = bozukMasa(bozukAkis({ tarife: bozukTarife, duyurular: bozukDuyuru }));
const onarilmis = onarilmisMasa(onarilmisAkis({ tarife: onarilmisTarife, duyurular: onarilmisDuyuru }));

let ayrilan = 0;
for (const g of GONDERILER) {
  const a = bozuk.teklifIstegi(g).tutar, b = onarilmis.teklifIstegi(g).tutar;
  if (a !== b) ayrilan += 1;
  console.log(`${g.agirlik.padStart(4)} kg  ${g.postaKodu}  ${g.sozlesme.padEnd(8)} ${String(a).padStart(6)}  ${String(b).padStart(6)}`);
}
console.log(`ayrilan sonuc = ${ayrilan} / ${GONDERILER.length}`);
console.log(`kademe istegi: ${bozuk.kademeIstegi().length} / ${onarilmis.kademeIstegi().length} kademe, duyuru = ${bozukDuyuru.sayi()} / ${onarilmisDuyuru.sayi()}`);
for (const kok of ["halkalar", "onarilmis"]) {
  console.log(`${kok}/alan-modeli/ucret-kurali.mjs ithal kapanisi = ${kapanis(`${kok}/alan-modeli/ucret-kurali.mjs`)}`);
}
node esdeger-mi.mjs
 0.4 kg  34710  yok        5000    5000
   3 kg  06800  standart  10900   10900
  12 kg  65100  hacimli   22950   22950
  28 kg  35400  yok       32700   32700
   5 kg  34100  hacimli    7450    7450
ayrilan sonuc = 0 / 5
kademe istegi: 4 / 4 kademe, duyuru = 5 / 5
halkalar/alan-modeli/ucret-kurali.mjs ithal kapanisi = 2
onarilmis/alan-modeli/ucret-kurali.mjs ithal kapanisi = 1

Beş gönderinin beşinde tutar aynı, iki ağaçta da beş duyuru gitti ve kademe listesi her iki yoldan dört satır döndü. Onarım davranışı değiştirmedi. Değişen şey en iç halkanın ithal kapanışıdır: 2 dosyadan 1 dosyaya indi, yani kuralı sınamak için artık indirim tablosunu yüklemek gerekmiyor. Kalite niteliği tarafındaki karşılığı budur — sürdürülebilirlik ölçüsü olarak en iç halkanın bağımsız yüklenebilmesi, iki kuralın ortak amacıdır; halka sırasının sıkılığı ise o amacın ne kadarının otomatik denetlenebildiğini belirler.

Özet

  • İki düzenin ortak kuralı bağımlılığın içe bakmasıdır; ayrıldıkları yer içerinin bölünüp bölünmediğidir: altıgende içeri tek küme ve dış kuşak sırasız, soğanda içeri sıralı halkalardır.
  • Dört halka arasındaki 16 olası kenardan altıgen kural 4’ünü, soğan kural 6’sını yasaklıyor; ortak yasakları 3 kenar, birleşik yasak 7.
  • Aynı ağaçtaki iki kusurdan altıgen kural yalnız 3->3 kenarını, soğan kural yalnız 0->1 kenarını gördü; her kural ötekinin yakaladığı kusura kördür.
  • Onarımdan sonra halka kenarı 5’ten 3’e, iki kuralın ihlali de 0’a indi; beş gönderinin beşinde tutar aynı kaldı.
  • En iç halkadaki kural dosyasının ithal kapanışı 2’den 1’e indi: kuralı yüklemek için artık başka bir halkayı yüklemek gerekmiyor.

Sonraki Adım

Bu dersin son sayısı bir soru bırakıyor: birleşik yasak 7 kenardı, ama hiçbir düzen o yedi kenarı birlikte yasaklamıyordu. Bir kural içerinin sırasını ilan ediyor ama dış kuşağı tek parça sayıyor; öteki dış kuşağı sıralamıyor ama içeriyi bölmüyor. İki kuralın birleşimi adlandırılmış bir düzen olarak yazıldığında ortaya çıkan yerleşimin adı temiz mimaridir. Sonraki ders bu birleşimi kurar ve iki yeni şeyi ölçer: sınırdan geçen verinin alan ve karakter sayısı ile denetim akışının yönü ile bağımlılık okunun yönünün ayrıldığı kenar sayısı.

İ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