İçeriğe geç
academia.sh

Ders 09 / 18

Model–Görünüm Aileleri

Aynı teklif ekranının denetleyici, sunucu ve görünüm modeli varyantlarıyla üretilmesi ve sorumluluk dağılımının sayıya dökülmesi: gösteren dosyanın kaç alan adını tanıdığı, ithal kapanışının kaç dosya olduğu, sınırdan kaç çağrı ve kaç bayt geçtiği, sıra ve biçim kararlarının hangi dosyalarda durduğu.

İçindekiler

Buraya kadarki iki biçem aynı türden birimler arasındaki etkileşimi ele aldı: konuşan tarafların ikisi de ücret hesaplayan ya da tarife duyuran birimlerdi, aradaki fark yalnız kimin başlattığıydı. Kitaplıkta bir de türü farklı bir sınır var. Operasyon masasındaki görevli ücretin nasıl hesaplandığını değil, ekranda ne yazdığını görür: bölge adı, kademe, indirim ve ödenecek tutar belirli bir biçimde dizilmiş olmalıdır. Bu sınırın iki tarafı eşit değildir — biri kuralı ve veriyi taşır, öbürü onu gösterir.

Bu ders sorumluluğun üç ayrı dağıtımını karşılaştırır ve gösteren dosyanın kaç alan adını tanıdığını, sınırdan kaç çağrı ile kaç bayt geçtiğini, sıra ve biçim kararlarının hangi dosyalarda durduğunu sayar. Üç varyantın hepsi aynı ekranı üretmek zorundadır; ölçüm ancak o zaman aynı işin iki düzenlenişini karşılaştırır.

Adlandırma ve Sınır

Aile adları üç harfle kısaltılır: model–görünüm–denetleyici (model–view–controller), model–görünüm–sunucu (model–view–presenter) ve görünüm modeli (view model) varyantı. Buradaki sunucu sözcüğü gösterilecek değerleri sunan taraftır; İstemci–Sunucu dersindeki bekleyen taraf anlamıyla akraba değildir, aynı sözcüğün iki ayrı kataloğa ait kullanımıdır.

Görünüm modeli Sunucu Tarafı Temelleri kursundaki Sunucuda Şablon Oluşturma dersinde kurulmuştu: şablon ile veri kaynağı arasındaki sözleşmedir, hesap ve veri erişimi şablonda değil onu kuran kodda yapılır. Şablon değerlendirmesi, kaçışlama ve düzen şablonları oraya aittir ve burada yeniden ele alınmaz. Uygulama Mimarisi: Yönlendirme, Durum ve Veri kursundaki durum yönetimi dersleri de aynı sınırın arayüz tarafındaydı. Burada yeni olan tek şey sorumluluk dağılımının sayılmasıdır.

Bağlandığı kalite niteliği sürdürülebilirliktir ve nitelik sorusu şudur: ekranın düzeni ya da tutar biçimi değiştiğinde kaç dosya düzenlenir.

Ortak Model

Üç varyant aynı hesabı kullanır. Model beş alan döndürür ve hiçbirinde biçimlendirme yoktur; sayılar sayı, kodlar kod olarak kalır.

mkdir -p model mvd mvs gm
// model/fiyat.mjs — ucretlendirme baglami: hesap ve alan adlari
export const IL_ADI = { "34": "Istanbul", "06": "Ankara", "65": "Van" };
const KATSAYI = { "34": 1, "06": 1.4, "65": 2.1 };

export function fiyatla(gonderi) {
  const kademe = gonderi.agirlik <= 1 ? 1 : gonderi.agirlik <= 5 ? 2 : gonderi.agirlik <= 20 ? 3 : 4;
  const ham = 25 * kademe * KATSAYI[gonderi.il];
  const indirim = ham * gonderi.sozlesmeOrani;
  return { il: gonderi.il, kademe, ham, indirim, net: ham - indirim };
}

Denetleyici Varyantı

Birinci varyantta denetleyici girdiyi alır, modeli çağırır ve görünümü seçer. Görünüm model sonucunu doğrudan alır: alan adlarını kendisi okur, biçimlendirmeyi kendisi yapar.

// mvd/gorunum.mjs — gorunum modelin alan adlarini kendisi okur ve bicimlendirmeyi kendisi yapar
import { IL_ADI } from "../model/fiyat.mjs";

export const goster = (sonuc) =>
  [
    ["Bolge", IL_ADI[sonuc.il]],
    ["Kademe", String(sonuc.kademe)],
    ["Taban ucret", `${sonuc.ham.toFixed(2)} TL`],
    ["Indirim", `${sonuc.indirim.toFixed(2)} TL`],
    ["Odenecek", `${sonuc.net.toFixed(2)} TL`],
  ]
    .map(([etiket, deger]) => `${etiket.padEnd(11)}: ${deger}`)
    .join("\n");

