İçeriğe geç
academia.sh

Ders 16 / 19

Çerçeve Kodunu Uzakta Tutmak

Çerçeve bağımlılığının yalıtımı: küçük bir dış aracın sözleşmesine dokunan dosyaların iki düzende sayılması, aracın sözleşmesi değiştiğinde düzeltilmesi gereken dosya sayısının karşılaştırılması ve bağdaştırıcının dosya ile satır cinsinden bedelinin ölçülmesi.

İçindekiler

Bir önceki ders sözleşme yüzeyini üç sayıyla ölçtü ve dar sınırın kitaplığın kendi iç adlandırmasını koruduğunu gösterdi. O sınırın iki tarafı da kitaplığın kendi kodudur: sözleşmeyi kim yazacaksa kitaplık yazar, daraltma kararı da kitaplığındır.

Kitaplığın dışında ise sözleşmesini başkasının yazdığı bir kod sınıfı durur — isteği karşılayan, yolu eşleştiren, işleyiciyi çağıran araçlar. Bunlar bir sözleşme dayatır ve o sözleşme değişirken kitaplığa danışılmaz. Bu ders böyle bir aracın sözleşmesine dokunan dosya sayısını iki düzende sayar, sonra sözleşmeyi değiştirip düzeltilmesi gereken dosyaları karşılaştırır.

Çerçeve ile Kütüphanenin Farkı

Ayrım işlevde değil, denetimin yönündedir. Bir kütüphane çağrılır: çağrıların sırasını yazan taraf kitaplıktır. Bir çerçeve (framework) ise çağırır: yazılan şey onun beklediği biçimde bir işleyicidir, ne zaman çalışacağına çerçeve karar verir. Bağlaşım ve Uyum konusundaki Hollywood ilkesi bu yönün adıdır; aynı terslik arayüz tarafında Bileşen Tabanlı Geliştirme kursunda kurulmuştu.

Yön tersliğinin bedeli, bağımlılığın imzada durmamasıdır. Kütüphane değişirse çağrı yerleri düzeltilir ve çağrı yerleri aranabilir. Çerçeve değişirse, çerçevenin verdiği bağlam nesnesinin alanlarını okuyan her yer düzeltilir. Kaç yer olduğu bir ürün seçimi değil, tasarım kararıdır — ölçülebilir bir karardır.

Ölçüm için küçük bir dış araç yazılıyor: yol desenlerini eşleştiren ve eşleşen işleyiciyi kendi bağlam nesnesiyle çağıran bir yönlendirici. Sözlüğü altı addan oluşuyor.

mkdir -p arac dogrudan bagdastirici
// arac/istek-yonlendirici.mjs — dis arac v1: yol desenini eslestirir, isleyiciyi kendi baglamiyla cagirir
export function yonlendiriciOlustur() {
  const kayitlar = [];
  const eslestir = (desen, yol) => {
    const d = desen.split("/"), p = yol.split("/");
    if (d.length !== p.length) return null;
    const cikan = {};
    for (let i = 0; i < d.length; i += 1) {
      if (d[i][0] === ":") cikan[d[i].slice(1)] = p[i];
      else if (d[i] !== p[i]) return null;
    }
    return cikan;
  };
  return {
    desenEkle: (desen, isleyici) => kayitlar.push([desen, isleyici]),
    istekIsle(yol, alanlar = {}) {
      for (const [desen, isleyici] of kayitlar) {
        const eslesme = eslestir(desen, yol);
        if (eslesme === null) continue;
        let yanit = null;
        isleyici({ parametreler: eslesme, sorgu: alanlar,
          yanitla: (kod, govde) => { yanit = { kod, govde }; } });
        return yanit ?? { kod: 500, govde: { hata: "yanit uretilmedi" } };
      }
      return { kod: 404, govde: { hata: "yol bulunamadi" } };
    },
  };
}

Ücret tabloları iki düzende de ortaktır ve aracı hiç anmaz.

// tarife.mjs — iki duzenin de kullandigi tablolar; dis araci hic anmaz
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };
const ASGARI_UCRET = 3990;

