İçeriğe geç
academia.sh

Ders 06 / 16

Doğru Yapıyı Seçmek

Aynı davranışın koşul zinciri, arama tablosu ve çok biçimlilikle üç sürümünün yazılması ve seçimin ölçüye bağlanması: yeni bir tarife türü eklendiğinde dokunulan dosya ve satır sayısı, tür adının kaç yerde geçtiği ve bir türün kendi durumunu taşımasının maliyeti.

İçindekiler

Önceki derste bölge katsayıları koşul zincirinden bir tabloya taşındı ve karmaşıklık düştü. Bu dönüşüm her zaman doğru değildir. Tablo, bir anahtara karşılık bir değer döndürür; karşılık bir değer değil de bir davranışsa, ya da her seçenek kendi verisini taşıyorsa üçüncü bir seçenek belirir: çok biçimlilik.

Üçü arasında seçim, üslup meselesi gibi görünür. Ölçülebilir bir yanı vardır: tasarımın maliyeti, üzerinde yapılacak değişiklikte ortaya çıkar. Bu ders aynı davranışı üç yapıyla yazar, sonra üçüne de aynı değişikliği uygular ve dokunulan dosya ile satır sayısını karşılaştırır.

Ortak Hesap

Üç tasarım da aynı temel ücretle başlar: kademe ücreti çarpı bölge katsayısı. Ayrıldıkları nokta, tarife türünün seçildiği yerdir. Üç tür var (standart, ekspres, ekonomik) ve her tür üç işlemde birden görünür: ücret, teslim günü, etiket.

mkdir -p kosul tablo cokbicim
// ortak.mjs — uc tasarimin paylastigi temel ucret hesabi
const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8 };
const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 },
  { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }];

export function temelUcret(gonderi) {
  const kademe = AGIRLIK_KADEMELERI.find((k) => gonderi.agirlikKg <= k.ustSinirKg);
  return kademe.kiloBasiUcret * gonderi.agirlikKg * BOLGE_KATSAYILARI[gonderi.bolgeKodu];
}

Üç Tasarım

Koşul zinciri. Tür kararı her işlemde yeniden verilir.

// kosul/ucret.mjs — tarife turu her islemde kosul zinciriyle secilir
import { temelUcret } from "../ortak.mjs";

export function ucret(gonderi, tur) {
  const temel = temelUcret(gonderi);
  if (tur === "standart") return temel;
  if (tur === "ekspres") return temel * 1.6 + 25;
  if (tur === "ekonomik") return Math.max(temel * 0.8, 65);
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}

export function teslimGunu(tur) {
  if (tur === "standart") return 3;
  if (tur === "ekspres") return 1;
  if (tur === "ekonomik") return 6;
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}

export function etiket(tur) {
  if (tur === "standart") return "Standart";
  if (tur === "ekspres") return "Ekspres";
  if (tur === "ekonomik") return "Ekonomik";
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}

Arama tablosu. Tür kararı bir kez verilir; bir tür bir veri satırıdır.

// tablo/ucret.mjs — tarife turleri tek tabloda, satir basina bir tur
import { temelUcret } from "../ortak.mjs";

const TARIFELER = {
  standart: { etiket: "Standart", gun: 3, hesap: (temel) => temel },
  ekspres: { etiket: "Ekspres", gun: 1, hesap: (temel) => temel * 1.6 + 25 },
  ekonomik: { etiket: "Ekonomik", gun: 6, hesap: (temel) => Math.max(temel * 0.8, 65) },
};

function tarife(tur) {
  const secilen = TARIFELER[tur];
  if (secilen === undefined) throw new RangeError(`bilinmeyen tarife: ${tur}`);
  return secilen;
}

export const ucret = (gonderi, tur) => tarife(tur).hesap(temelUcret(gonderi));
export const teslimGunu = (tur) => tarife(tur).gun;
export const etiket = (tur) => tarife(tur).etiket;

Çok biçimlilik. Bir tür bir tiptir; tipler aynı üyeleri gerçekleştirir ve bir kütüğe kaydedilir.

// cokbicim/turler.mjs — her tur ayni uc uyeyi gerceklestirir
export class Standart {
  etiket = "Standart";
  gun = 3;
  hesap(temel) { return temel; }
}

export class Ekspres {
  etiket = "Ekspres";
  gun = 1;
  hesap(temel) { return temel * 1.6 + 25; }
}