Sunucu Varyantı

İkinci varyantta görünüm edilgendir: kendisine yazılanı tutar, hiçbir alan adı bilmez, hiçbir hesap yapmaz. Sunucu modelden okur, biçimlendirir ve satır satır görünüme yazar.

// mvs/gorunum.mjs — edilgen gorunum: yalnizca yazilani tutar, hicbir alan adi bilmez
export function gorunum() {
  const satir = new Map();
  return {
    yaz: (etiket, deger) => satir.set(etiket, deger),
    metin: () => [...satir].map(([e, d]) => `${e.padEnd(11)}: ${d}`).join("\n"),
  };
}
// mvs/sunucu.mjs — modelden okur, bicimlendirir ve edilgen gorunume satir satir yazar
import { fiyatla, IL_ADI } from "../model/fiyat.mjs";
import { gorunum } from "./gorunum.mjs";

export function teklifEkrani(gonderi, kur = gorunum) {
  const s = fiyatla(gonderi);
  const g = kur();
  g.yaz("Bolge", IL_ADI[s.il]);
  g.yaz("Kademe", String(s.kademe));
  g.yaz("Taban ucret", `${s.ham.toFixed(2)} TL`);
  g.yaz("Indirim", `${s.indirim.toFixed(2)} TL`);
  g.yaz("Odenecek", `${s.net.toFixed(2)} TL`);
  return g.metin();
}

Görünüm Modeli Varyantı

Üçüncü varyantta araya bir veri yapısı girer. Görünüm modeli modelden okur ve gösterilmeye hazır alanlar üretir; görünüm yalnız o alanları okur ve nasıl dizileceğine karar verir.

// gm/gorunum-modeli.mjs — modelden gorunume hazir alanlar ureten ayri veri yapisi
import { fiyatla, IL_ADI } from "../model/fiyat.mjs";

export function gorunumModeli(gonderi) {
  const s = fiyatla(gonderi);
  return {
    bolgeAdi: IL_ADI[s.il],
    kademeMetni: String(s.kademe),
    tabanUcret: `${s.ham.toFixed(2)} TL`,
    indirimTutari: `${s.indirim.toFixed(2)} TL`,
    odenecek: `${s.net.toFixed(2)} TL`,
  };
}
// gm/gorunum.mjs — yalnizca gorunum modelinin alanlarini okur, hicbir hesap yapmaz
const DUZEN = [
  ["Bolge", "bolgeAdi"],
  ["Kademe", "kademeMetni"],
  ["Taban ucret", "tabanUcret"],
  ["Indirim", "indirimTutari"],
  ["Odenecek", "odenecek"],
];

export const goster = (gm) => DUZEN.map(([e, a]) => `${e.padEnd(11)}: ${gm[a]}`).join("\n");

Kalan iki dosya birer satırdır: denetleyici ile görünüm modelini kuran ekran. İkisi de görünümü bir parametre olarak alır, böylece ölçüm sırasında sınırdan geçenler sayılabilir.

cat > mvd/denetleyici.mjs <<'SON'
// mvd/denetleyici.mjs — girdiyi alir, modeli cagirir, gorunumu secer
import { fiyatla } from "../model/fiyat.mjs";
import { goster } from "./gorunum.mjs";

export const teklifEkrani = (gonderi, ciz = goster) => ciz(fiyatla(gonderi));
SON
cat > gm/ekran.mjs <<'SON'
// gm/ekran.mjs — gorunum modelini kurar ve gorunume verir
import { gorunumModeli } from "./gorunum-modeli.mjs";
import { goster } from "./gorunum.mjs";

export const teklifEkrani = (gonderi, ciz = goster) => ciz(gorunumModeli(gonderi));
SON

Ölçüm

Betik önce üç varyantı aynı gönderiyle koşturup çıktılarının aynı olduğunu doğrular. Sonra her ailenin görünüm dosyasında kaç model alan adı ve kaç sunum adı geçtiğini sayar, görünümün ithal kapanışını çıkarır ve sınırdan geçen çağrıları kaydederek bayt miktarını toplar. Son tablo iki kararı arar: satırların sırasını belirleyen dosya ile tutar biçimini belirleyen dosya.

// sorumluluk.mjs — uc ailenin ayni ekrani uretmesi, sinirdan gecen veri ve gorunumun bildigi adlar
import { readFileSync } from "node:fs";
import { dirname, join, normalize } from "node:path";
import { teklifEkrani as mvd } from "./mvd/denetleyici.mjs";
import { teklifEkrani as mvs } from "./mvs/sunucu.mjs";
import { teklifEkrani as gmEkran } from "./gm/ekran.mjs";
import { goster as gosterMvd } from "./mvd/gorunum.mjs";
import { goster as gosterGm } from "./gm/gorunum.mjs";