export function tabanUcret(agirlik, postaKodu) {
  const kademe = KADEMELER.find((k) => agirlik <= k.ustAgirlik);
  if (kademe === undefined) return null;
  const katsayi = KATSAYI[POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak"];
  return Math.max(Math.round((kademe.ucret * katsayi) / 50) * 50, ASGARI_UCRET);
}

Doğrudan Bağlanan Düzen

İlk düzende iş kuralı doğrudan aracın bağlamını okuyor ve yanıtını yine onun üzerinden veriyor. Doğrulama bile aracın sözlüğüne bağlı.

// dogrudan/dogrulama.mjs — dogrulama da yanitini aracin baglami uzerinden veriyor
export function agirligiDogrula(baglam) {
  const agirlik = Number(baglam.parametreler.agirlik);
  if (Number.isFinite(agirlik) && agirlik > 0) return agirlik;
  baglam.yanitla(400, { hata: "agirlik gecersiz" });
  return null;
}
// dogrudan/ucret-ucnoktasi.mjs — hesap dogrudan aracin baglamindan okur ve ona yazar
import { tabanUcret } from "../tarife.mjs";
import { agirligiDogrula } from "./dogrulama.mjs";

export function ucretIsleyici(baglam) {
  const agirlik = agirligiDogrula(baglam);
  if (agirlik === null) return;
  const tutar = tabanUcret(agirlik, baglam.parametreler.postaKodu);
  if (tutar === null) { baglam.yanitla(422, { hata: "agirlik kademesi yok" }); return; }
  const indirim = Number(baglam.sorgu.indirim ?? 0);
  baglam.yanitla(200, { tutar: Math.round(tutar * (1 - indirim)), paraBirimi: "kurus" });
}
// dogrudan/tasiyici-ucnoktasi.mjs — ikinci uc nokta, ayni baglam sozlesmesine bagli
import { tabanUcret } from "../tarife.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function tasiyiciIsleyici(baglam) {
  const taban = tabanUcret(Number(baglam.parametreler.agirlik), baglam.parametreler.postaKodu);
  if (taban === null) { baglam.yanitla(422, { hata: "agirlik kademesi yok" }); return; }
  const enUcuz = TASIYICILAR
    .map((t) => ({ tasiyici: t.ad, tutar: Math.round(taban * t.carpan) }))
    .sort((a, b) => a.tutar - b.tutar)[0];
  baglam.yanitla(200, enUcuz);
}
// dogrudan/uygulama.mjs — araci kuran ve uc noktalari kaydeden dosya
import { yonlendiriciOlustur } from "../arac/istek-yonlendirici.mjs";
import { ucretIsleyici } from "./ucret-ucnoktasi.mjs";
import { tasiyiciIsleyici } from "./tasiyici-ucnoktasi.mjs";

export function uygulama() {
  const y = yonlendiriciOlustur();
  y.desenEkle("/ucret/:agirlik/:postaKodu", ucretIsleyici);
  y.desenEkle("/tasiyici/:agirlik/:postaKodu", tasiyiciIsleyici);
  return (yol, alanlar) => y.istekIsle(yol, alanlar);
}

Bağdaştırıcı Arkasındaki Düzen

İkinci düzende iş kuralı düz bir işlemdir: (istek) => yanit. İsteğin de yanıtın da alan adları kitaplığın kendi seçimidir, aracınki değil. Araç ile bu biçim arasındaki çeviriyi tek bir dosya yapar; bu dosyaya bağdaştırıcı (adapter) denir.

// bagdastirici/dogrulama.mjs — dogrulama duz bir sonuc dondurur, arac adi gecmez
export function agirligiDogrula(degerler) {
  const agirlik = Number(degerler.agirlik);
  if (Number.isFinite(agirlik) && agirlik > 0) return { gecerli: true, agirlik };
  return { gecerli: false, yanit: { durum: 400, icerik: { hata: "agirlik gecersiz" } } };
}
// bagdastirici/ucret-islemi.mjs — islem duz bir istek alir, duz bir yanit dondurur
import { tabanUcret } from "../tarife.mjs";
import { agirligiDogrula } from "./dogrulama.mjs";

