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 iç 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.