İçeriğe geç
academia.sh

Ders 03 / 30

Kurucu

Çok alanlı bir nesnenin adım adım kurulması: sekiz sıralı parametreli çağrı ile adlandırılmış adımlardan oluşan kurucunun aynı bozma kümesiyle karşılaştırılması, yer değiştiren argümanların ve eksik alanların kaç durumda sessizce kabul edildiğinin sayılması, geçirilen parametre sayısının düşüşü ve kalıbın satır cinsinden bedeli.

İçindekiler

Soyut fabrika üç parçayı birlikte döndürdü, ama parçaların her biri tek ifadeyle kurulabilen bir sabitti. Kitaplığın merkezindeki nesne öyle değil: bir gönderinin ağırlığı, üç boyutu, gönderim ve teslim ili, beyan edilen değeri ve varsa sözleşme numarası var. Sekiz alan tek bir çağrıya sıkıştırıldığında çağrı yerinde hangi sayının hangi alana gittiği görünmez olur.

Kurucu (builder) kurulumu adlandırılmış adımlara böler ve nesneyi ancak son adımda üretir. Bu dersin ölçüsü kurulumun ne kadar okunaklı göründüğü değil: aynı bozma kümesi iki tasarıma uygulanır ve kaç bozmanın sessizce kabul edildiği sayılır. Kalıbın bedeli de aynı biçimde, satır sayısıyla ölçülür.

Sekiz Sıralı Parametre

Birinci sürümde gönderi tek bir çağrıyla kurulur. Hiçbir alan denetlenmez; çağrıya ne verilirse nesneye o girer.

// dogrudan/gonderi.mjs — sekiz sirali parametre; hicbir alan denetlenmiyor
export function gonderiYap(agirlik, en, boy, yukseklik, gonderimIli, teslimIli, deger, sozlesmeNo) {
  return { agirlik, en, boy, yukseklik, gonderimIli, teslimIli, deger, sozlesmeNo };
}

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const desi = (g) => Math.ceil((g.en * g.boy * g.yukseklik) / 5000);

export function ucret(g) {
  const taban = 3900 + Math.ceil(Math.max(g.agirlik, desi(g))) * 900;
  const katsayi = Math.max(BOLGE[g.gonderimIli] ?? 160, BOLGE[g.teslimIli] ?? 160);
  const sigorta = Math.round(g.deger * 0.004);
  const indirim = typeof g.sozlesmeNo === "string" ? 500 : 0;
  return Math.round((taban * katsayi) / 100) + sigorta - indirim;
}

Ücret sekiz alanın yedisini kullanır: ağırlık ile üç boyut taban ücrete, iki il kodu bölge katsayısına, beyan değeri sigortaya, sözleşme numarası indirime girer. Yani bir alanın yanlış yere gitmesi ücreti değiştirir.

Adlandırılmış Adımlar

İkinci sürümde her adım bir yöntemdir, kendi alanını denetler ve kurucunun kendisini döndürür. Nesne kur çağrısında üretilir; zorunlu alanların eksiksizliği yalnız orada, tek bir noktada denetlenir.

// kurucu/gonderi.mjs — adlandirilmis adimlar; her adim kendi alanini denetler
const BOLGE = { "34": 100, "06": 115, "65": 140 };
const IL = /^[0-9]{2}$/;
const ZORUNLU = ["agirlik", "en", "boy", "yukseklik", "gonderimIli", "teslimIli", "deger"];

const sayi = (x, ust) => typeof x === "number" && x > 0 && x <= ust;

export class GonderiKurucu {
  #alan = {};

  agirlik(kg) {
    if (!sayi(kg, 50)) throw new RangeError("agirlik 0-50 kg araliginda bir sayi olmali");
    this.#alan.agirlik = kg;
    return this;
  }