export class Ekonomik {
  etiket = "Ekonomik";
  gun = 6;
  hesap(temel) { return Math.max(temel * 0.8, 65); }
}
// cokbicim/ucret.mjs — turler bir kutuge kaydedilir, secim kutukten yapilir
import { temelUcret } from "../ortak.mjs";
import { Standart, Ekspres, Ekonomik } from "./turler.mjs";

const KUTUK = { standart: new Standart(), ekspres: new Ekspres(), ekonomik: new Ekonomik() };

function tarife(tur) {
  const secilen = KUTUK[tur];
  if (secilen === undefined) throw new RangeError(`bilinmeyen tarife: ${tur}`);
  return secilen;
}

export const ucret = (gonderi, tur) => tarife(tur).hesap(temelUcret(gonderi));
export const teslimGunu = (tur) => tarife(tur).gun;
export const etiket = (tur) => tarife(tur).etiket;

Üç tasarımın aynı sonuçları verdiği doğrulanır.

// karsilastir.mjs — uc tasarimin ayni sonuclari verdigi dogrulanir
import * as kosul from "./kosul/ucret.mjs";
import * as tablo from "./tablo/ucret.mjs";
import * as cokbicim from "./cokbicim/ucret.mjs";

const gonderi = { agirlikKg: 4.2, bolgeKodu: 2 };
for (const tur of ["standart", "ekspres", "ekonomik"]) {
  const satir = [kosul, tablo, cokbicim].map((t) =>
    `${t.etiket(tur)}/${t.teslimGunu(tur)}/${t.ucret(gonderi, tur).toFixed(2)}`);
  const ayni = satir.every((s) => s === satir[0]);
  console.log(`${tur.padEnd(10)} ${ayni ? "ayni  " : "FARKLI"} ${satir[0]}`);
}
standart   ayni   Standart/3/147.42
ekspres    ayni   Ekspres/1/260.87
ekonomik   ayni   Ekonomik/6/117.94

Değişiklik: Dördüncü Tür

Soğuk zincir tarifesi açılıyor: ücreti temel ücretin 2,2 katı artı kırk lira soğutma bedeli, teslim süresi iki gün. Değişiklik üç tasarımın kopyasına uygulanır.

cp -r kosul kosul-yeni && cp -r tablo tablo-yeni && cp -r cokbicim cokbicim-yeni
// kosul-yeni/ucret.mjs — her zincire bir dal daha
import { temelUcret } from "../ortak.mjs";

export function ucret(gonderi, tur) {
  const temel = temelUcret(gonderi);
  if (tur === "standart") return temel;
  if (tur === "ekspres") return temel * 1.6 + 25;
  if (tur === "ekonomik") return Math.max(temel * 0.8, 65);
  if (tur === "soguk-zincir") return temel * 2.2 + 40;
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}

export function teslimGunu(tur) {
  if (tur === "standart") return 3;
  if (tur === "ekspres") return 1;
  if (tur === "ekonomik") return 6;
  if (tur === "soguk-zincir") return 2;
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}

export function etiket(tur) {
  if (tur === "standart") return "Standart";
  if (tur === "ekspres") return "Ekspres";
  if (tur === "ekonomik") return "Ekonomik";
  if (tur === "soguk-zincir") return "Soguk Zincir";
  throw new RangeError(`bilinmeyen tarife: ${tur}`);
}
// tablo-yeni/ucret.mjs — tabloya bir satir daha
import { temelUcret } from "../ortak.mjs";

const TARIFELER = {
  standart: { etiket: "Standart", gun: 3, hesap: (temel) => temel },
  ekspres: { etiket: "Ekspres", gun: 1, hesap: (temel) => temel * 1.6 + 25 },
  ekonomik: { etiket: "Ekonomik", gun: 6, hesap: (temel) => Math.max(temel * 0.8, 65) },
  "soguk-zincir": { etiket: "Soguk Zincir", gun: 2, hesap: (temel) => temel * 2.2 + 40 },
};

function tarife(tur) {
  const secilen = TARIFELER[tur];
  if (secilen === undefined) throw new RangeError(`bilinmeyen tarife: ${tur}`);
  return secilen;
}

export const ucret = (gonderi, tur) => tarife(tur).hesap(temelUcret(gonderi));
export const teslimGunu = (tur) => tarife(tur).gun;
export const etiket = (tur) => tarife(tur).etiket;
// cokbicim-yeni/soguk-zincir.mjs — yeni tur, kendi dosyasinda
export class SogukZincir {
  etiket = "Soguk Zincir";
  gun = 2;
  hesap(temel) { return temel * 2.2 + 40; }
}
// cokbicim-yeni/ucret.mjs — kutuge bir kayit daha
import { temelUcret } from "../ortak.mjs";
import { Standart, Ekspres, Ekonomik } from "./turler.mjs";
import { SogukZincir } from "./soguk-zincir.mjs";