export function ucretIslemi(istek) {
  const d = agirligiDogrula(istek.degerler);
  if (!d.gecerli) return d.yanit;
  const tutar = tabanUcret(d.agirlik, istek.degerler.postaKodu);
  if (tutar === null) return { durum: 422, icerik: { hata: "agirlik kademesi yok" } };
  const indirim = Number(istek.secenekler.indirim ?? 0);
  return { durum: 200, icerik: { tutar: Math.round(tutar * (1 - indirim)), paraBirimi: "kurus" } };
}
// bagdastirici/tasiyici-islemi.mjs — ikinci islem, ayni duz sozlesme
import { tabanUcret } from "../tarife.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function tasiyiciIslemi(istek) {
  const taban = tabanUcret(Number(istek.degerler.agirlik), istek.degerler.postaKodu);
  if (taban === null) return { durum: 422, icerik: { hata: "agirlik kademesi yok" } };
  const enUcuz = TASIYICILAR
    .map((t) => ({ tasiyici: t.ad, tutar: Math.round(taban * t.carpan) }))
    .sort((a, b) => a.tutar - b.tutar)[0];
  return { durum: 200, icerik: enUcuz };
}
// bagdastirici/arac-koprusu.mjs — araca dokunan tek dosya: baglami duz isteke, duz yaniti baglama cevirir
import { yonlendiriciOlustur } from "../arac/istek-yonlendirici.mjs";

export function uygulamaKur(kayitlar) {
  const y = yonlendiriciOlustur();
  for (const [desen, islem] of kayitlar) {
    y.desenEkle(desen, (baglam) => {
      const yanit = islem({ degerler: baglam.parametreler, secenekler: baglam.sorgu });
      baglam.yanitla(yanit.durum, yanit.icerik);
    });
  }
  return (yol, alanlar) => y.istekIsle(yol, alanlar);
}
// bagdastirici/uygulama.mjs — kayit dosyasi; aracin sozlugunden hicbir ad gecmez
import { uygulamaKur } from "./arac-koprusu.mjs";
import { ucretIslemi } from "./ucret-islemi.mjs";
import { tasiyiciIslemi } from "./tasiyici-islemi.mjs";

export const uygulama = () => uygulamaKur([
  ["/ucret/:agirlik/:postaKodu", ucretIslemi],
  ["/tasiyici/:agirlik/:postaKodu", tasiyiciIslemi],
]);

Karşılaştırmanın anlamlı olması için iki düzenin aynı istekleri aynı biçimde yanıtlaması gerekir.

// kosum.mjs — iki duzen ayni istekleri ayni sekilde yanitliyor mu
import { uygulama as dogrudanUygulama } from "./dogrudan/uygulama.mjs";
import { uygulama as koprulu } from "./bagdastirici/uygulama.mjs";

const ISTEKLER = [
  ["/ucret/3/06800", {}], ["/ucret/12/65100", { indirim: 0.1 }],
  ["/ucret/0/34710", {}], ["/tasiyici/28/35400", {}], ["/rota/1/34000", {}],
];

const a = dogrudanUygulama(), b = koprulu();
let ayrilan = 0;
for (const [yol, sorgu] of ISTEKLER) {
  const x = JSON.stringify(a(yol, sorgu)), y = JSON.stringify(b(yol, sorgu));
  if (x !== y) ayrilan += 1;
  console.log(`${yol.padEnd(22)} ${x}`);
}
console.log(`ayrilan yanit = ${ayrilan} / ${ISTEKLER.length}`);
/ucret/3/06800         {"kod":200,"govde":{"tutar":11450,"paraBirimi":"kurus"}}
/ucret/12/65100        {"kod":200,"govde":{"tutar":24300,"paraBirimi":"kurus"}}
/ucret/0/34710         {"kod":400,"govde":{"hata":"agirlik gecersiz"}}
/tasiyici/28/35400     {"kod":200,"govde":{"tasiyici":"ekonomik","tutar":28688}}
/rota/1/34000          {"kod":404,"govde":{"hata":"yol bulunamadi"}}
ayrilan yanit = 0 / 5

Beş istekte de aynı durum kodu ve aynı gövde. Geçersiz ağırlık, kademe dışı ağırlık ve tanımsız yol dâhil, fark davranışta değil.

Araca Dokunan Dosya Sayısı

Yalıtımın ölçüsü, aracın sözlüğünden en az bir ad geçen dosya sayısıdır. Sözlük altı addan oluşuyor: kurucu, kayıt ve çalıştırma adları ile bağlamın üç alanı.

// dokunan-say.mjs — aracin sozlugunden ad gecen dosyalari sayar
import { readdirSync, readFileSync } from "node:fs";

const SOZLUK = ["yonlendiriciOlustur", "desenEkle", "istekIsle", "parametreler", "sorgu", "yanitla"];

