Ders 03 / 18
Temiz Mimari
İki halka kuralının tek bir yükleme birleştirilmesi: on altı halka kenarından yedisini yasaklayan kuralın yazılması, bileşim kökünden geçen sınırlarda denetim akışının yönü ile bağımlılık okunun ayrıldığı kenarların sayılması ve sınırdan geçen iki veri biçiminin erişilebilir ad ile karakter sayısıyla karşılaştırılması.
İçindekiler
Bir önceki ders iki halka kuralını aynı dört bölmeye uyguladı ve her birinin ötekinin yakaladığı kusura kör olduğunu ölçtü. Birleşik yasak 7 kenardı, ama hiçbir düzen o yediyi birlikte yasaklamıyordu: biri içerinin sırasını ilan ediyor ama dış kuşağı tek parça sayıyordu, öteki dış kuşağı sıralamıyordu.
Bu ders o birleşimi adlandırılmış bir düzen olarak kurar. Temiz mimari (clean architecture) halkaların sıralı olduğunu ve bağımlılık okunun daima içe baktığını birlikte söyler; yeni olan, kuralın bütün halka çiftleri üzerinde tek bir yüklem olarak yazılmasıdır. İki ölçü bu yazımdan çıkar: sınırdan geçen verinin alan ve karakter sayısı ile denetim akışı ve bağımlılık okunun ayrıldığı kenar sayısı.
Birleşmiş Kural
Politika ile ayrıntının ayrılması ve sınırın nereye çizileceği Tasarım İlkeleri kursundaki Politika ve Ayrıntı Ayrımı ile Sınırların Çizilmesi derslerinde, katman sorumlulukları da Veri Erişim Katmanı ve İş Mantığı kursundaki Katman Sorumlulukları dersinde kurulmuştu. Burada anlatılan ilke değil düzen.
Halka 0’da varlık nesneleri ile kural durur ve dışta hiçbir şeye bakmaz. Halka 1’de kullanım senaryoları: bir isteğin adımları, sırası ve senaryonun dışarıdan beklediği adların bildirimi. Halka 2 bağdaştırıcılardır; dış biçimi girdi modeline, çıktı modelini dış biçime çevirir. Halka 3 en dıştadır: ham satırlar ve giriş masası. Kural iki koşulun çarpımıdır — soğandan geleni, bir halkanın yalnız kendinden içteki ya da kendi halkasını ithal edebilmesi; altıgenden geleni, en dış halkaya hiçbir kenarın girememesi.
Dört Halkaya Yerleşmiş Kitaplık
Ücretlendirme bağlamı dört dizine dağıtılıyor; dizin adı halka numarasını verir. Bağlar parametreyle kuruluyor ve bileşim kökü halkaların dışındadır.
mkdir -p temiz/varliklar temiz/senaryolar temiz/bagdastiricilar temiz/altyapi
// temiz/varliklar/gonderi.mjs — halka 0: fiyatlanabilir gonderi ve ucretlenen agirligi const HACIM_BOLENI = 5000; export const gonderi = (girdi) => Object.freeze({ ...girdi, ucretlenenAgirlik: () => Math.max(girdi.agirlik, girdi.hacim / HACIM_BOLENI), });
// temiz/varliklar/ucret-kurali.mjs — halka 0: kural; kademe, katsayi ve oran disaridan gelir const ASGARI_UCRET = 3990; const YUVARLAMA_ADIMI = 50; const SIGORTA_ORANI = 0.004; export const ucretKurali = { hesapla: (g, kademeler, katsayi, indirimOrani) => { const agirlik = g.ucretlenenAgirlik(); const kademe = kademeler.find((k) => agirlik <= k.ustAgirlik); if (kademe === undefined) throw new RangeError("agirlik kademesi yok"); const ham = kademe.ucret * katsayi + Math.round(g.beyanDegeri * SIGORTA_ORANI); const net = Math.round((ham * (1 - indirimOrani)) / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI; return { tutar: Math.max(net, ASGARI_UCRET), kademe, ucretlenenAgirlik: agirlik }; }, };
Halka 1’in ilk dosyası kod değil bildirimdir: girdi ile çıktı modelinin alanları ve senaryonun geçitten beklediği adlar.
// temiz/senaryolar/sinir.mjs — halka 1: girdi modeli, cikti modeli ve gecidin bekledigi adlar export const GIRDI_ALANLARI = ["agirlik", "hacim", "bolge", "beyanDegeri", "sozlesme"]; export const CIKTI_ALANLARI = ["tutar", "paraBirimi", "kademeUstAgirlik", "ucretlenenAgirlik"]; export const GECIT_ADLARI = ["kademeler", "katsayi", "indirimOrani"]; export const eksikAd = (nesne, adlar) => adlar.filter((a) => typeof nesne?.[a] !== "function"); export const bildirilmemisAd = (nesne, izinli, onek = "") => Object.entries(nesne ?? {}).flatMap(([ad, deger]) => { const yol = onek === "" ? ad : `${onek}.${ad}`; if (deger !== null && typeof deger === "object") return bildirilmemisAd(deger, izinli, yol); return izinli.includes(yol) ? [] : [yol]; });
// temiz/senaryolar/teklif-senaryosu.mjs — halka 1: girdi modelinden cikti modeline import { gonderi } from "../varliklar/gonderi.mjs"; import { ucretKurali } from "../varliklar/ucret-kurali.mjs"; import { GECIT_ADLARI, eksikAd } from "./sinir.mjs"; export const teklifSenaryosu = (gecit) => { const eksik = eksikAd(gecit, GECIT_ADLARI); if (eksik.length > 0) throw new TypeError(`karsilanmayan gecit adi: ${eksik.join(", ")}`); return { calistir: (girdi) => { const g = gonderi(girdi); const sonuc = ucretKurali.hesapla(g, gecit.kademeler(), gecit.katsayi(g.bolge), gecit.indirimOrani(g.sozlesme)); return { tutar: sonuc.tutar, paraBirimi: "kurus", kademeUstAgirlik: sonuc.kademe.ustAgirlik, ucretlenenAgirlik: sonuc.ucretlenenAgirlik, }; }, }; };
// temiz/bagdastiricilar/teklif-denetleyicisi.mjs — halka 2: dis govdeyi girdi modeline, cikti modelini metne cevirir import { CIKTI_ALANLARI, GIRDI_ALANLARI, bildirilmemisAd } from "../senaryolar/sinir.mjs"; const DONUSTUR = { agirlik: Number, hacim: Number, bolge: String, beyanDegeri: Number, sozlesme: String }; export const teklifDenetleyicisi = (senaryo) => ({ teklifIstegi: (govde) => { const cikti = senaryo.calistir( Object.fromEntries(GIRDI_ALANLARI.map((a) => [a, DONUSTUR[a](govde[a])]))); const bildirilmemis = bildirilmemisAd(cikti, CIKTI_ALANLARI); if (bildirilmemis.length > 0) return { durum: 500, govde: `sozlesme disi ad: ${bildirilmemis.join(", ")}` }; return { durum: 200, govde: `${(cikti.tutar / 100).toFixed(2)} TL / ${cikti.ucretlenenAgirlik.toFixed(2)} kg` }; }, }); export const hataBicimleyicisi = (hata) => hata instanceof RangeError ? { durum: 422, govde: hata.message } : { durum: 500, govde: "beklenmeyen" };
// temiz/bagdastiricilar/tarife-gecidi.mjs — halka 2: ham satirlari kademe degerine cevirir import { GECIT_ADLARI, eksikAd } from "../senaryolar/sinir.mjs"; export const KAYNAK_ADLARI = ["satirlar", "katsayilar", "sozlesmeler"]; export const tarifeGecidi = (kaynak) => { const gecit = { kademeler: () => kaynak.satirlar() .map(([ustAgirlik, tl]) => ({ ustAgirlik, ucret: Math.round(tl * 100) })), katsayi: (bolge) => kaynak.katsayilar()[bolge] ?? 1.8, indirimOrani: (sozlesme) => kaynak.sozlesmeler()[sozlesme] ?? 0, }; const eksik = [...eksikAd(kaynak, KAYNAK_ADLARI), ...eksikAd(gecit, GECIT_ADLARI)]; if (eksik.length > 0) throw new TypeError(`karsilanmayan ad: ${eksik.join(", ")}`); return gecit; };
// temiz/altyapi/tarife-satirlari.mjs — halka 3: ham satirlar, bolge katsayilari, sozlesme oranlari const SATIRLAR = [[1, 49.9], [5, 84.9], [15, 149.9], [30, 249.9]]; const KATSAYILAR = { yakin: 1, orta: 1.35, uzak: 1.8 }; const SOZLESMELER = { yok: 0, standart: 0.05, hacimli: 0.12 }; export const tarifeSatirlari = { satirlar: () => SATIRLAR, katsayilar: () => KATSAYILAR, sozlesmeler: () => SOZLESMELER, };
// temiz/altyapi/masa.mjs — halka 3: disaridan geleni denetleyiciye verir, hatayi bicimleyiciden gecirir import { hataBicimleyicisi } from "../bagdastiricilar/teklif-denetleyicisi.mjs"; export const masa = (denetleyici) => ({ istek: (govde) => { try { return denetleyici.teklifIstegi(govde); } catch (hata) { return hataBicimleyicisi(hata); } }, });
Kuralın Yazımı ve İhlal
Araç kuralı iki kuraldan üretir, birleşimle aynı olduğunu sınar, sonra ağacın halka kenarlarını ithal satırlarından okuyup yasağa karşı sayar. Kenar çıkarma yordamı Tasarım İlkeleri kursundaki Bağımlılık Çizgesi Sağlığı dersinde kurulmuştu.
// temiz-kural.mjs — temiz kurali iki kuralin birlesimi olarak kurar ve agactaki ihlali sayar import { readdirSync, readFileSync } from "node:fs"; import { basename, dirname, join, normalize } from "node:path"; const HALKA = { varliklar: 0, senaryolar: 1, bagdastiricilar: 2, altyapi: 3 }; const ALTIGEN = (i, j) => j !== 3; const SOGAN = (i, j) => j <= i; const TEMIZ = (i, j) => SOGAN(i, j) && ALTIGEN(i, j); const tum = [0, 1, 2, 3].flatMap((i) => [0, 1, 2, 3].map((j) => [i, j])); const yasakla = (izin) => new Set(tum .filter(([i, j]) => izin(i, j) === false).map(([i, j]) => `${i}->${j}`)); export const YASAK = yasakla(TEMIZ); export function halkaKenarlari(kok) { const kenar = new Map(); for (const halka of readdirSync(kok).sort()) { for (const dosya of readdirSync(join(kok, halka)).sort()) { for (const m of readFileSync(join(kok, halka, dosya), "utf8") .matchAll(/^import\s.*?from\s+"(\.[^"]+)"/gm)) { const hedef = basename(dirname(normalize(join(kok, halka, m[1])))); kenar.set(`${HALKA[halka]}->${HALKA[hedef]}`, `${halka}/${dosya}`); } } } return kenar; } if (process.argv.length > 2) { const birlesim = new Set([...yasakla(ALTIGEN), ...yasakla(SOGAN)]); console.log(`olasi halka kenari = ${tum.length}`); console.log(`temiz kural: yasak = ${YASAK.size}, izinli = ${tum.length - YASAK.size}`); console.log(` yasak kenarlar = ${[...YASAK].join(" ")}`); console.log(` birlesimle ayni mi = ${YASAK.size === birlesim.size && [...YASAK].every((k) => birlesim.has(k))}`); for (const kok of process.argv.slice(2)) { const kenar = halkaKenarlari(kok); const ihlal = [...kenar].filter(([k]) => YASAK.has(k)); console.log(`\n${kok}/ halka kenari = ${kenar.size} (${[...kenar.keys()].sort().join(" ")})`); console.log(` ihlal = ${ihlal.length}`); for (const [k, kaynak] of ihlal) console.log(` ${k} ${kaynak}`); } }
Ağacın bir kopyasına iki küçük dosya ekleniyor: biri halka 2’de ham satırları kendisi ithal ediyor, öteki halka 0’da sınır bildirimini okuyor.
cp -r temiz kirik cat > kirik/bagdastiricilar/satir-onbellegi.mjs <<'EOF' // kirik/bagdastiricilar/satir-onbellegi.mjs — halka 2: ham satirlari kendisi ithal ediyor import { tarifeSatirlari } from "../altyapi/tarife-satirlari.mjs"; export const satirOnbellegi = () => tarifeSatirlari.satirlar(); EOF cat > kirik/varliklar/alan-ozeti.mjs <<'EOF' // kirik/varliklar/alan-ozeti.mjs — halka 0: sinir bildirimini kendisi okuyan yardimci import { CIKTI_ALANLARI } from "../senaryolar/sinir.mjs"; export const ciktiAlanSayisi = () => CIKTI_ALANLARI.length; EOF node temiz-kural.mjs temiz kirik
olasi halka kenari = 16
temiz kural: yasak = 7, izinli = 9
yasak kenarlar = 0->1 0->2 0->3 1->2 1->3 2->3 3->3
birlesimle ayni mi = true
temiz/ halka kenari = 4 (1->0 1->1 2->1 3->2)
ihlal = 0
kirik/ halka kenari = 6 (0->1 1->0 1->1 2->1 2->3 3->2)
ihlal = 2
2->3 bagdastiricilar/satir-onbellegi.mjs
0->1 varliklar/alan-ozeti.mjs
Yasak küme birleşimle birebir aynı çıktı; katkı yeni bir yasak eklemek değil, iki yasağı tek yüklemde toplamaktır. Temiz ağacın dört kenarı izinli dokuzun içinde. Eklenen iki dosya kenarı altıya çıkardı ve ikisi ihlal: biri en dış halkaya giren, öteki en iç halkadan çıkan kenar. Önceki dersin iki kuralı bunlardan birer tanesini görüyordu.
Denetim Akışı ile Bağımlılık Okunun Ayrılması
Halka kenarı bir isteğin hangi yoldan geçtiğini söylemez: kaynakta bağımlılık içe bakarken çalışma zamanında senaryo dışarıdaki geçidi çağırır. Betik bileşim kökünü kurar, her nesneyi bir sarmalayıcıdan geçirir, çağrı anında yığıtın tepesindeki halkayı çağıran olarak kaydeder ve kaydettiği her sınırı ithal kümesiyle karşılaştırır. Doğrudan ithalle yapılan çağrılar kaydedilmiyor: onlarda iki yön tanım gereği aynıdır.
// yon-ayrimi.mjs — bilesim kokunden gecen her sinirda denetim akisi ile bagimlilik okunu karsilastirir import { halkaKenarlari } from "./temiz-kural.mjs"; import { tarifeSatirlari } from "./temiz/altyapi/tarife-satirlari.mjs"; import { masa } from "./temiz/altyapi/masa.mjs"; import { teklifDenetleyicisi } from "./temiz/bagdastiricilar/teklif-denetleyicisi.mjs"; import { tarifeGecidi, KAYNAK_ADLARI } from "./temiz/bagdastiricilar/tarife-gecidi.mjs"; import { teklifSenaryosu } from "./temiz/senaryolar/teklif-senaryosu.mjs"; import { GECIT_ADLARI } from "./temiz/senaryolar/sinir.mjs"; const ithal = new Set(halkaKenarlari("temiz").keys()); const yigit = []; const cagri = new Map(); function sar(halka, nesne) { const sarili = {}; for (const [ad, fn] of Object.entries(nesne)) { sarili[ad] = (...args) => { const cagiran = yigit.at(-1); if (cagiran !== undefined && cagiran !== halka) { const k = `${cagiran}->${halka}`; cagri.set(k, (cagri.get(k) ?? 0) + 1); } yigit.push(halka); try { return fn(...args); } finally { yigit.pop(); } }; } return sarili; } const gecit = sar(2, tarifeGecidi(sar(3, tarifeSatirlari))); const denetleyici = sar(2, teklifDenetleyicisi(sar(1, teklifSenaryosu(gecit)))); const disMasa = sar(3, masa(denetleyici)); const GOVDELER = [ { agirlik: 3, hacim: 24000, bolge: "orta", beyanDegeri: 150000, sozlesme: "standart" }, { agirlik: 60, hacim: 90000, bolge: "uzak", beyanDegeri: 400000, sozlesme: "hacimli" }, ]; const yanit = GOVDELER.map((g) => disMasa.istek(g)); console.log(`istek = ${GOVDELER.length} yanit = ${yanit.map((y) => `${y.durum} ${y.govde}`).join(" | ")}`); const sayi = { ayni: 0, cevrilmis: 0, bagsiz: 0 }; console.log(`\nbilesim kokunden gecen sinir = ${cagri.size}`); for (const [k, n] of [...cagri].sort()) { const [i, j] = k.split("->"); const tur = ithal.has(k) ? "ayni" : ithal.has(`${j}->${i}`) ? "cevrilmis" : "bagsiz"; sayi[tur] += 1; const ok = tur === "ayni" ? k : tur === "cevrilmis" ? `${j}->${i}` : "yok"; console.log(` denetim ${k} bagimlilik ${ok} ${tur.padEnd(9)} cagri = ${n}`); } console.log(`ayni yon = ${sayi.ayni}, cevrilmis = ${sayi.cevrilmis}, bagsiz = ${sayi.bagsiz}`); console.log(`cevrilmis sinirlarda ice bildirilen ad: gecit ${GECIT_ADLARI.length}, kaynak ${KAYNAK_ADLARI.length}`);
node yon-ayrimi.mjs
istek = 2 yanit = 200 114.50 TL / 4.80 kg | 422 agirlik kademesi yok bilesim kokunden gecen sinir = 4 denetim 1->2 bagimlilik 2->1 cevrilmis cagri = 6 denetim 2->1 bagimlilik 2->1 ayni cagri = 2 denetim 2->3 bagimlilik 3->2 cevrilmis cagri = 6 denetim 3->2 bagimlilik 3->2 ayni cagri = 2 ayni yon = 2, cevrilmis = 2, bagsiz = 0 cevrilmis sinirlarda ice bildirilen ad: gecit 3, kaynak 3
Dört sınırın ikisinde iki yön aynı: masa denetleyiciyi hem çağırıyor hem ithal ediyor, denetleyici de senaryoyu. İkisinde ayrılıyor: senaryo geçidi çağırıyor ama geçit senaryoyu ithal ediyor; geçit ham kaynağı çağırıyor ama kaynağın halkası geçidin halkasını ithal ediyor. Ayrımı olanaklı kılan, çağrılan adların içeride bildirilmiş olmasıdır — üç ad halka 1’de, üç ad halka 2’de yazılı. Bedel de aynı sayıdadır: bu iki sınırda çalışma zamanı yolu ithal satırlarından okunamaz, yalnız bileşim köküne bakılarak bulunur.
Sınırdan Geçen Veri
İkinci ölçü sınırdan geçen verinin biçimidir. Senaryo, kuralın döndürdüğü nesneyi olduğu gibi dışa verebilir ya da bildirilmiş alanlara daraltabilir. Betik ikisini erişilebilir ad, bildirilmemiş ad ve JSON karakteri sayısıyla ölçüyor.
// sinir-verisi.mjs — sinirdan gecen iki veri bicimini erisilebilir ad ve karakter sayisiyla olcer import { gonderi } from "./temiz/varliklar/gonderi.mjs"; import { ucretKurali } from "./temiz/varliklar/ucret-kurali.mjs"; import { CIKTI_ALANLARI, bildirilmemisAd } from "./temiz/senaryolar/sinir.mjs"; import { teklifSenaryosu } from "./temiz/senaryolar/teklif-senaryosu.mjs"; import { teklifDenetleyicisi } from "./temiz/bagdastiricilar/teklif-denetleyicisi.mjs"; const sahteGecit = { kademeler: () => [{ ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 }], katsayi: (bolge) => (bolge === "orta" ? 1.35 : 1), indirimOrani: (sozlesme) => (sozlesme === "standart" ? 0.05 : 0), }; const GIRDI = { agirlik: 3, hacim: 24000, bolge: "orta", beyanDegeri: 150000, sozlesme: "standart" }; const g = gonderi(GIRDI); const ham = { ...ucretKurali.hesapla(g, sahteGecit.kademeler(), sahteGecit.katsayi(g.bolge), sahteGecit.indirimOrani(g.sozlesme)), gonderi: g }; const cikti = teklifSenaryosu(sahteGecit).calistir(GIRDI); for (const [ad, veri] of [["kuralin nesnesi", ham], ["cikti modeli", cikti]]) { const adlar = bildirilmemisAd(veri, []); const bildirilmemis = bildirilmemisAd(veri, CIKTI_ALANLARI); console.log(`${ad.padEnd(16)} erisilebilir ad = ${String(adlar.length).padStart(2)}` + ` bildirilmemis = ${String(bildirilmemis.length).padStart(2)}` + ` json karakteri = ${String(JSON.stringify(veri).length).padStart(3)}`); for (let i = 0; i < bildirilmemis.length; i += 4) { console.log(` bildirilmemis: ${bildirilmemis.slice(i, i + 4).join(", ")}`); } } const yanlis = teklifDenetleyicisi({ calistir: () => ham }); const dogru = teklifDenetleyicisi(teklifSenaryosu(sahteGecit)); console.log(`\nkuralin nesnesi disa verilirse -> durum ${yanlis.teklifIstegi(GIRDI).durum}`); const y = dogru.teklifIstegi(GIRDI); console.log(`cikti modeli disa verilirse -> durum ${y.durum} ${y.govde}`);
node sinir-verisi.mjs
kuralin nesnesi erisilebilir ad = 10 bildirilmemis = 8 json karakteri = 174 bildirilmemis: kademe.ustAgirlik, kademe.ucret, gonderi.agirlik, gonderi.hacim bildirilmemis: gonderi.bolge, gonderi.beyanDegeri, gonderi.sozlesme, gonderi.ucretlenenAgirlik cikti modeli erisilebilir ad = 4 bildirilmemis = 0 json karakteri = 81 kuralin nesnesi disa verilirse -> durum 500 cikti modeli disa verilirse -> durum 200 114.50 TL / 4.80 kg
Kuralın nesnesinde dışarıdan erişilebilen 10 ad var ve sekizi çıktı sözleşmesinde bildirilmemiş: ikisi kademe nesnesinin, altısı varlık nesnesinin iç alanları. Erişilebilir ad 10’dan 4’e, JSON karakteri 174’ten 81’e iniyor.
Fark boyut değil, bilme yükümlülüğüdür. Varlık nesnesi sınırdan geçtiğinde halka 2, halka 0’ın alan adlarını okur; kural bir alanı yeniden adlandırdığında bağdaştırıcı da değişir. Bu bağ ithal grafiğinde görünmez, çünkü ortada ithal yoktur — bağ nesnenin biçimindedir. Denetleyicinin çıktıyı bildirilmiş alanlara karşı sınaması bunu koşum anında yakalıyor: yanıt 500’e karşı 200.
Ödünleşimin Sayıları
Kazanç iki kalite niteliğinde ölçüldü. Sürdürülebilirlikte: sekiz dosyalı ağacın dört halka
kenarı izinli dokuzun içinde ve ihlal 0, eklenen iki küçük dosya ihlali 2’ye çıkardı.
Sınanabilirlikte: halka 1’den çıkan kenarlar 1->0 ve 1->1 olduğu için senaryo elle yazılmış
üç işlevlik bir geçitle koşuyor.
Bedel aynı sayılardadır: dört sınırın ikisi çevrilmiş ve her çevrilmiş sınır içeride bir bildirim ile dışarıda onu karşılayan bir tanım istiyor. Kenar kümesi ne kadar sıkıysa ihlal o kadar erken yakalanır, çevirme noktası sayısı da o kadar yüksek olur.
Özet
- Kural iki koşulun çarpımıdır: bir halka yalnız kendinden içteki ya da kendi halkasını ithal edebilir ve en dış halkaya hiçbir kenar giremez; on altı kenardan yedisi yasaklanır ve yasak küme iki halka kuralının birleşimiyle birebir aynıdır.
- Temiz ağacın dört kenarında ihlal 0; eklenen iki dosya en dış halkaya giren ve en iç halkadan çıkan iki kenar üretti, ihlal 2’ye çıktı.
- Bileşim kökünden geçen dört sınırın ikisinde denetim akışı ile bağımlılık oku ayrıldı; ayrımı çağrılan altı adın içeride bildirilmesi olanaklı kıldı.
- Kuralın nesnesi sınırdan geçtiğinde erişilebilir ad 10 ve bildirilmemiş ad 8, çıktı modeli geçtiğinde 4 ve 0; JSON karakteri 174’ten 81’e indi.
Sonraki Adım
Bu dersin bütün ölçüleri halka üzerindeydi. Halka bir kural birimidir, yayın birimi değildir: sekiz dosyanın hangi kümesinin birlikte paketlendiği, hangisinin ayrı sürümlenebildiği ve bir tüketicinin tek yetenek için kaç dosyaya bağlandığı hiçbir ölçüde görünmüyor. Sonraki ders birimi halkadan bileşene taşır: aynı kitaplığı dağıtılabilir birimlere böler, her bileşenin dışa açtığı ad sayısı ile iki bileşenin birlikte değişme oranını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.