const GONDERI = { il: "06", agirlik: 12, sozlesmeOrani: 0.15 };
const ALAN = ["il", "kademe", "ham", "indirim", "net"];
const SUNUM = ["bolgeAdi", "kademeMetni", "tabanUcret", "indirimTutari", "odenecek"];

const kapanis = (kok) => {
  const gorulen = new Set([kok]), yigit = [kok];
  while (yigit.length > 0) {
    const d = yigit.pop();
    for (const [, y] of readFileSync(d, "utf8").matchAll(/from "(\.[^"]+)"/g)) {
      const h = normalize(join(dirname(d), y));
      if (gorulen.has(h) === false) { gorulen.add(h); yigit.push(h); }
    }
  }
  return gorulen.size;
};

const adSay = (yol, kume) => {
  const metin = readFileSync(yol, "utf8");
  return kume.filter((a) => new RegExp(`[.\\["']${a}\\b`).test(metin)).length;
};

const sayarak = (asil) => {
  const kayit = [];
  return [(...a) => { kayit.push(a); return asil(...a); }, kayit];
};

function mvsKosum() {
  const kayit = [];
  const kur = () => {
    const satir = new Map();
    return {
      yaz: (e, d) => { kayit.push([e, d]); satir.set(e, d); },
      metin: () => { kayit.push([]); return [...satir].map(([e, d]) => `${e.padEnd(11)}: ${d}`).join("\n"); },
    };
  };
  return [mvs(GONDERI, kur), kayit];
}

const [cizMvd, kayitMvd] = sayarak(gosterMvd);
const [cizGm, kayitGm] = sayarak(gosterGm);
const [mvsCikti, kayitMvs] = mvsKosum();
const cikti = [mvd(GONDERI, cizMvd), mvsCikti, gmEkran(GONDERI, cizGm)];

console.log(cikti[0]);
console.log(`uc ailenin ciktisi ayni mi = ${new Set(cikti).size === 1}`);

const AILE = [
  ["MVD", "mvd/gorunum.mjs", ["mvd/gorunum.mjs", "mvd/denetleyici.mjs"], kayitMvd],
  ["MVS", "mvs/gorunum.mjs", ["mvs/gorunum.mjs", "mvs/sunucu.mjs"], kayitMvs],
  ["GM ", "gm/gorunum.mjs", ["gm/gorunum.mjs", "gm/gorunum-modeli.mjs", "gm/ekran.mjs"], kayitGm],
];

console.log("\naile  alan adi  sunum adi  kapanis  sinir cagrisi  gecen bayt");
for (const [ad, gorunumYolu, , kayit] of AILE) {
  const bayt = kayit.reduce((t, a) => t + JSON.stringify(a).length, 0);
  console.log(
    `${ad}   ${String(adSay(gorunumYolu, ALAN)).padStart(6)}${String(adSay(gorunumYolu, SUNUM)).padStart(11)}` +
      `${String(kapanis(gorunumYolu)).padStart(9)}${String(kayit.length).padStart(15)}${String(bayt).padStart(12)}`,
  );
}

console.log("\naile  sirayi belirleyen        bicimi belirleyen         dokunulan dosya");
for (const [ad, , dosyalar] of AILE) {
  const bul = (desen) => dosyalar.find((d) => readFileSync(d, "utf8").includes(desen)) ?? "-";
  const sira = bul('"Odenecek"'), bicim = bul("toFixed");
  console.log(`${ad}   ${sira.padEnd(24)}${bicim.padEnd(26)}${new Set([sira, bicim]).size}`);
}
node sorumluluk.mjs
Bolge      : Ankara
Kademe     : 3
Taban ucret: 105.00 TL
Indirim    : 15.75 TL
Odenecek   : 89.25 TL
uc ailenin ciktisi ayni mi = true

aile  alan adi  sunum adi  kapanis  sinir cagrisi  gecen bayt
MVD        5          0        2              1          62
MVS        0          0        1              6         106
GM         0          5        1              1         115

aile  sirayi belirleyen        bicimi belirleyen         dokunulan dosya
MVD   mvd/gorunum.mjs         mvd/gorunum.mjs           1
MVS   mvs/sunucu.mjs          mvs/sunucu.mjs            1
GM    gm/gorunum.mjs          gm/gorunum-modeli.mjs     2

Sayıların Okunması

Üç varyant aynı beş satırı üretti, yani ölçüm aynı işi karşılaştırıyor.