  olculer(en, boy, yukseklik) {
    if (![en, boy, yukseklik].every((x) => sayi(x, 200)))
      throw new RangeError("olcu 0-200 cm araliginda bir sayi olmali");
    Object.assign(this.#alan, { en, boy, yukseklik });
    return this;
  }

  adres(gonderimIli, teslimIli) {
    if (![gonderimIli, teslimIli].every((i) => typeof i === "string" && IL.test(i)))
      throw new RangeError("il kodu iki basamakli bir dizgi olmali");
    Object.assign(this.#alan, { gonderimIli, teslimIli });
    return this;
  }

  deger(kurus) {
    if (!Number.isInteger(kurus) || kurus < 0)
      throw new RangeError("deger negatif olmayan tam sayi olmali");
    this.#alan.deger = kurus;
    return this;
  }

  sozlesme(no) {
    if (typeof no !== "string" || !/^S[0-9]{4}$/.test(no))
      throw new RangeError("sozlesme numarasi S ve dort basamak olmali");
    this.#alan.sozlesmeNo = no;
    return this;
  }

  kur() {
    const eksik = ZORUNLU.filter((k) => this.#alan[k] === undefined);
    if (eksik.length > 0) throw new TypeError(`eksik alan: ${eksik.join(", ")}`);
    return Object.freeze({ sozlesmeNo: null, ...this.#alan });
  }
}

const desi = (g) => Math.ceil((g.en * g.boy * g.yukseklik) / 5000);

export function ucret(g) {
  const taban = 3900 + Math.ceil(Math.max(g.agirlik, desi(g))) * 900;
  const katsayi = Math.max(BOLGE[g.gonderimIli] ?? 160, BOLGE[g.teslimIli] ?? 160);
  const sigorta = Math.round(g.deger * 0.004);
  const indirim = typeof g.sozlesmeNo === "string" ? 500 : 0;
  return Math.round((taban * katsayi) / 100) + sigorta - indirim;
}

Üç ayrıntı kalıbın parçasıdır. Her adım this döndürür, böylece adımlar zincirlenebilir. Zorunlu alan listesi tek bir yerdedir, dolayısıyla eksiklik denetimi de tek yerdedir. Üretilen nesne dondurulmuştur; kurulum bittikten sonra alan değiştirilemez ve sözleşme numarası verilmemişse varsayılan null değeri kur tarafından konur.

// kur.mjs — ayni gonderi iki bicimde kurulur
import { gonderiYap, ucret as dogrudanUcret } from "./dogrudan/gonderi.mjs";
import { GonderiKurucu, ucret as kurucuUcret } from "./kurucu/gonderi.mjs";

const g1 = gonderiYap(2.4, 30, 40, 50, "34", "06", 180000, "S1042");
const g2 = new GonderiKurucu()
  .agirlik(2.4)
  .olculer(30, 40, 50)
  .adres("34", "06")
  .deger(180000)
  .sozlesme("S1042")
  .kur();

console.log(`dogrudan ${dogrudanUcret(g1)} kurus`);
console.log(`kurucu   ${kurucuUcret(g2)} kurus`);
node kur.mjs
dogrudan 17125 kurus
kurucu   17125 kurus

Aynı ücret. Bir çağrı satırı yedi satıra çıktı; kazancın bu satırlara değip değmediği sayılabilir.

Bozma Kümesinin Uygulanması

Aşağıdaki ölçer iki bozma kümesi üretir. Birincisi doğru değer dizisindeki her argüman çiftini yer değiştirir: sekiz alan için 28 çift. İkincisi her alanı sırayla eksiltir: 8 çağrı. Her bozulmuş çağrının sonucu dört sınıfa ayrılır — kurulum sırasında hata atanlar, sayı olmayan bir sonuç verenler, doğru sonucun aynısını verenler ve hata atmadan farklı bir ücret üretenler. Son sınıf sessizce kabul edilen yanlış kurulumlardır.

// sinama.mjs — bozulmus cagrilari uretir ve sonuclarini siniflandirir
import { readFileSync } from "node:fs";
import { gonderiYap, ucret as dogrudanUcret } from "./dogrudan/gonderi.mjs";
import { GonderiKurucu, ucret as kurucuUcret } from "./kurucu/gonderi.mjs";

const DOGRU = [2.4, 30, 40, 50, "34", "06", 180000, "S1042"];

const dogrudanKur = (v) => dogrudanUcret(gonderiYap(...v));
const kurucuKur = (v) =>
  kurucuUcret(new GonderiKurucu().agirlik(v[0]).olculer(v[1], v[2], v[3])
    .adres(v[4], v[5]).deger(v[6]).sozlesme(v[7]).kur());

function sinifla(kur, degerler, beklenen) {
  let sonuc;
  try {
    sonuc = kur(degerler);
  } catch {
    return "hata";
  }
  if (!Number.isFinite(sonuc)) return "gecersiz";
  return sonuc === beklenen ? "ayni" : "sessiz";
}

function say(etiket, kur, kumeler) {
  const beklenen = kur(DOGRU);
  const sayim = { hata: 0, gecersiz: 0, ayni: 0, sessiz: 0 };
  for (const v of kumeler) sayim[sinifla(kur, v, beklenen)] += 1;
  console.log(`${etiket} cagri=${kumeler.length} hata=${sayim.hata} ` +
    `gecersiz=${sayim.gecersiz} ayni=${sayim.ayni} sessiz-yanlis=${sayim.sessiz}`);
}

const yerDegistirme = [];
for (let i = 0; i < DOGRU.length; i += 1)
  for (let j = i + 1; j < DOGRU.length; j += 1) {
    const v = [...DOGRU];
    [v[i], v[j]] = [v[j], v[i]];
    yerDegistirme.push(v);
  }

const eksikAlan = DOGRU.map((_, i) => DOGRU.map((d, k) => (k === i ? undefined : d)));

const enYuksekArite = (yol) =>
  Math.max(...[...readFileSync(yol, "utf8").matchAll(/\(([^)]*)\)\s*\{/g)]
    .map((m) => m[1].split(",").filter((s) => s.trim() !== "").length));

console.log(`dogrudan en-yuksek-parametre=${enYuksekArite("dogrudan/gonderi.mjs")}`);
console.log(`kurucu   en-yuksek-parametre=${enYuksekArite("kurucu/gonderi.mjs")}`);
say("dogrudan yer-degistirme", dogrudanKur, yerDegistirme);
say("kurucu   yer-degistirme", kurucuKur, yerDegistirme);
say("dogrudan eksik alan    ", dogrudanKur, eksikAlan);
say("kurucu   eksik alan    ", kurucuKur, eksikAlan);
node sinama.mjs
dogrudan en-yuksek-parametre=8
kurucu   en-yuksek-parametre=3
dogrudan yer-degistirme cagri=28 hata=0 gecersiz=5 ayni=4 sessiz-yanlis=19
kurucu   yer-degistirme cagri=28 hata=21 gecersiz=0 ayni=4 sessiz-yanlis=3
dogrudan eksik alan     cagri=8 hata=0 gecersiz=5 ayni=0 sessiz-yanlis=3
kurucu   eksik alan     cagri=8 hata=8 gecersiz=0 ayni=0 sessiz-yanlis=0

Sayıların Okunması

İlk iki satır Bağlaşım Türleri dersinin ölçüsüdür: bir çağrıda geçirilen en yüksek parametre sayısı. Sekizden üçe düştü ve kalan üç parametre aynı türden üç ölçüdür (olculer), yani birlikte anlam taşırlar.

Yer değiştirme kümesinde birinci sürüm 28 bozmanın hiçbirinde hata atmadı. Beşinde sonuç sayı olmadı — bu tespit edilebilir bir bozulmadır. Dördünde sonuç değişmedi. Kalan 19 bozma hata atmadan farklı bir ücret üretti; bunların her biri, kod incelemesinde yakalanmazsa müşteriye yanlış tutar olarak gider.

İkinci sürümde aynı 28 bozmanın 21’i kurulum anında hata attı, çünkü sayı bekleyen bir adıma dizgi ya da aralık dışı bir sayı geldi. Sessizce kabul edilen bozma sayısı 19’dan 3’e indi. Kalan üçü şu üç çiftin yer değiştirmesidir: ağırlık ile en, ağırlık ile boy, ağırlık ile yükseklik. Üçü de aynı türden, aynı aralıkta sayılardır ve hiçbir tip denetimi bunları ayırmaz. Bu artık kurucunun çözebileceği bir problem değildir; kilogram ile santimetreyi ayrı tiplere taşımak gerekir, yani Veri Erişim Katmanı kursunda tanıtılan değer nesnesi.

Dördü değişmedi diye “doğru” sayılmaz. Bu dört bozmanın üçü boyutların desi çarpımındaki simetrisidir: en, boy ve yükseklik birbirinin yerine geçtiğinde çarpım aynı kalır ve ücret değişmez. Dördüncüsü gönderim ve teslim ilinin yer değiştirmesidir; bu tarife iki ilin bölge katsayısının büyüğünü aldığı için ücret aynı çıkar. Ölçü o hatayı görmez, çünkü ücret o hataya duyarlı değildir.

Eksik alan kümesi kalıbın ikinci kazancını verir. Birinci sürüm 8 eksiltmenin hiçbirinde hata vermedi: beşi sayı olmayan bir ücret, üçü sessizce yanlış bir ücret üretti. İkinci sürümde sekizinin sekizi kur çağrısında durdu ve hata iletisi hangi alanın eksik olduğunu adıyla söyledi. Nesnenin değişmezini tek bir noktada denetlemenin ölçülebilir karşılığı budur.

Bedel

Kalıbın maliyeti satır sayısında görünür.

for d in dogrudan/gonderi.mjs kurucu/gonderi.mjs; do
  echo "$d: $(grep -c '' $d) satir"
done
dogrudan/gonderi.mjs: 15 satir
kurucu/gonderi.mjs: 60 satir

Aynı nesneyi kuran modül 15 satırdan 60 satıra çıktı ve çağrı yeri bir satırdan yedi satıra uzadı. Bedelin bir kısmı denetimlere ait, bir kısmı adım yöntemlerinin kendisine. Kurucu bu yüzden alan sayısı azken uygulanmaz: iki üç alanlı bir nesnede yer değiştirme kümesi zaten küçüktür ve 45 satırlık ek metnin karşılığı çıkmaz.

İkinci bir bedel zincirin yarım kalabilmesidir. kur çağrılmadığı sürece kurucu nesnesi elde tutulabilir ve yarı kurulmuş bir durum taşır. Bu durumun dışarı sızmaması, kur dışında hiçbir adımın nesne döndürmemesine bağlıdır; kalıbın doğru uygulanması bu kısıtla birlikte gelir.

Özet

  • Kurucu çok alanlı bir nesnenin kurulumunu adlandırılmış adımlara böler ve nesneyi yalnız son adımda üretir; zorunlu alan denetimi o tek noktada toplanır.
  • Bir çağrıda geçirilen en yüksek parametre sayısı 8’den 3’e düştü ve kalan üç parametre aynı türden üç ölçüdür.
  • 28 argüman yer değiştirmesinden sessizce kabul edilenlerin sayısı 19’dan 3’e indi; kalan üçü aynı türden sayıların yer değiştirmesidir ve ancak ayrı tiplerle önlenebilir.
  • 8 eksik alan çağrısı birinci sürümde hiç hata vermedi, ikinci sürümde sekizi de hangi alanın eksik olduğunu adıyla söyleyerek durdu.
  • Bedel 15 satırdan 60 satıra çıkan modül ve bir satırdan yedi satıra uzayan çağrı yeridir; alan sayısı azken kalıp bu bedeli karşılamaz.

Sonraki Adım

Kurucu her nesneyi sıfırdan kuruyor. Oysa kitaplıkta kurulacak nesnelerin çoğu birbirinin küçük bir sapmasıdır: sözleşmeli bir müşterinin tarifesi standart tarifenin yalnız iki alanı değişmiş hâlidir, bir bölgeye özel tarife de öyle. Bu durumda bütün adımları yeniden yürütmek hem çağrı satırını uzatır hem de standart tarifeye eklenen her yeni alanın türetilmiş tarifelere elle taşınmasını gerektirir. Sonraki ders nesneyi kurmak yerine var olan bir nesneyi kopyalayıp değiştirmeyi ele alır: standart tarifeye yeni bir alan eklendiğinde elle yazan ile kopyalayan sürümlerde kaç türetilmiş tarifede alanın eksik kaldığını sayar ve sığ kopyanın prototiple kaç alt nesne paylaştığını ölçer.

İ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