Ders 19 / 19
Model Güdümlü Tasarım
Modelin gerçekleştirimle birlikte evrimi: alan uzmanının cümlesinde geçen yedi addan kaçının modelin sözlüğünde bulunduğunun sayılması, örtük kalan rota ayağı kavramı açığa çıkarıldığında köşeut dizi indekslemesi ile kural yazım yeri sayısındaki düşüşün ölçülmesi, iyileştirmenin satır bedeli ve üç raporun davranış eşitliğinin sınanması.
İçindekiler
Bu konunun dört dersi de aynı biçimde ilerledi: bir model kuruldu, bir ölçü seçildi, iki düzen aynı ölçüyle karşılaştırıldı. Kararların hiçbiri bir kez verilip bitmedi. Kullanım senaryosundaki karar sayısı, bağlantı noktalarının adları, okuma modelinin satırları ve olay türlerinin listesi alan uzmanıyla konuşuldukça değişir; her değişiklik de kodda bir bedel ister. Bir model iyileştirmesinin gerçekten iyileştirme olup olmadığı ancak o bedel sayıldığında bilinir.
Model güdümlü tasarım (model driven design) tam bu bağı zorunlu kılar: modelin kavramlarıyla gerçekleştirimin adları arasında birebir karşılık aranır, model değiştiğinde kod onunla birlikte değişir, kodda karşılığı ucuzca kurulamayan bir model iyileştirmesi de iyileştirme sayılmaz. Bağ tek yönlü değildir; gerçekleştirimde ortaya çıkan bir zorluk modelde eksik bir kavramın işaretidir.
Örtük Kalan Kavram
Operasyon uzmanı bir gecikme tartışmasında şu cümleyi kurar: rotanın her ayağı bir çıkış ile bir varış noktası arasında bir taşıyıcıya aittir; ayak planlanan günden sonra gerçekleşirse gecikme o ayağın taşıyıcısına yazılır. Cümlede yedi ad var: ayak, çıkış, varış, taşıyıcı, planlanan, gerçekleşen, gecikme.
Kitaplığın modelinde bu adların bir kısmı yok. Rota bir nokta dizisidir; taşıyıcılar, planlanan günler ve gerçekleşen günler ona köşeut ayrı dizilerdir. Ayak diye bir şey yoktur; her rapor onu indekslerden yeniden türetir. Böyle bir kavrama örtük kavram (implicit concept), onun açığa çıkarılmasına ise model kırılması (breakthrough) denir.
// ortak/kayit.mjs — ortuk modelin alan sozlugu: bir kayit, dort koseut dizi export function teslimatKaydi(id, rota, tasiyicilar, planlanan, gerceklesen) { return { id, rota, tasiyicilar, planlanan, gerceklesen }; }
Ölçümün iki sürümde aynı veriyi kullanması gerekir; üretici sabittir.
// ortak/uret.mjs — iki surumun de okudugu sabit kayit kumesi import { teslimatKaydi } from "./kayit.mjs"; const NOKTALAR = ["34", "06", "35", "01", "16"]; const TASIYICILAR = ["AN", "KG", "MT"]; export function uret(sayi) { let x = 29; const s = () => (x = (x * 48271 + 11) % 2147483647); return Array.from({ length: sayi }, (_, i) => { const nokta = 2 + (s() % 3); const bas = s() % NOKTALAR.length; const rota = Array.from({ length: nokta }, (_, j) => NOKTALAR[(bas + j) % NOKTALAR.length]); const tasiyicilar = [], planlanan = [], gerceklesen = []; let gun = 0, bitti = false; for (let a = 0; a < nokta - 1; a += 1) { tasiyicilar.push(TASIYICILAR[s() % TASIYICILAR.length]); gun += 1 + (s() % 3); planlanan.push(gun); if (bitti || s() % 6 === 0) { bitti = true; gerceklesen.push(null); continue; } const gecikme = s() % 3 === 0 ? 1 + (s() % 3) : 0; gerceklesen.push(gun + gecikme); } return teslimatKaydi(`T${i + 1}`, rota, tasiyicilar, planlanan, gerceklesen); }); }
Kavram Örtükken
Üç rapor aynı işi yapar: gecikme raporu geciken ayakları listeler, taşıyıcı karnesi gecikmeyi taşıyıcıya yazar, pano sıradaki ayağı ve teslimatın durumunu gösterir. Üçü de ayağı indekslerden türetir ve üçü de gecikme kuralını kendi içinde yazar.
// ortuk/raporlar.mjs — uc rapor, uc kez ayak turetmesi ve uc kez gecikme kurali export function gecikmeRaporu(kayitlar) { const satir = []; for (const k of kayitlar) { for (let i = 0; i < k.rota.length - 1; i += 1) { if (k.gerceklesen[i] === null) continue; if (k.gerceklesen[i] > k.planlanan[i]) { satir.push({ id: k.id, ayak: `${k.rota[i]}-${k.rota[i + 1]}`, tasiyici: k.tasiyicilar[i], gecikmeGunu: k.gerceklesen[i] - k.planlanan[i], }); } } } return satir; } export function tasiyiciKarnesi(kayitlar) { const karne = new Map(); for (const k of kayitlar) { for (let i = 0; i < k.rota.length - 1; i += 1) { const t = k.tasiyicilar[i]; const s = karne.get(t) ?? { tasiyici: t, ayak: 0, geciken: 0, toplamGecikme: 0 }; s.ayak += 1; if (k.gerceklesen[i] !== null && k.gerceklesen[i] > k.planlanan[i]) { s.geciken += 1; s.toplamGecikme += k.gerceklesen[i] - k.planlanan[i]; } karne.set(t, s); } } return [...karne.values()].sort((a, b) => a.tasiyici.localeCompare(b.tasiyici)); } export function pano(kayitlar) { return kayitlar.map((k) => { let siradaki = null, gecikmeli = false; for (let i = 0; i < k.rota.length - 1; i += 1) { if (k.gerceklesen[i] === null) { siradaki = `${k.rota[i]}-${k.rota[i + 1]}`; break; } if (k.gerceklesen[i] > k.planlanan[i]) gecikmeli = true; } const durum = siradaki === null ? "tamamlandi" : gecikmeli ? "gecikmeli" : "yolda"; return { id: k.id, siradakiAyak: siradaki, durum }; }); }
k.rota.length - 1 ifadesi üç yerde geçiyor ve her geçişinde aynı bilgiyi taşıyor: ayak
sayısı nokta sayısından bir eksiktir. Bu bilgi modelde bir yerde yazılı değil, üç rapor
gövdesinde tekrarlanıyor.
Kavramın Açığa Çıkarılması
Ayak bir değer nesnesi olur: kimliği yoktur, çıkışı, varışı, taşıyıcısı, planlanan ve gerçekleşen günü ile tanımlanır. Cümledeki kurallar da onun yöntemleridir.
// acik/alan/rota-ayagi.mjs — ayak: cikis, varis, tasiyici, planlanan ve gerceklesen gunu tasiyan deger nesnesi export class RotaAyagi { #cikis; #varis; #tasiyici; #planlanan; #gerceklesen; constructor({ cikis, varis, tasiyici, planlanan, gerceklesen }) { this.#cikis = cikis; this.#varis = varis; this.#tasiyici = tasiyici; this.#planlanan = planlanan; this.#gerceklesen = gerceklesen; } get ad() { return `${this.#cikis}-${this.#varis}`; } get tasiyici() { return this.#tasiyici; } tamamlandi() { return this.#gerceklesen !== null; } gecikti() { return this.tamamlandi() && this.#gerceklesen > this.#planlanan; } gecikmeGunu() { return this.gecikti() ? this.#gerceklesen - this.#planlanan : 0; } }
// acik/alan/rota.mjs — koseut dizileri ayak dizisine ceviren tek yer import { RotaAyagi } from "./rota-ayagi.mjs"; export class Rota { #ayaklar; static kayittan(k) { return new Rota(k.rota.slice(0, -1).map((cikis, i) => new RotaAyagi({ cikis, varis: k.rota[i + 1], tasiyici: k.tasiyicilar[i], planlanan: k.planlanan[i], gerceklesen: k.gerceklesen[i], }))); } constructor(ayaklar) { this.#ayaklar = ayaklar; } get ayaklar() { return this.#ayaklar; } siradakiAyak() { return this.#ayaklar.find((a) => a.tamamlandi() === false) ?? null; } gecikenAyaklar() { return this.#ayaklar.filter((a) => a.gecikti()); } }
// acik/raporlar.mjs — ayni uc rapor; turetme ve kural artik rapor dosyasinda degil import { Rota } from "./alan/rota.mjs"; export function gecikmeRaporu(kayitlar) { return kayitlar.flatMap((k) => Rota.kayittan(k).gecikenAyaklar().map((a) => ({ id: k.id, ayak: a.ad, tasiyici: a.tasiyici, gecikmeGunu: a.gecikmeGunu(), }))); } export function tasiyiciKarnesi(kayitlar) { const karne = new Map(); for (const k of kayitlar) { for (const a of Rota.kayittan(k).ayaklar) { const s = karne.get(a.tasiyici) ?? { tasiyici: a.tasiyici, ayak: 0, geciken: 0, toplamGecikme: 0 }; s.ayak += 1; if (a.gecikti()) { s.geciken += 1; s.toplamGecikme += a.gecikmeGunu(); } karne.set(a.tasiyici, s); } } return [...karne.values()].sort((a, b) => a.tasiyici.localeCompare(b.tasiyici)); } export function pano(kayitlar) { return kayitlar.map((k) => { const rota = Rota.kayittan(k), siradaki = rota.siradakiAyak(); const gecikmeli = rota.gecikenAyaklar().length > 0; const durum = siradaki === null ? "tamamlandi" : gecikmeli ? "gecikmeli" : "yolda"; return { id: k.id, siradakiAyak: siradaki === null ? null : siradaki.ad, durum }; }); }
Kalıcılık biçimi değişmedi: kayıt yine dört köşeut dizidir. Değişen tek şey modelin sözlüğüdür.
Ölçüm
Ölçüm dört sayı çıkarır. Birincisi hiza: cümledeki yedi addan kaçı modelin sözlüğünde bulunuyor. İkincisi köşeut dizi indekslemesi sayısı. Üçüncüsü kural başına yazım yeri. Dördüncüsü satır bedeli. Ardından üç rapor iki sürümde aynı kayıtlarla karşılaştırılır.
// evrim-say.mjs — ad ortusmesi, koseut indeksleme, kural yazim yeri, satir bedeli ve davranis esitligi import { readFileSync } from "node:fs"; import { uret } from "./ortak/uret.mjs"; import * as ortuk from "./ortuk/raporlar.mjs"; import * as acik from "./acik/raporlar.mjs"; const CUMLEDEKI_ADLAR = ["ayak", "cikis", "varis", "tasiyici", "planlanan", "gerceklesen", "gecikme"]; const KURAL = { "gecikme": /gerceklesen[^\n]*>[^\n]*planlanan/g, "tamamlanma": /gerceklesen[^\n]*(===|!==)[^\n]*null/g, "gecikme gunu": /gerceklesen[^\n]*-[^\n]*planlanan/g, }; const INDEKS = /(rota|tasiyicilar|planlanan|gerceklesen)\[/g; const SURUM = { "ortuk": { sozluk: ["ortak/kayit.mjs"], govde: ["ortuk/raporlar.mjs"] }, "acik": { sozluk: ["acik/alan/rota-ayagi.mjs", "acik/alan/rota.mjs"], govde: ["acik/raporlar.mjs"] }, }; const oku = (d) => readFileSync(d, "utf8"); const satir = (d) => oku(d).split("\n").filter((s) => s.trim() !== "").length; for (const [ad, { sozluk, govde }] of Object.entries(SURUM)) { const sozlukMetni = sozluk.map(oku).join("\n").toLowerCase(); const belirtec = new Set(sozlukMetni.match(/[a-z_][a-z0-9_]*/g)); const bulunan = CUMLEDEKI_ADLAR.filter((a) => [...belirtec].some((b) => b.startsWith(a))); const tumu = [...sozluk, ...govde].map(oku).join("\n"); console.log(`${ad} surum`); console.log(` alan sozlugu = ${sozluk.join(", ")} (${sozluk.reduce((s, d) => s + satir(d), 0)} satir)`); console.log(` cumledeki 7 addan alan sozlugunde bulunan = ${bulunan.length} (${bulunan.join(", ")})`); console.log(` koseut dizi indekslemesi = ${(tumu.match(INDEKS) ?? []).length}`); let toplamKural = 0; for (const [k, kalip] of Object.entries(KURAL)) { const n = (tumu.match(kalip) ?? []).length; toplamKural += n; console.log(` kural "${k}" yazim yeri = ${n}`); } console.log(` kural yazimi toplami = ${toplamKural}, rapor govdesi = ${govde.reduce((s, d) => s + satir(d), 0)} satir`); } const KAYITLAR = uret(60); const RAPORLAR = ["gecikmeRaporu", "tasiyiciKarnesi", "pano"]; let ayrilan = 0; for (const r of RAPORLAR) { const a = JSON.stringify(ortuk[r](KAYITLAR)), b = JSON.stringify(acik[r](KAYITLAR)); if (a !== b) ayrilan += 1; console.log(`${r.padEnd(15)} esit mi = ${a === b}, satir = ${JSON.parse(a).length}`); } console.log(`ayrilan rapor = ${ayrilan} / ${RAPORLAR.length}`); console.log(`tasiyici karnesi = ${JSON.stringify(acik.tasiyiciKarnesi(KAYITLAR))}`); console.log(`ornek pano satiri = ${JSON.stringify(acik.pano(KAYITLAR)[0])}`);
node evrim-say.mjs
ortuk surum
alan sozlugu = ortak/kayit.mjs (4 satir)
cumledeki 7 addan alan sozlugunde bulunan = 3 (tasiyici, planlanan, gerceklesen)
koseut dizi indekslemesi = 19
kural "gecikme" yazim yeri = 3
kural "tamamlanma" yazim yeri = 3
kural "gecikme gunu" yazim yeri = 2
kural yazimi toplami = 8, rapor govdesi = 43 satir
acik surum
alan sozlugu = acik/alan/rota-ayagi.mjs, acik/alan/rota.mjs (28 satir)
cumledeki 7 addan alan sozlugunde bulunan = 7 (ayak, cikis, varis, tasiyici, planlanan, gerceklesen, gecikme)
koseut dizi indekslemesi = 4
kural "gecikme" yazim yeri = 1
kural "tamamlanma" yazim yeri = 1
kural "gecikme gunu" yazim yeri = 1
kural yazimi toplami = 3, rapor govdesi = 27 satir
gecikmeRaporu esit mi = true, satir = 27
tasiyiciKarnesi esit mi = true, satir = 3
pano esit mi = true, satir = 60
ayrilan rapor = 0 / 3
tasiyici karnesi = [{"tasiyici":"AN","ayak":42,"geciken":13,"toplamGecikme":22},{"tasiyici":"KG","ayak":40,"geciken":7,"toplamGecikme":16},{"tasiyici":"MT","ayak":44,"geciken":7,"toplamGecikme":13}]
ornek pano satiri = {"id":"T1","siradakiAyak":null,"durum":"tamamlandi"}
Sayıların Okunması
Hiza sayısı 3’ten 7’ye çıktı. Örtük sürümün sözlüğünde cümledeki yedi addan yalnız üçü vardı; ayak, çıkış, varış ve gecikme kodda bir ad taşımıyordu. Açık sürümde yedisi de var ve hiçbiri bir raporun içine gömülü değil.
Köşeut dizi indekslemesi 19’dan 4’e indi. Kalan 4 indeksleme tek yerdedir: Rota.kayittan
yönteminde, kaydın dizilerini ayak nesnelerine çevirdiği satırlarda. “Ayak sayısı nokta
sayısından bir eksiktir” bilgisi artık modelde bir kez yazılı.
Kural yazımı toplamı 8’den 3’e indi ve dağılım daha önemlidir: gecikme kuralı üç yerden bir yere, tamamlanma kuralı üç yerden bir yere indi. Bu iki sayı doğrudan bir değişim maliyetidir. Gecikmenin tanımı değişirse — diyelim tolerans yarım gün eklenirse — örtük sürümde üç rapor gövdesi, açık sürümde bir değer nesnesi yöntemi düzenlenir.
Bedel de görünür durumda. İyileştirme 2 dosya ve 28 satır ekledi, rapor gövdesi 43 satırdan 27’ye indi; net artış 12 satır. Bedelin küçük kalmasının nedeni kalıcılık biçimine dokunulmamasıdır: göç işlemi ya da şema değişikliği gerektirmeyen bir iyileştirme ucuzdur.
Son satırlar davranışın korunduğunu gösteriyor: altmış kayıtta üç raporun üçü de aynı çıktıyı
üretti, ayrılan rapor 0. Taşıyıcı karnesinde AN 42 ayakta 13 gecikme ve 22 gün, KG 40
ayakta 7 gecikme ve 16 gün taşıyor.
İyileştirme Ölçütü
Her yeni sınıf bir model iyileştirmesi değildir. Ölçüt bu dört sayıdan çıkar: hiza arttı mı, türetme tek yere indi mi, kural yazımı azaldı mı, bedel bunların karşılığında makul mü?
Nokta kodunu saran bir Nokta sınıfı eklemek bu ölçütte kaybeder: cümlede “nokta” adı zaten
rota dizisiyle karşılanıyordu, indeksleme sayısı değişmez, hiçbir kural bir yerden bir yere
taşınmaz. Kalan tek etki 1 dosya ve bir dolaylılık basamağıdır — kazancı 0, bedeli 0’dan
büyük.
Ters yön de vardır. Gerçekleştirimde tekrar eden bir türetme, modelde eksik bir kavramın en
güvenilir işaretidir. Bu dersteki iyileştirme kod okunarak değil, k.rota.length - 1
ifadesinin üç yerde göründüğü fark edilerek bulundu. Model güdümlü tasarımın iki yönlü olması
budur: model kodu biçimlendirir, kodda biriken tekrar da modele bir soru sorar.
Özet
- Model güdümlü tasarım modelin kavramlarıyla gerçekleştirimin adları arasında birebir karşılık arar; kodda ucuzca kurulamayan bir iyileştirme iyileştirme sayılmaz.
- Alan uzmanının cümlesindeki yedi addan örtük sürümün sözlüğünde 3’ü, ayak kavramı açığa çıkarıldıktan sonra 7’si bulundu.
- Köşeut dizi indekslemesi 19’dan 4’e indi ve kalan dördü tek bir çevirim yönteminde toplandı; kural yazımı toplamı 8’den 3’e, gecikme kuralı üç yerden bir yere indi.
- Bedel 2 dosya ve 28 satır; rapor gövdesi 43’ten 27 satıra indiği için net artış 12 satır oldu ve kalıcılık biçimine dokunulmadı.
- Altmış kayıtta üç raporun üçü de aynı çıktıyı verdi; iyileştirme davranışı değiştirmedi.
- Kazancı 0 olan bir sınıf eklemek iyileştirme değildir; gerçekleştirimde tekrar eden türetme ise modelde eksik kavramın işaretidir.
Kurs Kapanışı
Kurs tek bir soruyla açıldı: bir kod tabanının yapısı alanın kendi ayrımlarından türetilebilir mi, ve türetildiğinde bu bir ölçüyle gösterilebilir mi? Üç konu bu soruyu üç ölçekte ele aldı ve her ders aynı alanın iki modelini aynı ölçüyle karşılaştırdı.
Alan Modeli konusu modelin içini kurdu. Her Yerde Aynı Dil dersinde beş kavramın on üç ad taşıdığı sayıldı; adlar birleştirildiğinde beş ada indi, iki ad arasındaki beş çeviri noktası 0’a düştü ve otuz gönderinin 19’unda ayrılan teklif ile ücret aynı sonuca geldi. Varlıklar ve Değer Nesneleri dersleri kimlikle ve değerle tanımlanmanın ölçütünü ayırdı. Toplamlar ve Kök dersinde tutarlılık sınırı çizildi: sınırı dışa açan modelde beş çağrı yolunun 4’ü indirim tavanını aşan bir gönderi üretebiliyordu, kökten geçen modelde 0’ı üretti. Alan Servisleri dersi nesneye ait olmayan davranışın yerini, Fabrikalar ve Depolar dersi yaratım ile erişimin alan diline uymasını belirledi. Alan Olayları dersinde uzmanın üç sorusundan genel kütük 1’ini, alan kütüğü 3’ünü yanıtladı. Konu, kuralı servise taşıyan düzenin bedeliyle kapandı: aynı kuralı yazan üç servis 36 gönderinin 24’ünde ayrıştı, kural nesnenin üzerine alındığında ayrışma 0.
Bağlamlar konusu modelin nerede geçerli olduğunu belirledi. Sınırlı Bağlam dersinde iki bağlamın tek sınıfta birleştirilmesinin bedeli sayıldı: birleşik sınıfta 105 alan çiftinin 68’i hiçbir senaryoda birlikte geçmiyordu, sınır çizildiğinde çift sayısı 56’ya ve birlikte geçmeyen çift 19’a indi; on değişmez tek sınıfta altı gerçek gönderinin altısını reddederken iki sınıfa dağıtıldığında altısı da geçti. Bağlam Haritası ilişki türlerini, Yozlaşma Önleyici Katman dersi dış modelin sızmasını ele aldı — sızan düzende dış sözlük iki çekirdek dosyanın ikisinde de geçiyordu, korumalı düzende 0 kaldı; bedel 2 dosya ve 19 satır. Paylaşılan Çekirdek ve Uyum Sağlayan dersi işbirliği kalıplarını, Alt Alanlar dersi çekirdek, destekleyici ve genel ayrımını kurdu.
Uygulama Mimarisine Bağlanma konusu modeli bir uygulamanın içine yerleştirdi. Kararlar kullanım senaryosundan toplama köküne indirildiğinde uygulama katmanının karar noktası 13’ten 2’ye, beş alan cümlesinin yazım yeri 10’dan 5’e indi. Alanın dışa açıldığı yerler bağlantı noktası olarak adlandırıldığında dış dünyaya giden doğrudan bağ 3’ten 0’a indi ve aynı alan dosyası iki bağdaştırıcı takımıyla koştu. Okuma modeli ayrıldığında pano sorgusunun gezdiği nesne 800 teslimatta 4621’den 5’e indi, karşılığında 5 komutluk bir gerilik ödendi. Olay dizisi saklandığında dört sorudan yanıtlanan 1’den 4’e çıktı; anlık görüntü aralığı 5’e düşürüldüğünde yeniden kurma için okunan olay 4699’dan 819’a indi. Bu son ders de modelin kendisinin durağan olmadığını gösterdi: cümledeki ad örtüşmesi 3’ten 7’ye, kural yazımı 8’den 3’e indi.
Ortak kalıp şudur: her ders bir hizasızlık buldu, ona bir sayı verdi, hizayı düzeltti ve bedeli aynı cinsten bir sayıyla yazdı. Hiçbir yerde “model daha ifadeli oldu” denmedi; ad sayısı, çağrı yolu, alan çifti, karar noktası, gezilen nesne ve okunan olay sayıldı.
Kursun bıraktığı soru bir basamak yukarıdadır. Buraya kadarki kararların hepsi bir uygulamanın içindeydi. Bağlam haritası birden çok bağlamı adlandırdı ama hepsi aynı çalışan programın parçasıydı. Bağlamlar ayrı programlara dağıtıldığında, ya da tek bir programda tutulmaya karar verildiğinde ölçüler değişir: ağ üzerinden geçen çağrı sayısı, bir isteğin dokunduğu birim sayısı, bir sürümün dağıtılabilmesi için birlikte yayınlanması gereken parça sayısı. Sonraki kurs Mimari Biçemler bu soruyu alır ve mimari düzenleri bu ölçülerle karşılaştırır: aynı kitaplığın tek parça, katmanlı ve dağıtık düzenlenişleri arasındaki fark nerede sayıya dökülür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.