Birinci sütun bilme yükümlülüğünü verir. Denetleyici varyantında görünüm beş model alan adını tanıyor; öbür ikisinde sıfır. İkinci sütun bunun nereye gittiğini gösterir: görünüm modeli varyantında görünüm beş sunum adı tanıyor, yani bilgi yok olmadı, model adlarından sunum adlarına çevrildi. Sunucu varyantında görünüm hiç ad tanımıyor; yazılan etiketi ve değeri olduğu gibi tutuyor.

Üçüncü sütun bunun somut sonucudur. Denetleyici varyantının görünüm dosyasının ithal kapanışı 2: görünümü yüklemek modeli de yüklemek anlamına gelir. Öbür iki görünümün kapanışı 1; ikisi de model olmadan yüklenip sınanabilir.

Dördüncü ve beşinci sütunlar sınırdan geçeni ölçer ve ödünleşimi açığa çıkarır. Denetleyici varyantında sınırı tek çağrı geçti ve 62 bayt taşıdı — ham sayılar kısa. Sunucu varyantında altı çağrı geçti ve 106 bayt taşıdı; her satır ayrı bir çağrıdır, biçimlendirilmiş dizeler ham sayıdan uzundur. Görünüm modeli varyantında tek çağrı 115 bayt taşıdı: beş biçimlendirilmiş alan bir arada. Sınırı geçen veri en az olan varyant, görünümün model hakkında en çok şey bildiği varyanttır. Ucuz sınır ile bilgisiz görünüm aynı düzende bulunmuyor.

Kararların Yeri

Son tablo sürdürülebilirlik sorusunu yanıtlar. Ekranın satır sırası ile tutar biçimi iki ayrı karardır ve hangi dosyada durdukları aileye göre değişir.

Denetleyici ve sunucu varyantlarında iki karar aynı dosyada duruyor, dolayısıyla ikisini de değiştiren bir istek tek dosyaya dokunur. Görünüm modeli varyantında sıra görünümde, biçim görünüm modelinde; aynı istek iki dosyaya dokunur. Bu, ayrımın bedelidir ve karşılığı da aynı tabloda okunur: yalnız sıranın değiştiği bir istek görünüm modeli varyantında görünüm dosyasına dokunur, o dosya ise modeli hiç tanımaz ve tek başına sınanabilir. Denetleyici varyantında aynı istek, beş model alan adını tanıyan dosyayı düzenlemeyi gerektirir.

Seçim ölçütü isteklerin türüne bağlıdır. Sık değişen şey düzense ayrım kazanç sağlar; sık değişen şey hangi alanların gösterildiğiyse iki dosyayı birlikte düzenlemek maliyettir.

Özet

  • Üç varyant aynı beş satırlık ekranı üretti; fark işlevde değil, model ile görünüm arasındaki sorumluluk dağılımındadır.
  • Görünümün tanıdığı model alan adı denetleyici varyantında 5, öbür ikisinde 0; görünüm modeli varyantında bu bilgi 5 sunum adına çevrildi, sunucu varyantında görünümde hiç ad kalmadı.
  • Görünüm dosyasının ithal kapanışı denetleyici varyantında 2, öbür ikisinde 1; ikinci ve üçüncü görünüm model olmadan yüklenebilir.
  • Sınırdan geçen veri ters yönde arttı: denetleyici varyantında 1 çağrı ve 62 bayt, sunucu varyantında 6 çağrı ve 106 bayt, görünüm modeli varyantında 1 çağrı ve 115 bayt.
  • Sıra ve biçim kararları ilk iki varyantta tek dosyada, görünüm modeli varyantında iki ayrı dosyada duruyor; ikisini birden değiştiren istek 1 yerine 2 dosyaya dokunur.

Sonraki Adım

Bu üç varyantta da veri tek bir sıçrayışla sınırı geçiyordu: model sonucu bir çağrıda verildi, biçimlendirme bir yerde yapıldı. Ücretlendirme bağlamının asıl iş akışı ise böyle değil. Günlük gönderi listesi işlenirken her kayıt sırayla birkaç adımdan geçer: alan adları normalleştirilir, bölge türetilir, tarife uygulanır, indirim düşülür, tutar yuvarlanır. Bugün bu beş adım tek bir gövdenin içinde. Yeni bir adım — yakıt ek ücreti — istendiğinde o gövde düzenlenmek zorunda ve adımların hiçbiri tek başına sınanamıyor. Sonraki ders adımları ayrı birimlere böler ve zinciri ölçer: yeni adım eklendiğinde düzenlenen dosya sayısı, her adımın tanıdığı ad sayısı, adımlar arasındaki sınırdan geçen alan sayısı ve kayıtlar tek tek akıtıldığında bellekte aynı anda tutulan kayıt sayısı.

İ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