const KUTUK = { standart: new Standart(), ekspres: new Ekspres(), ekonomik: new Ekonomik(),
  "soguk-zincir": new SogukZincir() };

function tarife(tur) {
  const secilen = KUTUK[tur];
  if (secilen === undefined) throw new RangeError(`bilinmeyen tarife: ${tur}`);
  return secilen;
}

export const ucret = (gonderi, tur) => tarife(tur).hesap(temelUcret(gonderi));
export const teslimGunu = (tur) => tarife(tur).gun;
export const etiket = (tur) => tarife(tur).etiket;

Ölçüm

Ölçer her tasarımın eski ve yeni dizinini dosya dosya karşılaştırır; çıkan satır değiştirilmiş ya da silinmiş, giren satır eklenmiş demektir.

// degisim-olc.mjs — ayni tur eklemesi uc tasarimda kac dosyaya ve kac satira dokunuyor
import { execSync } from "node:child_process";
import { readdirSync } from "node:fs";

// Ilk satir dosya adi isaretidir, karsilastirmaya girmez.
const fark = (eski, yeni) =>
  execSync(`diff <(tail -n +2 ${eski} 2>/dev/null) <(tail -n +2 ${yeni} 2>/dev/null) || true`,
    { shell: "/bin/bash" }).toString().split("\n");

for (const tasarim of ["kosul", "tablo", "cokbicim"]) {
  const adlar = [...new Set([...readdirSync(tasarim), ...readdirSync(`${tasarim}-yeni`)])].sort();
  let dokunulan = 0, cikan = 0, giren = 0;
  console.log(tasarim);
  for (const ad of adlar) {
    const satirlar = fark(`${tasarim}/${ad}`, `${tasarim}-yeni/${ad}`);
    const c = satirlar.filter((s) => s.startsWith("< ")).length;
    const g = satirlar.filter((s) => s.startsWith("> ")).length;
    if (c + g > 0) dokunulan += 1;
    [cikan, giren] = [cikan + c, giren + g];
    console.log(`  ${ad.padEnd(18)} cikan=${c}  giren=${g}`);
  }
  console.log(`  ${"= toplam".padEnd(18)} dosya=${dokunulan}  cikan=${cikan}  giren=${giren}`);
}
kosul
  ucret.mjs          cikan=0  giren=3
  = toplam           dosya=1  cikan=0  giren=3
tablo
  ucret.mjs          cikan=0  giren=1
  = toplam           dosya=1  cikan=0  giren=1
cokbicim
  soguk-zincir.mjs   cikan=0  giren=5
  turler.mjs         cikan=0  giren=0
  ucret.mjs          cikan=1  giren=3
  = toplam           dosya=2  cikan=1  giren=8

İkinci ölçüm, tür adının kaç yerde geçtiğini sayar. Bu sayı, bir türün değişmesi hâlinde aranacak yer sayısıdır.

grep -c ekonomik kosul/ucret.mjs tablo/ucret.mjs cokbicim/ucret.mjs
kosul/ucret.mjs:3
tablo/ucret.mjs:1
cokbicim/ucret.mjs:1

Sayıların Söylediği

Bu değişiklikte en ucuz tasarım tablodur: bir dosya, bir satır. Koşul zinciri üç satır ekletti, çünkü tür kararı üç ayrı yerde veriliyor. Bu üç sayının kaynağı ikinci ölçümdedir: ekonomik adı koşul zincirinde üç yerde geçiyor, diğer ikisinde bir yerde.

Fark, işlem sayısıyla birlikte büyür. Tarife türlerine dördüncü bir işlem eklendiğinde (örneğin sigorta oranı) koşul zincirine dördüncü bir fonksiyon eklenir ve o fonksiyon her türü yeniden sayar; tabloda her satıra bir alan eklenir. İşlem sayısı N, tür sayısı T iken koşul zinciri N × T karar taşır, tablo T satır taşır.

Çok biçimlilik bu değişiklikte en pahalısıdır: iki dosya, sekiz giren satır. Sekiz satırın beşi yeni tür dosyasının kendisi, üçü tür kaydına ekleme. Buna karşılık turler.mjs hiç açılmadı: var olan türlerin tanımlarına dokunulmadı. Tabloda da var olan satırlar değişmedi, ama yeni satır var olan tablo nesnesinin içine girdi.

