Ders 24 / 30
İşlem Betiği ve Alan Modeli
İş mantığının iki düzenlenişi: her senaryoyu baştan sona yürüten işlem betiği ile kuralı veriyi taşıyan nesnenin üzerine koyan alan modeli, aynı kuralın kaç yerde uygulandığının sayılması, iki düzenin aynı girdilerde eşitliğinin sınanması ve alan modelinin dosya, ad ve dolaylılık bedelinin tartılması.
İçindekiler
Davranışsal kalıplar konusu, kalıp bedelinin kalıp kazancının altına düştüğü noktayı gösterdi. Buraya kadarki bütün kalıplar nesne düzeyinde çalışıyordu: bir sorumluluk alınıyor, ayrı bir nesneye veriliyor ve bedeli dolaylılık basamağıyla ödeniyordu.
Bu konu ölçeği bir basamak yukarı çıkarır. Soru artık bir sorumluluğun hangi nesneye verileceği değil, bir uygulamanın katman düzeninde iş mantığının nereye yerleştiği ve kalıcılıkla arasındaki sınırın hangi kalıplarla kurulduğudur. Bu kalıpların çoğu Veri Erişim Katmanı kursunda kurulmuş ve orada başarım ölçüleriyle sınanmıştı. Burada aynı kalıplar katalog düzeyinde ele alınır: her biri hangi problemi çözer, çözümü nedir, hangi sonuçları doğurur ve ne zaman uygulanmaz.
Problem: Kural Kaç Yerde Durur
Gönderi ücretlendirme kitaplığında üç senaryo var: yeni gönderi kaydı açmak, var olan bir gönderiye indirim eklemek, iade ücreti hesaplamak. Üçü de aynı iki kuralı kullanır: indirim oranlarının toplamı bir tavanı aşamaz, net ücret asgari ücretin altına inemez.
Kuralın nereye yazıldığı iki düzenleme doğurur. İşlem betiği (transaction script) her senaryoyu baştan sona yürüten bir yordam yazar; kural senaryonun içinde durur. Alan modeli (domain model) kuralı veriyi taşıyan nesnenin üzerine koyar; senaryo yalnız sırayı kurar.
mkdir -p betik alan
İşlem Betiği
// betik/senaryolar.mjs — uc senaryo, her biri kurali kendi icinde uygular const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]]; const KATSAYI = { "34": 1, "06": 1.35, "65": 1.8 }; const ASGARI_UCRET = 3990; const INDIRIM_TAVANI = 0.4; const taban = (agirlik) => KADEME.find(([ust]) => agirlik <= ust)[1]; const katsayi = (postaKodu) => KATSAYI[postaKodu.slice(0, 2)] ?? 1.8; const toplamOran = (indirimler) => indirimler.reduce((t, i) => t + i.oran, 0); export function yeniGonderi(girdi) { const ham = Math.round(taban(girdi.agirlik) * katsayi(girdi.postaKodu)); const oran = Math.min(toplamOran(girdi.indirimler), INDIRIM_TAVANI); return { ...girdi, ham, net: Math.max(Math.round(ham * (1 - oran)), ASGARI_UCRET) }; } export function indirimEkle(gonderi, indirim) { const indirimler = [...gonderi.indirimler, indirim]; const oran = Math.min(toplamOran(indirimler), INDIRIM_TAVANI); return { ...gonderi, indirimler, net: Math.max(Math.round(gonderi.ham * (1 - oran)), ASGARI_UCRET) }; } export function iadeUcreti(gonderi) { const oran = Math.min(toplamOran(gonderi.indirimler), INDIRIM_TAVANI); return Math.round(gonderi.ham * 0.3 * (1 - oran)); }
Düzenin kazancı doğrudan okunabilirliğidir: bir senaryoyu anlamak için tek bir yordamı baştan sona okumak yeterlidir, başka dosyaya gitmek gerekmez. Kaybı, kuralın senaryo sayısı kadar tekrarlanmasıdır.
Alan Modeli
Aynı iş, kuralı nesnenin üzerine koyarak da yazılır.
// alan/gonderi.mjs — kural nesnenin uzerinde, tek yerde const KADEME = [[1, 3900], [5, 6400], [Infinity, 11800]]; const KATSAYI = { "34": 1, "06": 1.35, "65": 1.8 }; const ASGARI_UCRET = 3990; const INDIRIM_TAVANI = 0.4; export class Gonderi { constructor(girdi) { this.agirlik = girdi.agirlik; this.postaKodu = girdi.postaKodu; this.indirimler = [...girdi.indirimler]; this.ham = Math.round(this.#taban() * this.#katsayi()); } #taban() { return KADEME.find(([ust]) => this.agirlik <= ust)[1]; } #katsayi() { return KATSAYI[this.postaKodu.slice(0, 2)] ?? 1.8; } indirimOrani() { return Math.min(this.indirimler.reduce((t, i) => t + i.oran, 0), INDIRIM_TAVANI); } net() { return Math.max(Math.round(this.ham * (1 - this.indirimOrani())), ASGARI_UCRET); } indirimEkle(indirim) { this.indirimler.push(indirim); return this; } iadeUcreti() { return Math.round(this.ham * 0.3 * (1 - this.indirimOrani())); } }
// alan/senaryolar.mjs — senaryolar yalniz sirayi kurar, kural nesnede kalir import { Gonderi } from "./gonderi.mjs"; export const yeniGonderi = (girdi) => new Gonderi(girdi); export const indirimEkle = (gonderi, indirim) => gonderi.indirimEkle(indirim); export const iadeUcreti = (gonderi) => gonderi.iadeUcreti();
Senaryo dosyası üç satıra indi. İki kural artık indirimOrani ve net yöntemlerinin içinde,
her biri bir kez yazılmış durumda.
Kural Uygulama Noktasının Sayılması
İki düzenin farkı iddia edilmez, sayılır. Ölçü kural uygulama noktası sayısıdır: kuralın adının, tanımlandığı satır dışında kaç ayrı satırda kullanıldığı.
// kural-say.mjs — iki duzende kural uygulama noktasi, dosya ve disa acilan ad sayisi import { readFileSync } from "node:fs"; const DUZEN = { "islem betigi": ["betik/senaryolar.mjs"], "alan modeli": ["alan/gonderi.mjs", "alan/senaryolar.mjs"], }; const KURAL = { "indirim tavani": /INDIRIM_TAVANI/, "asgari ucret": /ASGARI_UCRET/ }; for (const [ad, dosyalar] of Object.entries(DUZEN)) { let satir = 0, disaAcilan = 0; const nokta = Object.fromEntries(Object.keys(KURAL).map((k) => [k, 0])); for (const dosya of dosyalar) { const satirlar = readFileSync(dosya, "utf8").split("\n"); satir += satirlar.filter((s) => s.trim() !== "").length; for (const s of satirlar) { if (/^\s*export\b/.test(s)) disaAcilan += 1; if (/^\s*const [A-Z_]+ =/.test(s)) continue; for (const [k, kalip] of Object.entries(KURAL)) if (kalip.test(s)) nokta[k] += 1; } } console.log(`${ad}`); console.log(` dosya = ${dosyalar.length} satir = ${satir} disa acilan ad = ${disaAcilan}`); for (const [k, n] of Object.entries(nokta)) console.log(` ${k} uygulama noktasi = ${n}`); }
node kural-say.mjs
islem betigi dosya = 1 satir = 22 disa acilan ad = 3 indirim tavani uygulama noktasi = 3 asgari ucret uygulama noktasi = 2 alan modeli dosya = 2 satir = 26 disa acilan ad = 4 indirim tavani uygulama noktasi = 1 asgari ucret uygulama noktasi = 1
Sayılar ödünleşimi tek satıra indiriyor. İndirim tavanı işlem betiğinde 3, alan modelinde 1 noktada uygulanıyor; asgari ücret 2 noktaya karşı 1 noktada. Kuralın biçimi değişirse — tavan sabit bir oran olmaktan çıkıp müşteri türüne göre hesaplanan bir değere dönüşürse — işlem betiğinde 3, alan modelinde 1 satır yeniden doğrulanmak zorundadır.
Bedel tarafı aynı çıktıda duruyor: alan modeli 1 dosya yerine 2 dosya, 22 satır yerine 26 satır ve 3 dışa açılan ad yerine 4 ad tutuyor. Kurala ulaşmak için okunması gereken dosya sayısı da 1’den 2’ye çıkmış durumda.
İki Düzen Aynı Sonucu Veriyor mu
Bir düzenlemenin ötekinin yerine geçebileceği iddia edilmez, sınanır.
// esdeger-mi.mjs — iki duzen ayni girdilerde ayni sonucu veriyor mu import * as betik from "./betik/senaryolar.mjs"; import * as alan from "./alan/senaryolar.mjs"; const GIRDILER = [ { agirlik: 0.6, postaKodu: "34710", indirimler: [] }, { agirlik: 3, postaKodu: "06800", indirimler: [{ ad: "sozlesme", oran: 0.15 }] }, { agirlik: 12, postaKodu: "65100", indirimler: [{ ad: "sozlesme", oran: 0.25 }, { ad: "hacim", oran: 0.3 }] }, { agirlik: 0.4, postaKodu: "81600", indirimler: [{ ad: "hacim", oran: 0.5 }] }, ]; const EK = { ad: "kampanya", oran: 0.1 }; const say = (n) => String(n).padStart(5); let ayrilan = 0; for (const girdi of GIRDILER) { const b = betik.yeniGonderi(girdi), a = alan.yeniGonderi(girdi); const ilk = [b.net, a.net()]; const b2 = betik.indirimEkle(b, EK), a2 = alan.indirimEkle(a, EK); const son = [b2.net, a2.net()]; const iade = [betik.iadeUcreti(b2), alan.iadeUcreti(a2)]; if (ilk[0] !== ilk[1] || son[0] !== son[1] || iade[0] !== iade[1]) ayrilan += 1; console.log(`${String(girdi.agirlik).padStart(4)} kg ${girdi.postaKodu} net ${say(ilk[0])}/${say(ilk[1])} ek indirimli ${say(son[0])}/${say(son[1])} iade ${say(iade[0])}/${say(iade[1])}`); } console.log(`ayrilan sonuc = ${ayrilan} / ${GIRDILER.length}`);
node esdeger-mi.mjs
0.6 kg 34710 net 3990/ 3990 ek indirimli 3990/ 3990 iade 1053/ 1053 3 kg 06800 net 7344/ 7344 ek indirimli 6480/ 6480 iade 1944/ 1944 12 kg 65100 net 12744/12744 ek indirimli 12744/12744 iade 3823/ 3823 0.4 kg 81600 net 4212/ 4212 ek indirimli 4212/ 4212 iade 1264/ 1264 ayrilan sonuc = 0 / 4
Dört girdinin dördünde üç sonuç da aynı. İlk satırda asgari ücret, dördüncü satırda indirim tavanı devreye giriyor; iki düzen de aynı sınırı aynı yerde uyguluyor. Seçim bir doğruluk seçimi değil, bir değişim maliyeti seçimidir.
Sürümler arasında görünmeyen bir ayrım var: işlem betiği her adımda yeni bir kayıt döndürüyor, alan modeli aynı nesneyi değiştiriyor. Bu ayrım, aynı gönderinin bellekte kaç kopyası bulunduğu sorusunu doğurur ve kimlik haritası dersinin ölçtüğü şeydir.
Ne Zaman Uygulanmaz
Alan modelinin kazancı tekrarı kesmesinden gelir, dolayısıyla kazanç tekrar sayısıyla büyür. Yukarıdaki ölçümde kazanç, tavan kuralında 3 noktadan 1 noktaya inmektir; bedel 1 ek dosya, 1 ek dışa açılan ad ve 4 ek satırdır.
Tek senaryolu bir kitaplıkta aynı hesap tersine döner: uygulama noktası zaten 1’dir, kazanç 0 olur, bedel değişmez. Bir kuralı üç yerde yazmak zorunda kalmayan bir uygulamada alan modeli, ödediği dolaylılığın karşılığını alamaz. Ayrım ölçütü senaryo sayısı da değildir — aynı kurala dokunan senaryo sayısıdır. Beş senaryodan biri kuralı kullanıyorsa işlem betiği hâlâ ucuzdur.
İkinci karşı durum, kuralların veriden değil dış sistemlerden türediği işlerdir. Alan modeli nesnenin kendi verisinden karar üretir; kararın girdisi her seferinde bir dış çağrıysa nesne yalnız veri taşır, kural yine senaryoda kalır. Bu duruma anemik alan modeli (anemic domain model) adı verilir: sınıf vardır, davranış yoktur, dolayısıyla bedel ödenir ama kazanç alınmaz.
Özet
- İşlem betiği her senaryoyu tek yordamda yürütür ve kuralı senaryonun içine yazar; alan modeli kuralı veriyi taşıyan nesneye taşır, senaryoya yalnız sırayı bırakır.
- Kural uygulama noktası sayıldığında indirim tavanı işlem betiğinde 3, alan modelinde 1 noktada, asgari ücret 2 noktaya karşı 1 noktada uygulanıyordu.
- Alan modelinin bedeli aynı ölçümde göründü: 1 dosya yerine 2 dosya, 22 satır yerine 26 satır, 3 dışa açılan ad yerine 4 ad ve kurala ulaşmak için 2 dosya okuma.
- Dört girdinin dördünde iki düzen aynı net ücreti ve aynı iade ücretini verdi; seçim doğruluk değil değişim maliyeti seçimidir.
- Kazanç aynı kurala dokunan senaryo sayısıyla büyür; tek senaryoda kazanç sıfırlanır, bedel kalır. Davranışı olmayan sınıflar anemik alan modeli üretir.
Sonraki Adım
Bu dersteki iki düzen de gönderiyi bellekte tuttu; hiçbir yere yazmadı. Gönderi saklanmaya başladığında nesne ile saklanan kayıt arasında bir çevrim doğar ve bu çevrimin nereye konulacağı ikinci bir kalıp çiftini ortaya çıkarır: çevrimi nesnenin kendi üzerine koymak mı, ayrı bir eşleyiciye vermek mi. Sonraki ders bu iki kalıbı aynı gönderi üzerinde kurar, alan modülünün kalıcılık hakkında bilmek zorunda olduğu ad sayısını ve ithal kapanışının boyutunu sayar, ikinci bir saklama biçimi eklendiğinde dokunulan dosya sayısını karşılaştırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.