Ders 08 / 10
Bağımlılık ve Katman Denetimi
Yapısal mimari kuralların gerçek bir ithal grafiği üzerinde sınanması: aynı katman kuralının izin listesi ve yasak listesi biçiminde yazılması, beş kural biçiminin bilinen ihlal kümesinde yakalanan, kaçan ve yanlış alarm sayılarının karşılaştırılması, süreli istisnaların yaşlanması ve yeni bir modül eklendiğinde kuralın kaç yerde güncellendiğinin sayılması.
İçindekiler
Önceki dersin uygunluk işlevi tek bir modülün dosyasına baktı. Mimari kuralların bir bölümü ise tek dosyada görünmez: hangi modülün hangi modülü ithal edebileceği ancak grafiğin tamamı okunarak denetlenir. Katman denetimi ve bağımlılık denetimi bu tür kuralların adıdır ve ikisi de aynı veriyi okur — kod tabanının ithal grafiği.
Bu grafiğin kendisi daha önce ölçüldü: servis sınırları kursu aynı türden bir ithal grafiğinden sınır ihlallerini saymıştı. Buradaki soru o sayım değildir. Buradaki soru kuralın kendisidir. Aynı katman kuralı en az iki biçimde yazılabilir — izin listesi (“şu kenar türleri serbesttir”) ve yasak listesi (“şu kenar türleri yasaktır”) — ve iki biçim aynı grafikte farklı sonuç verir. Ders bu farkı üç sayıyla ölçer: hangisi daha az yanlış alarm veriyor, hangisi kaç ihlali kaçırıyor, kod tabanına yeni bir modül girdiğinde kural kaç yerde güncelleniyor. Through-line bölgesel kütüphane ağıdır ve kurgudur.
Denetlenecek İthal Grafiği
YN9 — kod tabanı on üç modülden oluşur, her modül kendi katmanını kaynağında bildirir ve katmanlar giriş, uygulama, alan, altyapı ve ortak olmak üzere beştir. Gerekçe: katman denetimi ancak her modülün bir katmana atanmış olduğu bir kod tabanında koşabilir; katman bildiriminin kaynakta durması, denetleyicinin dışarıdan bir haritaya bağlı kalmamasını sağlar.
// kur.mjs — bolgesel kutuphane agi kod tabanini katmanlariyla diske yazar import { mkdirSync, writeFileSync } from "node:fs"; const KATMAN = { subeUcu: "giris", personelUcu: "giris", oduncAkisi: "uygulama", ucretAkisi: "uygulama", uyelikAkisi: "uygulama", oduncKurali: "alan", ucretKurali: "alan", katalogKurali: "alan", depoErisimi: "altyapi", katalogKoprusu: "altyapi", kimlikKoprusu: "altyapi", olayGunlugu: "altyapi", ortakBicim: "ortak", }; const KENAR = [ "subeUcu->oduncAkisi", "subeUcu->ucretAkisi", "subeUcu->ortakBicim", "personelUcu->uyelikAkisi", "personelUcu->oduncAkisi", "personelUcu->ortakBicim", "oduncAkisi->oduncKurali", "oduncAkisi->katalogKurali", "oduncAkisi->depoErisimi", "oduncAkisi->katalogKoprusu", "oduncAkisi->ortakBicim", "ucretAkisi->ucretKurali", "ucretAkisi->depoErisimi", "ucretAkisi->ortakBicim", "uyelikAkisi->kimlikKoprusu", "uyelikAkisi->ortakBicim", "oduncKurali->ortakBicim", "ucretKurali->ortakBicim", "katalogKurali->ortakBicim", "depoErisimi->ortakBicim", "olayGunlugu->ortakBicim", "katalogKoprusu->ortakBicim", "katalogKoprusu->katalogKurali", "kimlikKoprusu->ortakBicim", "subeUcu->katalogKoprusu", "ucretAkisi->oduncAkisi", // istisna kaydi olan iki kenar "oduncKurali->depoErisimi", "ucretKurali->katalogKoprusu", "depoErisimi->oduncAkisi", "subeUcu->depoErisimi", "subeUcu->katalogKurali", "ortakBicim->olayGunlugu", "oduncAkisi->kimlikKoprusu", "ucretAkisi->katalogKoprusu", ]; mkdirSync("kod", { recursive: true }); for (const ad of Object.keys(KATMAN)) { const ithal = KENAR.filter((k) => k.split("->")[0] === ad) .map((k) => `import { ${k.split("->")[1]}Islem } from "./${k.split("->")[1]}.mjs";`); writeFileSync(`kod/${ad}.mjs`, `// katman: ${KATMAN[ad]}\n${ithal.join("\n")}\n` + `export const ${ad}Islem = (x) => x;\n`); } const K = ["giris", "uygulama", "alan", "altyapi", "ortak"]; const sayi = {}; for (const k of KENAR) { const [a, b] = k.split("->"); sayi[`${KATMAN[a]}->${KATMAN[b]}`] = (sayi[`${KATMAN[a]}->${KATMAN[b]}`] ?? 0) + 1; } console.log(`${Object.keys(KATMAN).length} modul dosyasi, ${KENAR.length} ithal kenari`); console.log(`\nkatmandan katmana kenar sayisi (satir: ithal eden, sutun: ithal edilen)`); console.log(`${"".padEnd(10)}${K.map((k) => k.padStart(9)).join("")}`); for (const a of K) { console.log(`${a.padEnd(10)}${K.map((b) => String(sayi[`${a}->${b}`] ?? 0).padStart(9)).join("")}`); }
13 modul dosyasi, 34 ithal kenari
katmandan katmana kenar sayisi (satir: ithal eden, sutun: ithal edilen)
giris uygulama alan altyapi ortak
giris 0 4 1 2 2
uygulama 0 1 3 6 3
alan 0 0 0 2 3
altyapi 0 1 1 0 4
ortak 0 0 0 1 0
Matris kuralın hangi hücrelerde çiğnendiğini daha kural yazılmadan gösterir: alan satırında
altyapıya iki kenar, giriş satırında alana bir ve altyapıya iki kenar, ortak satırında
altyapıya bir kenar vardır. Kuralı yazmak bu hücreleri adlandırmaktır — ve adlandırmanın iki ayrı
yolu vardır.
Aynı Kuralın Beş Biçimi
Denetleyici dosyaları okur, // katman: satırından katmanı ve from "./..." satırlarından kenarı
çıkarır. Aynı kural beş biçimde yazılır: katman granülerliğinde izin listesi, buna eklenen istisna
listesi (süresi denetlenen ve denetlenmeyen), modül granülerliğinde izin listesi ve katman
granülerliğinde yasak listesi.
YN10 — mimar otuz dört kenarı elle okudu ve dokuzunu gerçek ihlal olarak işaretledi; iki kenarın süreli istisna kaydı vardır ve birinin süresi dolmuştur. Gerekçe: yakalama, kaçırma ve yanlış alarm ancak elle etiketlenmiş bir kümeye karşı sayılabilir; istisna kayıtları da bu kümenin parçasıdır, çünkü onaylanmış bir kenarı ihlal saymak yanlış alarmdır.
// katman.mjs — kod/ dizinindeki gercek ithal grafigini okur; ayni kural bes bicimde kosturulur import { readFileSync, readdirSync, writeFileSync } from "node:fs"; function grafik(dizin = "kod") { const katman = {}, kenar = []; for (const d of readdirSync(dizin)) { const ad = d.replace(".mjs", ""); const t = readFileSync(`${dizin}/${d}`, "utf8"); katman[ad] = t.match(/^\/\/ katman: (\w+)$/m)[1]; for (const m of t.matchAll(/from "\.\/(\w+)\.mjs"/g)) kenar.push(`${ad}->${m[1]}`); } return { katman, kenar }; } const { katman, kenar } = grafik(); const kat = (k) => `${katman[k.split("->")[0]]}->${katman[k.split("->")[1]]}`; // YN10: mimar otuz dort kenari elle okudu; asagidaki dokuzu gercek ihlaldir. Istisna kaydi olan // subeUcu->katalogKoprusu gecerlidir; ucretAkisi->oduncAkisi istisnasinin suresi dolmustur. const IHLAL = new Set([ "ucretAkisi->oduncAkisi", "oduncKurali->depoErisimi", "ucretKurali->katalogKoprusu", "depoErisimi->oduncAkisi", "subeUcu->depoErisimi", "subeUcu->katalogKurali", "ortakBicim->olayGunlugu", "oduncAkisi->kimlikKoprusu", "ucretAkisi->katalogKoprusu", ]); const CEYREK = 4; const ISTISNA = [ // kenar | biten ceyrek | gerekce ["subeUcu->katalogKoprusu", 5, "goc donemi: sube ucu eski katalog koprusunu okuyor"], ["ucretAkisi->oduncAkisi", 2, "gecici: ucret akisi odunc akisindan okuyor"], ]; const IZIN_KATMAN = ["giris->uygulama", "giris->ortak", "uygulama->alan", "uygulama->altyapi", "uygulama->ortak", "alan->ortak", "altyapi->alan", "altyapi->ortak"]; const YASAK_KATMAN = ["alan->altyapi", "alan->uygulama", "alan->giris", "altyapi->uygulama", "altyapi->giris", "uygulama->giris"]; const IZIN_MODUL = [ "subeUcu->oduncAkisi", "subeUcu->ucretAkisi", "subeUcu->ortakBicim", "personelUcu->uyelikAkisi", "personelUcu->oduncAkisi", "personelUcu->ortakBicim", "oduncAkisi->oduncKurali", "oduncAkisi->katalogKurali", "oduncAkisi->depoErisimi", "oduncAkisi->katalogKoprusu", "oduncAkisi->ortakBicim", "ucretAkisi->ucretKurali", "ucretAkisi->depoErisimi", "ucretAkisi->ortakBicim", "uyelikAkisi->kimlikKoprusu", "uyelikAkisi->ortakBicim", "oduncKurali->ortakBicim", "ucretKurali->ortakBicim", "katalogKurali->ortakBicim", "depoErisimi->ortakBicim", "olayGunlugu->ortakBicim", "katalogKoprusu->ortakBicim", "katalogKoprusu->katalogKurali", "kimlikKoprusu->ortakBicim", "subeUcu->katalogKoprusu", ]; writeFileSync("izin_modul.json", JSON.stringify(IZIN_MODUL)); const izinliKenar = (sureDenetle) => new Set(ISTISNA .filter(([, bitis]) => !sureDenetle || bitis >= CEYREK).map(([k]) => k)); const BICIM = { "izin-katman": { n: IZIN_KATMAN.length, f: (k) => !IZIN_KATMAN.includes(kat(k)) }, "izin-katman + istisna": { n: IZIN_KATMAN.length + ISTISNA.length, f: (k) => !IZIN_KATMAN.includes(kat(k)) && !izinliKenar(false).has(k) }, "izin-katman + sureli ist.": { n: IZIN_KATMAN.length + ISTISNA.length, f: (k) => !IZIN_KATMAN.includes(kat(k)) && !izinliKenar(true).has(k) }, "izin-modul": { n: IZIN_MODUL.length, f: (k) => !IZIN_MODUL.includes(k) }, "yasak-katman": { n: YASAK_KATMAN.length, f: (k) => YASAK_KATMAN.includes(kat(k)) }, }; console.log(`${kenar.length} kenar okundu; bilinen ihlal ${IHLAL.size}, istisna kaydi ${ISTISNA.length} ` + `(${ISTISNA.filter(([, b]) => b < CEYREK).length} tanesinin suresi dolmus)`); console.log(`\n${"kural bicimi".padEnd(27)}${"kural satiri".padStart(13)}${"isaretlenen".padStart(13)}` + `${"yakalanan".padStart(11)}${"kacan".padStart(7)}${"yanlis alarm".padStart(14)}`); for (const [ad, b] of Object.entries(BICIM)) { const isaret = kenar.filter(b.f); const y = isaret.filter((k) => IHLAL.has(k)).length; console.log(`${ad.padEnd(27)}${String(b.n).padStart(13)}${String(isaret.length).padStart(13)}` + `${`${y}/${IHLAL.size}`.padStart(11)}${String(IHLAL.size - y).padStart(7)}` + `${String(isaret.length - y).padStart(14)}`); } console.log(`\nkacan ihlaller (kural bicimi basina):`); for (const [ad, b] of Object.entries(BICIM)) { const kacan = [...IHLAL].filter((k) => !b.f(k)); console.log(` ${ad.padEnd(27)}${kacan.length ? kacan.map((k) => `${k} (${kat(k)})`).join("; ") : "-"}`); }
34 kenar okundu; bilinen ihlal 9, istisna kaydi 2 (1 tanesinin suresi dolmus) kural bicimi kural satiri isaretlenen yakalanan kacan yanlis alarm izin-katman 8 8 7/9 2 1 izin-katman + istisna 10 6 6/9 3 0 izin-katman + sureli ist. 10 7 7/9 2 0 izin-modul 25 9 9/9 0 0 yasak-katman 6 3 3/9 6 0 kacan ihlaller (kural bicimi basina): izin-katman oduncAkisi->kimlikKoprusu (uygulama->altyapi); ucretAkisi->katalogKoprusu (uygulama->altyapi) izin-katman + istisna ucretAkisi->oduncAkisi (uygulama->uygulama); oduncAkisi->kimlikKoprusu (uygulama->altyapi); ucretAkisi->katalogKoprusu (uygulama->altyapi) izin-katman + sureli ist. oduncAkisi->kimlikKoprusu (uygulama->altyapi); ucretAkisi->katalogKoprusu (uygulama->altyapi) izin-modul - yasak-katman ucretAkisi->oduncAkisi (uygulama->uygulama); subeUcu->depoErisimi (giris->altyapi); subeUcu->katalogKurali (giris->alan); ortakBicim->olayGunlugu (ortak->altyapi); oduncAkisi->kimlikKoprusu (uygulama->altyapi); ucretAkisi->katalogKoprusu (uygulama->altyapi)
İzin Listesi ile Yasak Listesinin Ayrımı
Tablo yaygın bir beklentiyi bozar. Yasak listesi sıfır yanlış alarm verir ve dokuz ihlalin altısını
kaçırır. Nedeni biçimin kendisindedir: yasak listesi yalnız sayılan kenar türlerini görür.
Listede alan->altyapi ve altyapi->uygulama yazılıdır, bu yüzden üç ihlal yakalanır; ama
giris->altyapi, giris->alan, ortak->altyapi ve aynı katman içindeki uygulama->uygulama
listede yoktur ve o kenarlar hiç görülmez. Yasak listesi ne kadar eksikse o kadar sessizdir ve
eksik olduğu hiçbir yerde belli olmaz.
İzin listesi ters çalışır: listede olmayan her şey ihlaldir. Katman granülerliğindeki izin
listesi sekiz satırla dokuz ihlalin yedisini yakalar. Kaçırdığı ikisi kuralın ifade gücünün
sınırıdır: oduncAkisi->kimlikKoprusu ve ucretAkisi->katalogKoprusu kenarları katman düzeyinde
serbest olan uygulama->altyapi çiftine düşer; ikisinin de yasak olduğunu söyleyen kural katmanla
değil modül adıyla yazılır. Buna karşılık izin listesinin bir yanlış alarmı vardır ve o alarm
onaylanmış bir göç kenarını işaretler.
İstisna listesi bu yanlış alarmı kaldırır ama kendi kusurunu getirir. Süresi denetlenmeyen istisna listesinde yakalama 7’den 6’ya düşer: süresi iki çeyrek önce dolmuş bir istisna hâlâ bir gerçek ihlali gizlemektedir. Süre denetlendiğinde yakalama 7’ye döner ve yanlış alarm sıfırda kalır. Bir istisna listesi, süresi denetlenmediği sürece kuralın kendisini aşındırır — ve aşınmanın büyüklüğü tam olarak süresi dolmuş istisna sayısı kadardır.
Modül granülerliğindeki izin listesi tabloda kusursuzdur: dokuz ihlalin dokuzu yakalanır, hiç kaçan ve hiç yanlış alarm yoktur. Bedeli satır sayısında görünür — sekiz satır yerine yirmi beş satır, yani kural artık mimarinin bir özeti değil, grafiğin tam kopyasıdır. Asıl bedel ise bu tabloda hiç görünmez; sonraki koşum onu ölçer.
Yeni Modül Girdiğinde
Kuralın bakım maliyeti kod tabanı durduğu sürece sıfırdır. Aşağıdaki blok kod tabanına yeni bir modül ekler ve aynı kuralları güncellenmemiş hâlleriyle koşturur.
// yeni.mjs — kod tabanina yeni bir modul eklenir; her kural bicimi kac yerde guncelleniyor import { readFileSync, readdirSync, writeFileSync } from "node:fs"; // YN11: sonraki ceyrekte kod tabanina tek bir yeni modul girer ve alti yeni ithal kenari acar const YENI = "rezervasyonAkisi"; const YENI_KENAR = [ // besi kurala uygun, biri ihlal `subeUcu->${YENI}`, `personelUcu->${YENI}`, `${YENI}->oduncKurali`, `${YENI}->depoErisimi`, `${YENI}->ortakBicim`, `${YENI}->personelUcu`, ]; writeFileSync(`kod/${YENI}.mjs`, `// katman: uygulama\n` + YENI_KENAR.filter((k) => k.startsWith(`${YENI}->`)) .map((k) => `import { ${k.split("->")[1]}Islem } from "./${k.split("->")[1]}.mjs";`).join("\n") + `\nexport const ${YENI}Islem = (x) => x;\n`); for (const k of YENI_KENAR.filter((k) => k.endsWith(`->${YENI}`))) { const a = k.split("->")[0]; const t = readFileSync(`kod/${a}.mjs`, "utf8"); writeFileSync(`kod/${a}.mjs`, t.replace(/\n/, `\nimport { ${YENI}Islem } from "./${YENI}.mjs";\n`)); } const katman = {}, kenar = []; for (const d of readdirSync("kod")) { const ad = d.replace(".mjs", ""); const t = readFileSync(`kod/${d}`, "utf8"); katman[ad] = t.match(/^\/\/ katman: (\w+)$/m)[1]; for (const m of t.matchAll(/from "\.\/(\w+)\.mjs"/g)) kenar.push(`${ad}->${m[1]}`); } const kat = (k) => `${katman[k.split("->")[0]]}->${katman[k.split("->")[1]]}`; const IZIN_KATMAN = ["giris->uygulama", "giris->ortak", "uygulama->alan", "uygulama->altyapi", "uygulama->ortak", "alan->ortak", "altyapi->alan", "altyapi->ortak"]; const YASAK_KATMAN = ["alan->altyapi", "alan->uygulama", "alan->giris", "altyapi->uygulama", "altyapi->giris", "uygulama->giris"]; const IZIN_MODUL_ESKI = JSON.parse(readFileSync("izin_modul.json", "utf8")); const uygunYeni = YENI_KENAR.filter((k) => IZIN_KATMAN.includes(kat(k))); const IZIN_MODUL_YENI = [...IZIN_MODUL_ESKI, ...uygunYeni]; const BICIM = { "izin-katman": { veri: IZIN_KATMAN, f: (k) => !IZIN_KATMAN.includes(kat(k)) }, "yasak-katman": { veri: YASAK_KATMAN, f: (k) => YASAK_KATMAN.includes(kat(k)) }, "izin-modul (eski)": { veri: IZIN_MODUL_ESKI, f: (k) => !IZIN_MODUL_ESKI.includes(k) }, "izin-modul (guncel)": { veri: IZIN_MODUL_YENI, f: (k) => !IZIN_MODUL_YENI.includes(k) }, }; const YENI_IHLAL = new Set([`${YENI}->personelUcu`]); console.log(`yeni modul: ${YENI} (katman: uygulama), ${YENI_KENAR.length} yeni kenar; ` + `${uygunYeni.length} tanesi katman kuralina uygun, ${YENI_KENAR.length - uygunYeni.length} tanesi ihlal`); console.log(`grafik simdi ${Object.keys(katman).length} modul ve ${kenar.length} kenar`); console.log(`\n${"kural bicimi".padEnd(22)}${"kural satiri".padStart(13)}${"guncellenen satir".padStart(19)}` + `${"yeni ihlal yakalandi".padStart(22)}${"yeni yanlis alarm".padStart(19)}`); for (const [ad, b] of Object.entries(BICIM)) { const isaret = YENI_KENAR.filter(b.f); const y = isaret.filter((k) => YENI_IHLAL.has(k)).length; const guncel = ad === "izin-modul (guncel)" ? uygunYeni.length : 0; console.log(`${ad.padEnd(22)}${String(b.veri.length).padStart(13)}${String(guncel).padStart(19)}` + `${`${y}/${YENI_IHLAL.size}`.padStart(22)}${String(isaret.length - y).padStart(19)}`); } console.log(`\nyeni modulun kendi dosyasinda her bicim icin ayni tek satir var: "// katman: uygulama"`);
yeni modul: rezervasyonAkisi (katman: uygulama), 6 yeni kenar; 5 tanesi katman kuralina uygun, 1 tanesi ihlal grafik simdi 14 modul ve 40 kenar kural bicimi kural satiri guncellenen satir yeni ihlal yakalandi yeni yanlis alarm izin-katman 8 0 1/1 0 yasak-katman 6 0 1/1 0 izin-modul (eski) 25 0 1/1 5 izin-modul (guncel) 30 5 1/1 0 yeni modulun kendi dosyasinda her bicim icin ayni tek satir var: "// katman: uygulama"
Modül granülerliğindeki izin listesinin gizli bedeli budur. Yeni modül tek satırlık bir katman bildirimiyle gelir; katman granülerliğindeki iki biçim hiçbir yerde güncellenmez ve yeni ihlali yakalar. Modül granülerliğindeki izin listesi ise güncellenmediğinde beş yanlış alarm üretir ve bu beş alarmın hepsi kurala tümüyle uygun kenarlar üzerinedir; güncellendiğinde alarm sıfırlanır ve kural yirmi beş satırdan otuza çıkar. Her yeni modül kuralı beş satır büyütür.
Bu, önceki dersin devre dışı bırakma koşumuyla aynı olgudur. Kurala uygun bir değişikliği engelleyen bir denetim, birkaç yayımdan sonra ya kapatılır ya da kural kendiliğinden onaylanan bir listeye dönüşür. Modül granülerliğindeki izin listesi tabloda kusursuz görünürken pratikte en kırılgan biçimdir, çünkü doğruluğu her yeni kenarda elle güncellenmesine bağlıdır.
Ölçülen üç biçim arasındaki seçim şu üç sayının bileşimidir: yasak listesi hiç bakım istemez ve altı ihlal kaçırır; katman izin listesi hiç bakım istemez, iki ihlal kaçırır ve ihlalleri yakalamak için istisna listesinin süre denetimine muhtaçtır; modül izin listesi hiçbir şey kaçırmaz ve her modülde beş satır bakım ister. Kuralın granülerliği yükseldikçe yakalama artar ve bakım maliyeti onunla birlikte artar; hiçbir biçim ikisini birden ucuzlatmaz.
Özet
- Katman denetimi gerçek dosyalardan okunan bir ithal grafiği üzerinde koşturuldu: 13 modül, 34 kenar, 9 elle etiketlenmiş ihlal ve süreli 2 istisna kaydı.
- Yasak listesi biçimi 6 satırla hiç yanlış alarm vermez ama 9 ihlalin 6’sını kaçırır; kaçırdığı kenar türleri listede sayılmadığı için sessizce geçer.
- Katman granülerliğindeki izin listesi 8 satırla 7 ihlal yakalar, 2 kaçırır ve onaylanmış bir göç kenarına 1 yanlış alarm verir; kaçırdığı ikisi katmanla değil modül adıyla yazılabilen kurallardır.
- Süresi denetlenmeyen istisna listesi yakalamayı 7’den 6’ya düşürür; süresi dolmuş tek bir istisna bir gerçek ihlali gizler, süre denetimi eklendiğinde yakalama 7’ye döner ve yanlış alarm sıfırdır.
- Modül granülerliğindeki izin listesi 25 satırla 9/9 yakalar ve hiç yanlış alarm vermez; bedeli yeni modülde ortaya çıkar — güncellenmediğinde kurala uygun 5 kenara yanlış alarm verir, güncellendiğinde 5 satır büyür.
- Kuralın granülerliği yükseldikçe yakalama artar ve bakım maliyeti onunla birlikte artar; katman bildirimi kaynakta durduğu için katman biçimleri yeni modülde 0 satır güncelleme ister.
Sonraki Adım
Bu üç derste kurallar yazıldı, denetime çevrildi ve denetimin yakaladığı ile kaçırdığı sayıldı. Hepsinin ortak varsayımı şudur: mimari sabittir ve kural onu korur. Oysa bu dersin son koşumu tam tersini gösterdi — kod tabanına bir modül girdiği anda kuralın kendisi de değişmek zorunda kaldı. Bir mimarinin ölçülmesi gereken niteliği yalnız bugün kurala uyup uymadığı değil, değişimi ne kadar ucuza kabul ettiğidir. Sonraki ders bu niteliği ele alır: değişimi ucuzlatan yapı kararlarının kendisi bir denetime çevrilebilir mi, çevrilebiliyorsa hangi sayı ölçülür ve çevrilemeyen kısmın yerine ne konur.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.