Ders 04 / 18
Bileşen Tabanlı Mimari
Birimin halkadan yayınlanabilir bileşene taşınması: her bileşenin dışa açtığı ve kapsadığı ad sayısının sayılması, aynı dosyaların iki bölünmesinde birlikte yayın oranının karşılaştırılması, yüzeyi atlayan bir tüketicinin dışa açılan ad sayısını nasıl yükselttiğinin ölçülmesi ve yeni bir yeteneğin düzenlettiği, eklettiği, yeniden yayınlattığı parçaların sayılması.
İçindekiler
Bir önceki dersin bütün ölçüleri halka üzerindeydi ve halka bir kural birimidir. Hangi dosya kümesinin birlikte paketlendiği, hangisinin ayrı sürümlenebildiği ve bir tüketicinin tek bir yetenek için kaç ada bağlandığı o ölçülerde görünmüyordu.
Bu ders birimi değiştirir. Bileşen (component) burada ayrı paketlenen, ayrı sürümlenen ve dışa açtığı adları bildirilmiş bir dosya kümesidir; Bileşen Tabanlı Geliştirme kursundaki bileşen ise ekranda bir parçayı çizen bir görünüm işleviydi — aynı sözcüğün iki ayrı birimi. Ölçüler de değişir: bir bileşenin dışa açtığı ad sayısı, kapsadığı ad sayısı, iki bileşenin birlikte yayınlanma oranı ve yeni bir yeteneğin düzenlettiği dosya sayısı.
Yayın Birimi Olarak Bileşen
Bileşen sınırının nereden geçeceği Tasarım İlkeleri kursundaki Bileşen Uyum İlkeleri ve Bileşen Bağlaşım İlkeleri derslerinde kurulmuştu: yeniden kullanım–yayın eşdeğerliği, ortak kapanış, ortak yeniden kullanım ve kararlı bağımlılıklar. Burada o ilkeler yeniden anlatılmıyor, bir düzen olarak uygulanıyor.
Düzenin taşıyıcı kararı şudur: her bileşen dışa açtığı adları tek bir dosyada bildirir, geri kalanı kendi içinde tutar. Bu karar iki sayıyı ayırır. Dışa açılan ad, bileşenin dışındaki bir dosyanın o bileşenden ithal ettiği addır. Kapsanan ad, bileşenin ihraç ettiği ama dışından kimsenin ithal etmediği addır. İkincisi serbestlik payıdır: kapsanan bir adı değiştirmek hiçbir tüketiciyi kırmaz.
Kitaplığın Bileşenleri
Kitaplık beş bileşene bölünüyor. Üçü ücretlendirme bağlamındadır (ortak, bolge, ucret),
biri teslimat operasyonundadır (rota), biri de ikisini tek bir istekte birleştirir (masa).
Her bileşenin dis.mjs dosyası o bileşenin yüzeyini bildirir.
mkdir -p bilesenler/ortak bilesenler/bolge bilesenler/ucret bilesenler/rota bilesenler/masa
// bilesenler/ortak/dis.mjs — ortak bileseni: para birimi cevrimi export const kurus = (lira) => Math.round(lira * 100); export const yazi = (k) => `${(k / 100).toFixed(2)} TL`;
// bilesenler/bolge/esleme.mjs — bolge bileseni ici: posta kodu esdegeri ve katsayilar export const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" }; export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 }; export const bolgeBul = (postaKodu) => POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak";
// bilesenler/bolge/dis.mjs — bolge bileseninin disa actigi yuzey import { KATSAYI, bolgeBul } from "./esleme.mjs"; export const bolge = (postaKodu) => bolgeBul(postaKodu); export const katsayi = (postaKodu) => KATSAYI[bolgeBul(postaKodu)];
// bilesenler/ucret/tarife.mjs — ucret bileseni ici: kademeler, asgari tutar, yuvarlama import { kurus } from "../ortak/dis.mjs"; export const KADEMELER = [[1, 49.9], [5, 84.9], [15, 149.9], [30, 249.9]] .map(([ustAgirlik, lira]) => ({ ustAgirlik, ucret: kurus(lira) })); export const ASGARI = kurus(39.9); export const yuvarla = (k) => Math.round(k / 50) * 50;
// bilesenler/ucret/kural.mjs — ucret bileseni ici: ucret kurali import { ASGARI, KADEMELER, yuvarla } from "./tarife.mjs"; export const ucret = (gonderi, katsayi) => { const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik); if (kademe === undefined) throw new RangeError("agirlik kademesi yok"); return Math.max(yuvarla(kademe.ucret * katsayi), ASGARI); };
// bilesenler/ucret/dis.mjs — ucret bileseninin disa actigi yuzey import { katsayi } from "../bolge/dis.mjs"; import { yazi } from "../ortak/dis.mjs"; import { ucret } from "./kural.mjs"; export const teklifVer = (gonderi) => { const tutar = ucret(gonderi, katsayi(gonderi.postaKodu)); return { tutar, gosterim: yazi(tutar) }; };
// bilesenler/rota/tasiyici.mjs — rota bileseni ici: tasiyici listesi ve kapsamlari export const TASIYICILAR = [ { ad: "kuzey", kapsam: ["yakin", "orta"], aktarma: 1 }, { ad: "genel", kapsam: ["yakin", "orta", "uzak"], aktarma: 2 }, ]; export const uygunlar = (b) => TASIYICILAR.filter((t) => t.kapsam.includes(b));
// bilesenler/rota/dis.mjs — rota bileseninin disa actigi yuzey import { bolge } from "../bolge/dis.mjs"; import { uygunlar } from "./tasiyici.mjs"; export const rotaKur = (gonderi) => { const secilen = uygunlar(bolge(gonderi.postaKodu)) .sort((x, y) => x.aktarma - y.aktarma)[0]; if (secilen === undefined) throw new RangeError("uygun tasiyici yok"); return { tasiyici: secilen.ad, aktarma: secilen.aktarma }; };
// bilesenler/masa/dis.mjs — masa bileseni: dis istegi iki bilesene dagitir import { rotaKur } from "../rota/dis.mjs"; import { teklifVer } from "../ucret/dis.mjs"; export const istek = (gonderi) => ({ ...teklifVer(gonderi), ...rotaKur(gonderi) });
Dışa Açılan ve Kapsanan Adlar
Araç her dosyanın ihraç ettiği adları ve ithal satırlarındaki adları okur, sonra aynı dosyaları
iki bölünmeye göre gruplar: yeteneğe göre (dizin adı) ve role göre (dis.mjs yüzey,
kural.mjs kural, geri kalanı veri). Dosyalar ve ithaller iki bölünmede aynıdır; değişen yalnız
sınırın nereden geçtiğidir.
// yuzey-olc.mjs — her birimin ihrac ettigi, disa actigi ve kapsadigi ad sayisini olcer import { readdirSync, readFileSync } from "node:fs"; import { join, normalize } from "node:path"; const IHRAC = /^export const (\w+)/gm; const ITHAL = /^import\s*\{([^}]*)\}\s*from\s+"([^"]+)"/gm; export const BOLUNME = { yetenek: (f) => f.dizin, rol: (f) => (f.ad === "dis.mjs" ? "arayuz" : f.ad === "kural.mjs" ? "kural" : "veri"), }; export function oku(kok) { const dosyalar = []; for (const dizin of readdirSync(kok).sort()) { for (const ad of readdirSync(join(kok, dizin)).sort()) { const metin = readFileSync(join(kok, dizin, ad), "utf8"); dosyalar.push({ yol: `${dizin}/${ad}`, dizin, ad, ihrac: [...metin.matchAll(IHRAC)].map((m) => m[1]), ithal: [...metin.matchAll(ITHAL)].map((m) => ({ adlar: m[1].split(",").map((s) => s.trim()).filter((s) => s !== ""), hedef: normalize(join(dizin, m[2])), })), }); } } return dosyalar; } if (import.meta.filename === process.argv[1]) { const kok = process.argv[2]; const dosyalar = oku(kok); const toplamIhrac = dosyalar.reduce((n, f) => n + f.ihrac.length, 0); console.log(`${kok}/ dosya = ${dosyalar.length}, toplam ihrac = ${toplamIhrac}`); for (const [bad, birimi] of Object.entries(BOLUNME)) { const ihrac = new Map(); const disa = new Map(); for (const f of dosyalar) { ihrac.set(birimi(f), (ihrac.get(birimi(f)) ?? 0) + f.ihrac.length); disa.set(birimi(f), disa.get(birimi(f)) ?? new Set()); } for (const f of dosyalar) { for (const i of f.ithal) { const hedef = dosyalar.find((d) => d.yol === i.hedef); if (hedef === undefined || birimi(hedef) === birimi(f)) continue; for (const n of i.adlar) disa.get(birimi(hedef)).add(n); } } console.log(` ${bad} bolunmesi (${ihrac.size} birim)`); let td = 0; for (const b of [...ihrac.keys()].sort()) { const d = disa.get(b).size; td += d; console.log(` ${b.padEnd(8)} ihrac ${String(ihrac.get(b)).padStart(2)}` + ` disa acilan ${String(d).padStart(2)} kapsanan ${String(ihrac.get(b) - d).padStart(2)}`); } console.log(` ${"toplam".padEnd(8)} ihrac ${String(toplamIhrac).padStart(2)}` + ` disa acilan ${String(td).padStart(2)} kapsanan ${String(toplamIhrac - td).padStart(2)}`); } }
node yuzey-olc.mjs bilesenler
bilesenler/ dosya = 9, toplam ihrac = 16
yetenek bolunmesi (5 birim)
bolge ihrac 5 disa acilan 2 kapsanan 3
masa ihrac 1 disa acilan 0 kapsanan 1
ortak ihrac 2 disa acilan 2 kapsanan 0
rota ihrac 3 disa acilan 1 kapsanan 2
ucret ihrac 5 disa acilan 1 kapsanan 4
toplam ihrac 16 disa acilan 6 kapsanan 10
rol bolunmesi (3 birim)
arayuz ihrac 7 disa acilan 1 kapsanan 6
kural ihrac 1 disa acilan 1 kapsanan 0
veri ihrac 8 disa acilan 6 kapsanan 2
toplam ihrac 16 disa acilan 8 kapsanan 8
On altı ihraç edilen addan yetenek bölünmesinde 6’sı dışa açılıyor ve 10’u kapsanıyor; rol
bölünmesinde 8’i dışa açılıyor, 8’i kapsanıyor. Rol bölünmesinde veri birimi sekiz adından
altısını dışa açmak zorundadır, çünkü kuralı ve yüzeyi ondan ayıran sınır tam o adların geçtiği
yerdedir. ucret bileşeni ise beş adından dördünü içinde tutuyor; dışarıya yalnız teklifVer
çıkıyor. masa bileşeninin dışa açılan adı 0: ona bağlı başka bir bileşen yok, yani Bileşen
Bağlaşım İlkeleri dersindeki kararsızlık ölçüsünün üst ucundadır.
Yüzeyi Atlayan Tüketici
Dışa açılan ad sayısı bileşenin tek başına özelliği değil, bileşen ile tüketicilerinin ortak özelliğidir. Aşağıdaki blok ağacın bir kopyasına yüzeyleri atlayıp komşularının içine bağlanan bir bileşen ekliyor.
cp -r bilesenler derin-agac mkdir -p derin-agac/derin cat > derin-agac/derin/dis.mjs <<'EOF' // derin-agac/derin/dis.mjs — yuzeyi atlayip komsularin icine baglanan tuketici import { KATSAYI, bolgeBul } from "../bolge/esleme.mjs"; import { yazi } from "../ortak/dis.mjs"; import { uygunlar } from "../rota/tasiyici.mjs"; import { ucret } from "../ucret/kural.mjs"; export const istek = (gonderi) => { const b = bolgeBul(gonderi.postaKodu); const tutar = ucret(gonderi, KATSAYI[b]); return { tutar, gosterim: yazi(tutar), tasiyici: uygunlar(b)[0].ad }; }; EOF node yuzey-olc.mjs derin-agac | head -9 node -e "import('./derin-agac/derin/dis.mjs').then(m => console.log('derin tuketici ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))"
derin-agac/ dosya = 10, toplam ihrac = 17
yetenek bolunmesi (6 birim)
bolge ihrac 5 disa acilan 4 kapsanan 1
derin ihrac 1 disa acilan 0 kapsanan 1
masa ihrac 1 disa acilan 0 kapsanan 1
ortak ihrac 2 disa acilan 2 kapsanan 0
rota ihrac 3 disa acilan 2 kapsanan 1
ucret ihrac 5 disa acilan 2 kapsanan 3
toplam ihrac 17 disa acilan 10 kapsanan 7
derin tuketici {"tutar":11450,"gosterim":"114.50 TL","tasiyici":"kuzey"}
Tüketici doğru çalışıyor ve yüzeyden geçen yolla aynı tutarı üretiyor: 11450 kuruş. Değişen şey
sınırdır. Dışa açılan ad 6’dan 10’a çıktı, kapsanan ad 10’dan 7’ye indi; bolge bileşeninin
kapsanan adı 3’ten 1’e düştü. Tek bir dosya, dokunmadığı üç bileşenin serbestlik payını harcadı.
Ortak yeniden kullanım ilkesinin ölçülebilir karşılığı budur: derin ithal, tüketiciyi bileşenin
sürüm sözleşmesinin dışında kalan adlara bağlar.
Birlikte Yayın Oranı
İkinci ölçü yayınla ilgilidir. Bir dosya değiştiğinde onu ithal eden her dosya yeniden sınanır ve yayınlanır, bu yüzden bir değişikliğin yeniden yayınlattığı birim kümesi ithal grafiğinden çıkarılabilir. İki birimin birlikte yayın oranı, ikisini birlikte yayınlatan dosya sayısının en az birini yayınlatan dosya sayısına oranıdır.
// yayin-olc.mjs — bir dosya degisince hangi birimlerin yeniden yayinlanmasi gerektigini sayar import { BOLUNME, oku } from "./yuzey-olc.mjs"; const [kok, hedef] = process.argv.slice(2); const dosyalar = oku(kok); const ithalEden = new Map(dosyalar.map((f) => [f.yol, []])); for (const f of dosyalar) { for (const i of f.ithal) if (ithalEden.has(i.hedef)) ithalEden.get(i.hedef).push(f.yol); } function kapanis(yol) { const gorulen = new Set([yol]); for (const y of gorulen) for (const k of ithalEden.get(y)) gorulen.add(k); return gorulen; } for (const [bad, birimi] of Object.entries(BOLUNME)) { const birimOf = new Map(dosyalar.map((f) => [f.yol, birimi(f)])); const etkilenen = dosyalar.map((f) => new Set([...kapanis(f.yol)].map((y) => birimOf.get(y)))); const birimler = [...new Set(birimOf.values())].sort(); console.log(`${kok}/ ${bad} bolunmesi (${birimler.length} birim)`); for (const b of birimler) { console.log(` ${b.padEnd(8)} dosya ${dosyalar.filter((f) => birimi(f) === b).length}` + ` yeniden yayinlatan dosya ${etkilenen.filter((e) => e.has(b)).length} / ${dosyalar.length}`); } let toplam = 0, cift = 0, enYuksek = ["yok", 0]; for (let i = 0; i < birimler.length; i += 1) { for (let j = i + 1; j < birimler.length; j += 1) { const [a, b] = [birimler[i], birimler[j]]; const oran = etkilenen.filter((e) => e.has(a) && e.has(b)).length / etkilenen.filter((e) => e.has(a) || e.has(b)).length; toplam += oran; cift += 1; if (oran > enYuksek[1]) enYuksek = [`${a}+${b}`, oran]; } } console.log(` cift ${cift}, ortalama birlikte yayin orani ${(toplam / cift).toFixed(2)}` + `, en yuksek ${enYuksek[0]} ${enYuksek[1].toFixed(2)}`); if (hedef !== undefined) { const kume = new Set([...kapanis(hedef)].map((y) => birimOf.get(y))); const dosyaSayisi = dosyalar.filter((f) => kume.has(birimi(f))).length; console.log(` ${hedef} degisince: ${kume.size} / ${birimler.length} birim,` + ` ${dosyaSayisi} / ${dosyalar.length} dosya (${[...kume].sort().join(", ")})`); } }
node yayin-olc.mjs bilesenler
bilesenler/ yetenek bolunmesi (5 birim) bolge dosya 2 yeniden yayinlatan dosya 2 / 9 masa dosya 1 yeniden yayinlatan dosya 9 / 9 ortak dosya 1 yeniden yayinlatan dosya 1 / 9 rota dosya 2 yeniden yayinlatan dosya 4 / 9 ucret dosya 3 yeniden yayinlatan dosya 6 / 9 cift 10, ortalama birlikte yayin orani 0.27, en yuksek masa+ucret 0.67 bilesenler/ rol bolunmesi (3 birim) arayuz dosya 5 yeniden yayinlatan dosya 9 / 9 kural dosya 1 yeniden yayinlatan dosya 3 / 9 veri dosya 3 yeniden yayinlatan dosya 4 / 9 cift 3, ortalama birlikte yayin orani 0.39, en yuksek arayuz+veri 0.44
Ortalama birlikte yayın oranı yetenek bölünmesinde 0.27, rol bölünmesinde 0.39. Daha keskin olan
sayı tablodadır: iki bölünmede de bir birim dokuz dosyanın dokuzunda yeniden yayınlanıyor, ama
yetenek bölünmesinde o birim 1 dosyalık masa, rol bölünmesinde 5 dosyalık arayuz. Her
değişiklikte yeniden yayınlanan parça beş katına çıkıyor. bolge bileşeni ise dokuz dosyanın
yalnız ikisinde yeniden yayınlanıyor; ortak kapanış ilkesinin karşılığı bu satırdır.
Yeni Bir Yetenek
Üçüncü ölçü uzantı maliyetidir. Ücretlendirmeye yakıt ek ücreti ekleniyor: oran kendi dosyasına giriyor, kural onu uyguluyor.
cp -r bilesenler yakitli cat > yakitli/ucret/yakit.mjs <<'EOF' // yakitli/ucret/yakit.mjs — ucret bileseni ici: yakit ek ucreti orani export const YAKIT_ORANI = 0.06; export const yakitEkle = (k) => k * (1 + YAKIT_ORANI); EOF cat > yakitli/ucret/kural.mjs <<'EOF' // yakitli/ucret/kural.mjs — ucret bileseni ici: ucret kurali, yakit ek ucretiyle import { ASGARI, KADEMELER, yuvarla } from "./tarife.mjs"; import { yakitEkle } from "./yakit.mjs"; export const ucret = (gonderi, katsayi) => { const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik); if (kademe === undefined) throw new RangeError("agirlik kademesi yok"); return Math.max(yuvarla(yakitEkle(kademe.ucret * katsayi)), ASGARI); }; EOF diff -rq bilesenler yakitli node yuzey-olc.mjs yakitli | head -8 node yayin-olc.mjs yakitli ucret/kural.mjs | grep degisince node -e "import('./bilesenler/masa/dis.mjs').then(m => console.log('yakitsiz ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))" node -e "import('./yakitli/masa/dis.mjs').then(m => console.log('yakitli ' + JSON.stringify(m.istek({ agirlik: 3, postaKodu: '06800' }))))"
Files bilesenler/ucret/kural.mjs and yakitli/ucret/kural.mjs differ
Only in yakitli/ucret: yakit.mjs
yakitli/ dosya = 10, toplam ihrac = 18
yetenek bolunmesi (5 birim)
bolge ihrac 5 disa acilan 2 kapsanan 3
masa ihrac 1 disa acilan 0 kapsanan 1
ortak ihrac 2 disa acilan 2 kapsanan 0
rota ihrac 3 disa acilan 1 kapsanan 2
ucret ihrac 7 disa acilan 1 kapsanan 6
toplam ihrac 18 disa acilan 6 kapsanan 12
ucret/kural.mjs degisince: 2 / 5 birim, 5 / 10 dosya (masa, ucret)
ucret/kural.mjs degisince: 2 / 3 birim, 6 / 10 dosya (arayuz, kural)
yakitsiz {"tutar":11450,"gosterim":"114.50 TL","tasiyici":"kuzey","aktarma":1}
yakitli {"tutar":12150,"gosterim":"121.50 TL","tasiyici":"kuzey","aktarma":1}
Değişiklik bir dosyayı düzenledi ve bir dosya ekledi. ucret bileşeninin ihraç ettiği ad 5’ten
7’ye çıktı ama dışa açılan adı 1’de kaldı: yeni iki ad kapsanan tarafa yazıldı, yüzey değişmedi
ve hiçbir tüketici düzenlenmedi. Yeniden yayınlanan parça yetenek bölünmesinde 5 birimin 2’si ve
10 dosyanın 5’i, rol bölünmesinde 3 birimin 2’si ve 10 dosyanın 6’sı. Teslimat operasyonu tarafına
dokunulmadı: iki koşumda tasiyici ve aktarma alanları aynı kaldı, yalnız tutar 11450’den
12150’ye çıktı.
Bedelin sayısı da burada. Beş bileşen beş yüzey dosyası ve beş sürüm demektir; tek ad ihraç eden
masa için bile ayrı bir yayın birimi tutuluyor. Yüzeyi daraltmak kapsanan ad sayısını yükseltir,
bileşen sayısını artırmak yayınlanacak birim sayısını yükseltir — sürdürülebilirlik ile dağıtımın
basitliği arasındaki ödünleşim bu iki sayıdır.
Özet
- Bileşen burada ayrı paketlenen, ayrı sürümlenen ve dışa açtığı adları bir yüzey dosyasında bildirilmiş bir dosya kümesidir; kapsanan ad sayısı o bileşenin serbestlik payıdır.
- Aynı dokuz dosya yeteneğe göre bölündüğünde 16 addan 6’sı dışa açılıyor, role göre bölündüğünde
8’i;
ucretbeş adından dördünü,verisekiz adından yalnız ikisini içinde tutuyor. - Yüzeyleri atlayan tek bir tüketici bileşeni dışa açılan adı 6’dan 10’a çıkardı ve
bolgebileşeninin kapsanan adını 3’ten 1’e indirdi; ürettiği tutar yüzeyden geçen yolla aynıydı. - Ortalama birlikte yayın oranı yeteneğe göre 0.27, role göre 0.39; her değişiklikte yeniden yayınlanan birim yetenek bölünmesinde 1 dosyalık, rol bölünmesinde 5 dosyalık.
- Yakıt ek ücreti 1 dosya düzenletti, 1 dosya eklettirdi, dışa açılan adı 6’da bıraktı ve yetenek bölünmesinde 5 birimin 2’sini, rol bölünmesinde 3 birimin 2’sini yeniden yayınlattı.
Sonraki Adım
Bileşen sınırı yeniden kullanım birimini verdi: ucret bileşenine yeni bir yetenek eklendiğinde
yüzey değişmedi ve dört bileşene dokunulmadı. Ama bu ölçülerin hepsi yayın zamanındadır. Bu
birimlerden hangisi program koşarken takılıp çıkarılabilir? masa bileşeni iki bileşeni adıyla
ithal ediyor, dolayısıyla üçüncü bir yeteneği eklemek için yine düzenlenmesi gerekir; bileşen
sınırı bu soruyu yanıtlamıyor. Sonraki ders birimleri iki sınıfa böler: değişmeyen ve tek başına
koşabilen bir çekirdek ile ona kayıtla takılan eklentiler. Ölçüler de değişir: çekirdeğin
eklentiler hakkında bildiği ad sayısı, yeni bir eklentinin çekirdekte düzenlettiği satır sayısı ve
eklentisiz çekirdeğin koşup koşmadığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.