for (const dizin of ["dogrudan", "bagdastirici"]) {
  const dosyalar = readdirSync(dizin).filter((d) => d.endsWith(".mjs")).sort();
  let dokunan = 0;
  console.log(`${dizin}/`);
  for (const d of dosyalar) {
    const metin = readFileSync(`${dizin}/${d}`, "utf8");
    const gecen = SOZLUK.filter((ad) => new RegExp(`\\b${ad}\\b`).test(metin));
    if (gecen.length > 0) dokunan += 1;
    console.log(`  ${d.padEnd(24)} ${gecen.length > 0 ? gecen.join(" ") : "-"}`);
  }
  console.log(`  araca dokunan dosya = ${dokunan} / ${dosyalar.length}`);
}
dogrudan/
  dogrulama.mjs            parametreler yanitla
  tasiyici-ucnoktasi.mjs   parametreler yanitla
  ucret-ucnoktasi.mjs      parametreler sorgu yanitla
  uygulama.mjs             yonlendiriciOlustur desenEkle istekIsle
  araca dokunan dosya = 4 / 4
bagdastirici/
  arac-koprusu.mjs         yonlendiriciOlustur desenEkle istekIsle parametreler sorgu yanitla
  dogrulama.mjs            -
  tasiyici-islemi.mjs      -
  ucret-islemi.mjs         -
  uygulama.mjs             -
  araca dokunan dosya = 1 / 5

İlk düzende dört dosyanın dördü de araca bağlı; ikincisinde beş dosyanın biri. Dikkate değer satır dogrudan/dogrulama.mjs: ortada uç nokta olmadığı hâlde ağırlık doğrulaması aracın sözlüğüne girmiş. Çerçeveye bağlanma uç noktalarda kalmaz, hata yolunu izleyerek içeri yürür.

Aracın Sözleşmesi Değişince

Sayılar ancak bir değişiklik geldiğinde karşılığını verir. Aracın ikinci sürümü bağlamın üç adını değiştiriyor: parametreler yerine yolDegerleri, sorgu yerine sorguDegerleri, iki argümanlı yanitla yerine tek nesne alan gonder. Yeni sürüm birincisinden türetilip ayrı bir ağaca konuyor.

mkdir -p v2 && cp -r arac dogrudan bagdastirici tarife.mjs kosum.mjs v2/
sed -e 's/parametreler: eslesme, sorgu: alanlar,/yolDegerleri: eslesme, sorguDegerleri: alanlar,/' \
    -e 's/yanitla: (kod, govde) => { yanit = { kod, govde }; }/gonder: ({ durum, govde }) => { yanit = { kod: durum, govde }; }/' \
    arac/istek-yonlendirici.mjs > v2/arac/istek-yonlendirici.mjs
grep -n "yolDegerleri\|gonder:" v2/arac/istek-yonlendirici.mjs
21:        isleyici({ yolDegerleri: eslesme, sorguDegerleri: alanlar,
22:          gonder: ({ durum, govde }) => { yanit = { kod: durum, govde }; } });

Aracın kendi içinde değişen tek yer bağlam nesnesinin kurulduğu iki satır. Şimdi aynı geçiş her iki düzene uygulanıyor ve düzeltilen dosyalar sayılıyor. Yerinde düzenleme seçeneğinin yazımı BSD ile GNU sed arasında ayrıştığı için -i.yedek biçimi kullanılıyor; yanına yedek dosya bırakır ve .mjs süzgecine takılmaz.

sed -i.yedek -e 's/baglam\.parametreler/baglam.yolDegerleri/g' \
             -e 's/baglam\.sorgu/baglam.sorguDegerleri/g' \
             -e 's/baglam\.yanitla(\([^,]*\), \(.*\))/baglam.gonder({ durum: \1, govde: \2 })/g' \
             v2/dogrudan/*.mjs v2/bagdastirici/*.mjs
for d in dogrudan bagdastirici; do
  n=0; t=0
  for f in "$d"/*.mjs; do
    t=$((t + 1)); cmp -s "$f" "v2/$f" || n=$((n + 1))
  done
  echo "$d: duzeltilen dosya = $n / $t"
done
cd v2 && node kosum.mjs
dogrudan: duzeltilen dosya = 3 / 4
bagdastirici: duzeltilen dosya = 1 / 5
/ucret/3/06800         {"kod":200,"govde":{"tutar":11450,"paraBirimi":"kurus"}}
/ucret/12/65100        {"kod":200,"govde":{"tutar":24300,"paraBirimi":"kurus"}}
/ucret/0/34710         {"kod":400,"govde":{"hata":"agirlik gecersiz"}}
/tasiyici/28/35400     {"kod":200,"govde":{"tasiyici":"ekonomik","tutar":28688}}
/rota/1/34000          {"kod":404,"govde":{"hata":"yol bulunamadi"}}
ayrilan yanit = 0 / 5

Doğrudan bağlanan düzende üç dosya düzeltildi, bağdaştırıcılı düzende bir. Kayıt dosyası dogrudan/uygulama.mjs bu sefer değişmedi, çünkü değişen adlar bağlamın alanlarıydı, kurucu ve kayıt adları değil. Aracın kurucu adını da değiştiren bir sürüm gelseydi doğrudan düzende dört, bağdaştırıcılı düzende yine bir dosya düzeltilecekti: köprünün sayısı aracın sözleşmesindeki değişikliğin büyüklüğünden bağımsızdır.

İki sürümün beş isteğe verdiği yanıtlar da aynı kaldı; geçiş davranışı değiştirmedi.

Yalıtımın Bedeli

Bağdaştırıcı bedava değildir ve bedeli aynı ölçekte sayılabilir.

for d in dogrudan bagdastirici; do
  echo "$d: $(ls "$d"/*.mjs | wc -l | tr -d ' ') dosya, $(cat "$d"/*.mjs | wc -l | tr -d ' ') satir"
done
dogrudan: 4 dosya, 45 satir
bagdastirici: 5 dosya, 55 satir

Bir dosya ve on satır fazladan. Buna karşılık aracın sözleşmesindeki bir değişiklikte düzeltilen dosya sayısı üçten bire indi. On satırlık sabit maliyet, sözleşme her değiştiğinde tekrarlanan iki dosyalık farkla karşılaştırılır; aracın sözleşmesi hiç değişmeyecekse yalıtım karşılığını vermez.

İkinci bedel sayıyla değil, kapsamla ölçülür: köprü yalnız çevirdiği kadarını geçirir. Aracın sunduğu ama köprünün eşlemediği bir yetenek — akış hâlinde yanıt, istek iptali — iş kuralı tarafından kullanılamaz; kullanılacaksa önce köprünün sözleşmesine eklenmesi gerekir. Bu, ilkeyi “her çerçeve mutlaka sarmalanır” biçiminde okumayı engeller. Yalıtımın gerekçesi, aracın sözleşmesinin kitaplığın denetiminde olmaması ve değişme geçmişinin bulunmasıdır; gerekçe yoksa köprü yalnız bir dolaylılık katmanıdır.

Özet

  • Çerçeve ile kütüphane işlevle değil denetimin yönüyle ayrılır: kütüphane çağrılır, çerçeve çağırır; bu yüzden çerçeve bağımlılığı çağrı yerlerinde değil, bağlamı okuyan her yerde durur.
  • Yalıtımın ölçüsü, aracın sözlüğünden en az bir ad geçen dosya sayısıdır: doğrudan bağlanan düzende 4/4, bağdaştırıcılı düzende 1/5.
  • Çerçeveye bağlanma uç noktalarda kalmaz; hata yolunu izleyerek doğrulama modülüne kadar yürüdü.
  • Aracın bağlam sözleşmesindeki üç ad değişince doğrudan düzende 3, bağdaştırıcılı düzende 1 dosya düzeltildi; beş isteğin yanıtı iki düzende de değişmedi.
  • Yalıtımın bedeli 1 dosya ve 10 satırdır, üstelik köprü yalnız eşlediği yeteneği geçirir; aracın sözleşmesi değişmiyorsa bu bedel karşılıksız kalır.

Sonraki Adım

Buraya kadarki üç ders sınırın nereye çizileceğini tek tek dosyalar üzerinde tartıştı: politika hangi dosyada, sözleşme kaç addan oluşuyor, araca kaç dosya dokunuyor. Kod tabanı büyüdüğünde birim dosya olmaktan çıkar; birlikte paketlenen, birlikte sürümlenen ve birlikte yayımlanan dosya kümesi bir bileşen olur. O zaman soru şudur: hangi dosyalar aynı bileşene girmelidir? Sonraki ders bu soruyu değişiklik günlüğünden yanıtlar — birlikte değişen dosyaları kümeleyen bir araç yazar, bileşen sınırı bu kümelere göre çizildiğinde bileşen sayısını ve bileşenler arası bağ sayısını ölçer, bir değişikliğin kaç bileşene dokunduğunu ve tek bir yeteneğin kaç dosyaya bağladığını üç bölünmede 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