Aradaki asıl fark satır sayısında değil, bir türün ne taşıyabildiğindedir. Tablo satırı bir veri kaydıdır; her satır aynı alanları aynı biçimde doldurmak zorundadır. Tip ise kendi durumunu taşıyabilir.

// sozlesmeli.mjs — durumunu tasiyan tarife turu: bir davranis tanimi, uc ornek
import { temelUcret } from "./ortak.mjs";

class SozlesmeliTarife {
  constructor(etiket, gun, carpan, sabitUcret) {
    Object.assign(this, { etiket, gun, carpan, sabitUcret });
  }
  hesap(temel) { return temel * this.carpan + this.sabitUcret; }
}

const SOZLESMELER = [
  new SozlesmeliTarife("Musteri A", 2, 1.1, 0),
  new SozlesmeliTarife("Musteri B", 3, 0.9, 15),
  new SozlesmeliTarife("Musteri C", 1, 1.4, 30),
];

const gonderi = { agirlikKg: 4.2, bolgeKodu: 2 };
for (const t of SOZLESMELER) {
  console.log(`${t.etiket.padEnd(10)} ${t.gun} gun  ${t.hesap(temelUcret(gonderi)).toFixed(2)}`);
}
console.log(`tarife sayisi=${SOZLESMELER.length}  davranis tanimi=1`);
Musteri A  2 gun  162.16
Musteri B  3 gun  147.68
Musteri C  1 gun  236.39
tarife sayisi=3  davranis tanimi=1

Üç sözleşmeli tarife tek bir davranış tanımından üretildi. Sözleşme sayısı üç yerine üç yüz olsaydı kod satırı sayısı değişmezdi; artan şey yalnız veri olurdu. Koşul zincirinde her sözleşme üç dal, tabloda her sözleşme bir satır eklerdi.

Seçim Ölçütü

Ölçümler üç kuralı destekliyor.

Koşul zinciri, tür sayısı da işlem sayısı da küçük ve sabit olduğunda yerindedir; kararın tek bir yerde ve göz önünde durması bir kazançtır. Aynı ayrım ikinci bir yerde tekrarlandığı anda maliyeti çarpmaya başlar.

Arama tablosu, seçenekler aynı biçimi paylaşan veri kayıtlarıysa ve seçim bir anahtara bakmaktan ibaretse en ucuzudur. Sınırı, her satırın aynı alanları doldurmak zorunda olmasıdır: bir seçenek fazladan bir bilgiye ihtiyaç duyduğunda tüm satırların biçimi değişir.

Çok biçimlilik, her seçenek kendi durumunu taşıdığında, birden çok işlemi birlikte gerçekleştirdiğinde ya da seçenek sayısı veriyle belirlendiğinde karşılığını verir. Sabit bedeli vardır — bu değişiklikte iki dosya ve sekiz satır — ve seçenekler basit veriyken bu bedel boşa gider.

Özet

  • Aynı davranış üç yapıyla yazıldı ve üçü de aynı sonuçları üretti; karşılaştırma davranış sabitken yapıldı.
  • Dördüncü tarife türü eklendiğinde tablo bir dosyada bir satır, koşul zinciri bir dosyada üç satır, çok biçimlilik iki dosyada sekiz satır değişiklik istedi.
  • Tür adının geçtiği yer sayısı koşul zincirinde üç, diğer ikisinde birdir; işlem sayısı N ve tür sayısı T iken koşul zinciri N × T karar taşır, tablo T satır taşır.
  • Çok biçimlilikte var olan türlerin dosyası hiç açılmadı; bedeli, yeni tür için ayrı bir dosya ve tür kaydına bir satırdır.
  • Tablo satırı sabit biçimli bir veri kaydıdır; tip kendi durumunu taşır. Üç sözleşmeli tarife tek bir davranış tanımından üretildi, sözleşme sayısı arttığında kod satırı artmaz.

Sonraki Adım

Bu konu boyunca kararlar kodun yapısı üzerineydi: adlar, düzeyler, biçim, dallanma ve seçenek yapısı. Bir fonksiyonun dışarıya söyledikleri henüz ele alınmadı. tarife fonksiyonu bilinmeyen bir tür için hata atıyor; oysa aynı sorunun yaygın çözümü boş bir değer döndürmektir. Boş değer döndüren bir imza, çağıran tarafa hiçbir şey söylemez ve hata, boşluğun üretildiği yerde değil kullanıldığı yerde ortaya çıkar. Sonraki konu bu mesafeyi ölçerek başlar: boş değer geçirilen bir çağrı zincirinde hata, kaç çağrı sonra görünür hâle gelir.

İ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