İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat