İçeriğe geç
academia.sh

Ders 15 / 19

Sınırların Çizilmesi

Modüller arası sözleşme yüzeyinin ölçülmesi: dışa açılan ad sayısı, sınırdan geçen alan sayısı ve karşı tarafın bilmek zorunda olduğu ad sayısı; geniş ile dar sınır tanımının aynı ölçüyle karşılaştırılması ve iç adlandırma değiştiğinde hangi tarafın kırıldığının sınanması.

İçindekiler

Bir önceki ders kararlı kuralı değişken ayrıntıdan ayırdı ve kuralın tarife nesnesinden üç ad beklediğini gösterdi: kademe bulma, yuvarlama, asgari ücret. Bu üç ad iki modül arasındaki sözleşmedir ve sözleşmenin genişliği o ana kadar ölçülmedi.

Sınır, iki modülün birbiriyle konuştuğu noktadır ve genişliği somut bir sayıyla verilir: kaç ad dışa açılıyor, o adlardan geçen değerlerde kaç alan var, karşı taraf bunların kaçını bilmek zorunda. Bu ders aynı işi yapan iki tasarımı bu üç ölçüyle karşılaştırıyor.

Ölçülecek İki Tasarım

Kitaplığın ücretlendirme modülü ile taşıyıcı seçimi modülü arasındaki sınır ele alınıyor. Seçim modülü bir gönderi için üç taşıyıcı seçeneğinden en ucuzunu bulur ve sözleşmeli müşteri indirimini uygular.

mkdir -p genis dar

İlk tasarımda ücretlendirme modülü tablolarını ve ara adımlarını da dışa açıyor.

// genis/ucret-modulu.mjs — genis sinir: tablolar, ara adimlar ve dokum disa acik
export const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
export const ASGARI_UCRET = 3990;
export const YUVARLAMA_ADIMI = 50;
export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
export const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const kademeBul = (agirlik) => KADEMELER.find((k) => agirlik <= k.ustAgirlik) ?? null;
export const bolgeBul = (postaKodu) => POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak";
export const yuvarla = (t) => Math.round(t / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;

export function ucretDokumu(gonderi) {
  const kademe = kademeBul(gonderi.agirlik);
  if (kademe === null) throw new RangeError("agirlik kademesi yok");
  const bolge = bolgeBul(gonderi.postaKodu);
  const katsayi = KATSAYI[bolge];
  const hamTutar = kademe.ucret * katsayi;
  const yuvarlanmis = yuvarla(hamTutar);
  return {
    kademeUst: kademe.ustAgirlik, kademeUcret: kademe.ucret, bolge, katsayi,
    hamTutar, yuvarlanmis, asgariUygulandi: yuvarlanmis < ASGARI_UCRET,
    toplam: Math.max(yuvarlanmis, ASGARI_UCRET),
  };
}

Seçim modülü indirimi uygulayabilmek için dökümün ara alanına iniyor ve yuvarlama ile asgari ücret adımlarını kendi tarafında tekrarlıyor.

// genis/tasiyici-secimi.mjs — indirimi uygulamak icin karsi tarafin ic adimlarini tekrarlar
import { ucretDokumu, yuvarla, ASGARI_UCRET } from "./ucret-modulu.mjs";

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

export function enUygunTasiyici(gonderi, indirimOrani) {
  const dokum = ucretDokumu(gonderi);
  return TASIYICILAR
    .map((t) => ({
      tasiyici: t.ad,
      tutar: Math.max(yuvarla(dokum.hamTutar * t.carpan * (1 - indirimOrani)), ASGARI_UCRET),
      bolge: dokum.bolge,
    }))
    .sort((a, b) => a.tutar - b.tutar)[0];
}

İkinci tasarımda sınırdan niyet geçiyor: taşıyıcı çarpanı ve indirim oranı ücretlendirme modülüne parametre olarak veriliyor, ara adımlar dışarı çıkmıyor.

// dar/ucret-modulu.mjs — dar sinir: tek ad disa acik, tablolar iceride
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const ucretlendirici = {
  fiyatla(gonderi, { hizCarpani = 1, indirimOrani = 0 } = {}) {
    const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik);
    if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
    const katsayi = KATSAYI[POSTA_BOLGE[gonderi.postaKodu.slice(0, 2)] ?? "uzak"];
    const hamTutar = kademe.ucret * katsayi * hizCarpani * (1 - indirimOrani);
    const yuvarlanmis = Math.round(hamTutar / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;
    return { tutar: Math.max(yuvarlanmis, ASGARI_UCRET), paraBirimi: "kurus" };
  },
};
// dar/tasiyici-secimi.mjs — sinirdan yalniz niyet gecer, ara adim gecmez
import { ucretlendirici } from "./ucret-modulu.mjs";

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

export function enUygunTasiyici(gonderi, indirimOrani) {
  return TASIYICILAR
    .map((t) => {
      const fiyat = ucretlendirici.fiyatla(gonderi, { hizCarpani: t.carpan, indirimOrani });
      return { tasiyici: t.ad, tutar: fiyat.tutar };
    })
    .sort((a, b) => a.tutar - b.tutar)[0];
}

İki tasarımın karşılaştırılabilmesi için aynı sonucu üretmeleri gerekir.

// secim-karsilastir.mjs — iki sinir tanimi ayni sonucu mu veriyor
import { enUygunTasiyici as genis } from "./genis/tasiyici-secimi.mjs";
import { enUygunTasiyici as dar } from "./dar/tasiyici-secimi.mjs";

const DENEMELER = [
  [{ agirlik: 3, postaKodu: "06800" }, 0],
  [{ agirlik: 12, postaKodu: "65100" }, 0.1],
  [{ agirlik: 0.4, postaKodu: "34710" }, 0.25],
];
for (const [g, indirim] of DENEMELER) {
  const a = genis(g, indirim), b = dar(g, indirim);
  console.log(`${String(g.agirlik).padStart(4)} kg ${g.postaKodu} indirim ${indirim}  ${a.tasiyici}/${a.tutar}  ${b.tasiyici}/${b.tutar}`);
}
   3 kg 06800 indirim 0  ekonomik/9750  ekonomik/9750
  12 kg 65100 indirim 0.1  ekonomik/20650  ekonomik/20650
 0.4 kg 34710 indirim 0.25  standart/3990  standart/3990

Üç denemede de aynı taşıyıcı ve aynı tutar. Fark davranışta değil, sınırda.

Sözleşme Yüzeyinin Ölçülmesi

Ölçüm aracı üç şey sayar: modülün dışa açtığı adlar, sınırdan geçen değerin alanları ve tüketicinin bu ikisinden fiilen andığı adlar.

// yuzey-olc.mjs — bir sinirin sozlesme yuzeyini sayan olcum araci
import { readFileSync } from "node:fs";

const DISA_ACILAN = /^export\s+(?:const|let|function|class)\s+(\w+)/gm;

export const disaAcilanlar = (yol) =>
  [...readFileSync(yol, "utf8").matchAll(DISA_ACILAN)].map((m) => m[1]);

export function ithalEdilenler(tuketici, kaynakEki) {
  const kalip = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*"[^"]*${kaynakEki}"`, "g");
  return [...readFileSync(tuketici, "utf8").matchAll(kalip)]
    .flatMap((m) => m[1].split(",").map((s) => s.trim())).filter(Boolean);
}

export const okunanAlanlar = (tuketici, degisken) =>
  [...new Set([...readFileSync(tuketici, "utf8")
    .matchAll(new RegExp(`\\b${degisken}\\.(\\w+)`, "g"))].map((m) => m[1]))];

Sınırdan geçen alan sayısı kaynak metinden değil, çağrının gerçek dönüş değerinden alınır; böylece dolaylı olarak eklenen alanlar da sayıya girer.

// yuzey-raporu.mjs — iki sinir tanimini ayni olculerle karsilastirir
import { disaAcilanlar, ithalEdilenler, okunanAlanlar } from "./yuzey-olc.mjs";
import { ucretDokumu } from "./genis/ucret-modulu.mjs";
import { ucretlendirici } from "./dar/ucret-modulu.mjs";

const ORNEK = { agirlik: 3, postaKodu: "06800" };
const SINIRLAR = [
  { ad: "genis", modul: "genis/ucret-modulu.mjs", tuketici: "genis/tasiyici-secimi.mjs",
    degisken: "dokum", gecen: ucretDokumu(ORNEK) },
  { ad: "dar  ", modul: "dar/ucret-modulu.mjs", tuketici: "dar/tasiyici-secimi.mjs",
    degisken: "fiyat", gecen: ucretlendirici.fiyatla(ORNEK) },
];

for (const s of SINIRLAR) {
  const disa = disaAcilanlar(s.modul);
  const gecen = Object.keys(s.gecen);
  const ithal = ithalEdilenler(s.tuketici, "ucret-modulu.mjs");
  const okunan = okunanAlanlar(s.tuketici, s.degisken);
  console.log(`${s.ad} sinir`);
  console.log(`  disa acilan ad        ${String(disa.length).padStart(2)}   ${disa.join(", ")}`);
  console.log(`  sinirdan gecen alan   ${String(gecen.length).padStart(2)}   ${gecen.join(", ")}`);
  console.log(`  tuketicinin ithali    ${String(ithal.length).padStart(2)}   ${ithal.join(", ")}`);
  console.log(`  tuketicinin okudugu   ${String(okunan.length).padStart(2)}   ${okunan.join(", ")}`);
  console.log(`  bilinmesi zorunlu ad  ${String(ithal.length + okunan.length).padStart(2)}`);
}
genis sinir
  disa acilan ad         9   KADEMELER, ASGARI_UCRET, YUVARLAMA_ADIMI, KATSAYI, POSTA_BOLGE, kademeBul, bolgeBul, yuvarla, ucretDokumu
  sinirdan gecen alan    8   kademeUst, kademeUcret, bolge, katsayi, hamTutar, yuvarlanmis, asgariUygulandi, toplam
  tuketicinin ithali     3   ucretDokumu, yuvarla, ASGARI_UCRET
  tuketicinin okudugu    2   hamTutar, bolge
  bilinmesi zorunlu ad   5
dar   sinir
  disa acilan ad         1   ucretlendirici
  sinirdan gecen alan    2   tutar, paraBirimi
  tuketicinin ithali     1   ucretlendirici
  tuketicinin okudugu    1   tutar
  bilinmesi zorunlu ad   2

Dışa açılan ad dokuzdan bire, sınırdan geçen alan sekizden ikiye, karşı tarafın bilmek zorunda olduğu ad beşten ikiye indi. Üç ölçünün üçü de aynı yöne bakıyor.

Dışa açılan ad sayısı ile bilinmesi zorunlu ad sayısı arasındaki fark ayrıca anlamlıdır. Geniş sınırda dokuz ad açık ama tüketici beşini anıyor; kalan dört ad kullanılmadığı hâlde sözleşmenin parçasıdır, çünkü başka bir tüketici onlara bağlanabilir. Sözleşme, fiilen kullanılan yüzey değil, kullanılabilir yüzeydir.

Sınır Neyi Korur

Ölçünün karşılığı, modül sahibinin iç adlandırmayı değiştirmekte ne kadar özgür olduğudur. Aşağıdaki komut her iki tasarımın bir kopyasını alıp ücretlendirme modülündeki hamTutar adını araToplam yapıyor. Tüketici dosyalarına dokunulmuyor.

cp -r genis genis-v2 && cp -r dar dar-v2
sed -i.yedek 's/hamTutar/araToplam/g' genis-v2/ucret-modulu.mjs
sed -i.yedek 's/hamTutar/araToplam/g' dar-v2/ucret-modulu.mjs
grep -c araToplam genis-v2/ucret-modulu.mjs dar-v2/ucret-modulu.mjs
genis-v2/ucret-modulu.mjs:3
dar-v2/ucret-modulu.mjs:2

BSD ve GNU sed arasında yerinde düzenleme seçeneğinin yazımı farklıdır; -i.yedek biçimi ikisinde de çalışır ve yanına bir yedek dosya bırakır.

// kirilma-dene.mjs — modul sahibi bir ic adi degistirdiginde hangi taraf kiriliyor
import { enUygunTasiyici as genisV2 } from "./genis-v2/tasiyici-secimi.mjs";
import { enUygunTasiyici as darV2 } from "./dar-v2/tasiyici-secimi.mjs";

const G = { agirlik: 3, postaKodu: "06800" };
console.log("genis-v2 ->", JSON.stringify(genisV2(G, 0)));
console.log("dar-v2   ->", JSON.stringify(darV2(G, 0)));
genis-v2 -> {"tasiyici":"hizli","tutar":null,"bolge":"orta"}
dar-v2   -> {"tasiyici":"ekonomik","tutar":9750}

Geniş sınırda üç satırlık bir iç yeniden adlandırma tüketiciyi bozdu; üstelik hata fırlatmadan bozdu. Okunmayan alan undefined döndü, çarpım NaN üretti ve JSON’a dönüştüğünde null oldu. Dar sınırda aynı değişiklik hiçbir şeye dokunmadı, çünkü hamTutar sınırın dışına hiç çıkmamıştı.

Sessiz bozulma buradaki asıl bulgudur. Geniş bir sınır yalnız değişimi zorlaştırmaz; değişimin sonucunu da gizler.

Sınır Nereye Çizilir

Ölçüler bir tasarımın iyiliğini kanıtlamaz, karşılaştırma için ortak zemin verir. Sınırın yerini seçerken üç soru sorulur.

Birincisi: sınırdan veri mi geçiyor, niyet mi? Geniş tasarımda döküm nesnesi bir hesap adımının fotoğrafıdır; dar tasarımda parametreler bir isteğin tarifidir. Bir önceki konudaki sorma, söyle ilkesinin modül ölçeğindeki karşılığı budur.

İkincisi: aynı hesap iki tarafta da yazılıyor mu? Geniş tasarımda yuvarlama ve asgari ücret adımları hem ücretlendirme modülünde hem seçim modülünde duruyordu. Kuralın iki yerde durması, sınırın yanlış yerden geçtiğinin en somut belirtisidir.

Üçüncüsü: sınır hangi eksende değişime dayanıklı? Dar tasarım iç adlandırmaya ve ara adımların sırasına dayanıklıdır ama taşıyıcı çarpanının parametre olduğu varsayımına bağlıdır. Her sınır bir şeyi korur, hepsini değil; korunan şeyin ne olduğu yazılabilir olmalıdır.

Özet

  • Sözleşme yüzeyi üç sayıyla ölçülür: dışa açılan ad sayısı, sınırdan geçen değerin alan sayısı ve karşı tarafın bilmek zorunda olduğu ad sayısı.
  • Aynı davranışı veren iki tasarımda bu üç sayı sırasıyla 9→1, 8→2 ve 5→2 olarak değişti.
  • Dışa açılan ad sayısı ile fiilen kullanılan ad sayısı ayrıdır; sözleşme kullanılabilir yüzeydir, kullanılan yüzey değil.
  • Ücretlendirme modülünde üç satırlık bir iç yeniden adlandırma geniş sınırda tüketiciyi sessizce bozdu, dar sınırda hiçbir etki yapmadı.
  • Sınırın yeri üç soruyla seçilir: veri mi niyet mi geçiyor, aynı hesap iki tarafta da var mı, sınır hangi eksende değişime dayanıklı.

Sonraki Adım

Dar sınırın koruduğu şey kitaplığın kendi iç adlandırmasıydı. Kitaplığın dışında ise kendisinin yazmadığı bir kod sınıfı duruyor: istek yönlendiren, süreç başlatan, yaşam döngüsünü yöneten araçlar. Bunlar bir sözleşme sunar ve o sözleşme kitaplığın denetiminde değildir. Sonraki ders küçük bir dış araç tanımlar, iki düzende ona kaç dosyanın dokunduğunu sayar, aracın sözleşmesi değiştiğinde her iki düzende kaç dosyanın düzeltilmesi gerektiğini ölçer ve yalıtımın bedelini de aynı ölçekte gösterir.

İ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