Ders 03 / 16
Küçük Fonksiyonlar ve Sınıflar
Bölme ölçütünün satır sayısından çıkarılıp tek soyutlama düzeyi kuralına bağlanması: bir gövdenin kaç ayrı soyutlama düzeyinin sözcüğünü taşıdığını sayan çözümleyici, ücret satırı üretiminin iki sürümünde düzey açıklığının karşılaştırılması ve aynı ölçütün sınıflara uygulanması.
İçindekiler
Yorumları ada çevirirken tek bir fonksiyon dört parçaya bölünmüştü ve bölme yerlerini yorumların durduğu yerler belirlemişti. Bu rastlantısal bir ölçüttür: yorum yazma alışkanlığı kişiden kişiye değişir. Bölmenin ölçütü nedir? Satır sayısı akla gelen ilk cevaptır, ama iki satırlık bir fonksiyon da bölünmesi gereken bir şey içerebilir.
Bu ders ölçütü satır sayısından çıkarıp tek soyutlama düzeyi kuralına bağlar. Kural şudur: bir fonksiyonun gövdesi tek bir soyutlama düzeyinin sözcükleriyle konuşmalıdır. Sözcüklerin düzeyleri sayılabildiği için kural da sayılabilir.
Kitaplığın Üç Düzeyi
Ücretlendirme kitaplığında üç ayrı sözcük dağarcığı var ve bunlar birbirinden bağımsız değişir.
- Ölçü düzeyi: gönderinin fiziksel ölçüleri ve bunlardan türetilen ağırlık.
Sözcükleri
enCm,boyCm,yukseklikCm,agirlikKg,HACIMSEL_BOLEN. Taşıyıcı hacimsel böleni değiştirdiğinde yalnız bu düzey değişir. - Ücret politikası düzeyi: kademeler, bölge katsayıları, indirim ve asgari ücret.
Sözcükleri
AGIRLIK_KADEMELERI,ustSinirKg,kiloBasiUcret,BOLGE_KATSAYILARI,SOZLESMELI_MUSTERI_INDIRIMI,ASGARI_UCRET. Tarife görüşmesi bu düzeyi değiştirir. - Sunum düzeyi: sonucun metne dönüşmesi. Sözcükleri
padEnd,toFixed,PARA_BIRIMI. Raporun biçimi değiştiğinde yalnız bu düzey değişir.
Bir gövdede kaç düzeyin sözcüğü geçtiği sayılabilir. İki sayı kullanılacak: düzey sayısı (gövdede sözcüğü geçen düzeylerin adedi) ve açıklık (en yüksek düzey ile en düşük düzey arasındaki basamak farkı). Tek soyutlama düzeyi kuralı, düzey sayısının bir olmasını ister.
Her İşi Yapan Sürüm
İlk sürüm ücret satırını tek fonksiyonda üretir. Yanına, ölçütün satır sayısı olmadığını göstermek için iki satırlık ikinci bir fonksiyon konuldu.
mkdir -p buyuk bolunmus sinif
// buyuk/ucret.mjs — ucret satiri uretimi tek fonksiyonda const HACIMSEL_BOLEN = 3000; const ASGARI_UCRET = 52; const SOZLESMELI_MUSTERI_INDIRIMI = 0.15; const PARA_BIRIMI = "TL"; 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 kisaOzet(gonderi) { const kg = Math.max(gonderi.agirlikKg, (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN); return `${AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg).kiloBasiUcret.toFixed(2)} ${PARA_BIRIMI}/kg`; } export function ucretSatiriUret(gonderi, bolgeKodu) { const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN; const agirlikKg = Math.max(gonderi.agirlikKg, hacimsel); const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg); const ham = kademe.kiloBasiUcret * agirlikKg * BOLGE_KATSAYILARI[bolgeKodu]; const ucret = Math.max(ham * (1 - SOZLESMELI_MUSTERI_INDIRIMI), ASGARI_UCRET); return `${gonderi.kod.padEnd(8)} ${agirlikKg.toFixed(2)} kg ${ucret.toFixed(2)} ${PARA_BIRIMI}`; }
Düzey Ölçer
Çözümleyici kaynağı fonksiyon ve sınıf bloklarına ayırır, her gövdedeki adları toplar ve düzey sözlüğüyle eşleştirir. Bir fonksiyonun parametreleri ve yerel değişkenleri sayılmaz: bunlar fonksiyonun kendi sözcükleridir, üst düzeyle arasındaki bağdır. Sayılan şey, gövdenin kendi dışından uzandığı adlardır — modül sabitleri, okuduğu alan adları ve çağırdığı yerleşik işlemler.
// duzey-olcer.mjs — her fonksiyonun ve sinifin kac soyutlama duzeyinin sozcugunu tasidigi import { readFileSync } from "node:fs"; // Kitapligin uc soyutlama duzeyi ve her duzeyin sozcukleri. const DUZEYLER = [ ["olcu", ["enCm", "boyCm", "yukseklikCm", "agirlikKg", "HACIMSEL_BOLEN"]], ["ucret", ["AGIRLIK_KADEMELERI", "ustSinirKg", "kiloBasiUcret", "BOLGE_KATSAYILARI", "SOZLESMELI_MUSTERI_INDIRIMI", "ASGARI_UCRET"]], ["sunum", ["padEnd", "toFixed", "PARA_BIRIMI"]], ]; const ANAHTAR = new Set(["if", "for", "while", "switch", "catch", "return", "function"]); const derinlikFarki = (s) => (s.match(/{/g) ?? []).length - (s.match(/}/g) ?? []).length; // Bir baslangic oruntusunden kapanan suslu paranteze kadar suren bloklari ayirir. function bloklar(satirlar, oruntu) { const cikti = []; let acik = null, derinlik = 0; satirlar.forEach((satir, i) => { if (acik === null) { const m = satir.match(oruntu); if (!m || ANAHTAR.has(m[1])) return; [acik, derinlik] = [{ ad: m[1], imza: m[2] ?? "", bas: i, govde: [] }, 0]; } else acik.govde.push(satir); derinlik += derinlikFarki(satir); if (derinlik === 0) { cikti.push({ ...acik, son: i }); acik = null; } }); return cikti; } // Parametreler ve yerel degiskenler fonksiyonun kendi sozcukleridir; sayilmaz. function duzeyler(imza, govde) { const kendi = new Set([...imza.matchAll(/\w+/g), ...govde.matchAll(/(?:const|let)\s+(\w+)/g)] .map((m) => m[1] ?? m[0])); const adlar = new Set([...govde.matchAll(/\w+/g)].map((m) => m[0]).filter((a) => !kendi.has(a))); return DUZEYLER.map(([d], i) => [d, i]).filter(([, i]) => DUZEYLER[i][1].some((s) => adlar.has(s))); } const yaz = (ad, ek, bulunan) => { const i = bulunan.map(([, n]) => n); const aciklik = i.length ? Math.max(...i) - Math.min(...i) : 0; console.log(` ${ad.padEnd(26)}${ek} duzey=${bulunan.length} aciklik=${aciklik}` + ` [${bulunan.map(([d]) => d).join(", ") || "yalniz cagri"}]`); }; for (const dosya of process.argv.slice(2)) { const satirlar = readFileSync(dosya, "utf8").split("\n"); const siniflar = bloklar(satirlar, /^\s*(?:export\s+)?class\s+(\w+)/); console.log(dosya); for (const s of [...siniflar, null]) { const kapsam = (f) => (s ? f.bas > s.bas && f.son <= s.son : siniflar.every((k) => f.bas < k.bas || f.son > k.son)); const uyeler = bloklar(satirlar, /^\s*(?:export\s+)?(?:function\s+)?(\w+)\s*\(([^)]*)\)\s*{/) .filter(kapsam); const hepsi = new Map(); for (const f of uyeler) { const bulunan = duzeyler(f.imza, f.govde.join("\n")); for (const [d, i] of bulunan) hepsi.set(d, i); yaz((s ? `${s.ad}.` : "") + f.ad, `satir=${String(f.govde.length - 1).padEnd(2)}`, bulunan); } if (s) yaz(`= ${s.ad}`, `uye=${String(uyeler.length).padEnd(4)}`, [...hepsi]); } }
Bölünmüş Sürüm
İkinci sürüm aynı aritmetiği yapar. Her fonksiyon tek düzeyin sözcükleriyle konuşur; en üstteki fonksiyon hiçbir düzeyin sözcüğüne dokunmaz, yalnız çağırır.
// bolunmus/ucret.mjs — ayni hesap, her fonksiyon tek duzeyde const HACIMSEL_BOLEN = 3000; const ASGARI_UCRET = 52; const SOZLESMELI_MUSTERI_INDIRIMI = 0.15; const PARA_BIRIMI = "TL"; 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 }, ]; function ucretlendirilenAgirlikKg(gonderi) { const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN; return Math.max(gonderi.agirlikKg, hacimsel); } function kademeUcreti(agirlikKg) { const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg); return kademe.kiloBasiUcret * agirlikKg; } function bolgeliUcret(ucret, bolgeKodu) { return ucret * BOLGE_KATSAYILARI[bolgeKodu]; } function indirimliUcret(ucret) { return Math.max(ucret * (1 - SOZLESMELI_MUSTERI_INDIRIMI), ASGARI_UCRET); } function ucretSatiri(gonderiKodu, agirlikKg, ucret) { return `${gonderiKodu.padEnd(8)} ${agirlikKg.toFixed(2)} kg ${ucret.toFixed(2)} ${PARA_BIRIMI}`; } export function ucretSatiriUret(gonderi, bolgeKodu) { const agirlik = ucretlendirilenAgirlikKg(gonderi); const ucret = indirimliUcret(bolgeliUcret(kademeUcreti(agirlik), bolgeKodu)); return ucretSatiri(gonderi.kod, agirlik, ucret); }
İki sürümün aynı satırı ürettiği doğrulanır.
// cagri.mjs — iki surumun ayni satiri urettigi dogrulanir import { ucretSatiriUret as buyuk } from "./buyuk/ucret.mjs"; import { ucretSatiriUret as bolunmus } from "./bolunmus/ucret.mjs"; const gonderi = { kod: "GN-4172", agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18 }; console.log(buyuk(gonderi, 2)); console.log(bolunmus(gonderi, 2));
GN-4172 4.32 kg 128.89 TL GN-4172 4.32 kg 128.89 TL
Ölçüm
node duzey-olcer.mjs buyuk/ucret.mjs bolunmus/ucret.mjs
buyuk/ucret.mjs kisaOzet satir=2 duzey=3 aciklik=2 [olcu, ucret, sunum] ucretSatiriUret satir=6 duzey=3 aciklik=2 [olcu, ucret, sunum] bolunmus/ucret.mjs ucretlendirilenAgirlikKg satir=2 duzey=1 aciklik=0 [olcu] kademeUcreti satir=2 duzey=1 aciklik=0 [ucret] bolgeliUcret satir=1 duzey=1 aciklik=0 [ucret] indirimliUcret satir=1 duzey=1 aciklik=0 [ucret] ucretSatiri satir=1 duzey=1 aciklik=0 [sunum] ucretSatiriUret satir=3 duzey=0 aciklik=0 [yalniz cagri]
Tablo satır sayısı ile düzey sayısını yan yana koyuyor ve ikisinin bağımsız olduğunu
gösteriyor. kisaOzet iki satırdır ve üç düzeyi birden taşır: hacimsel ağırlığı hesaplar,
kademe seçer, biçimlendirir. Bölünmüş sürümdeki ucretSatiriUret üç satırdır ve hiçbir
düzeyin sözcüğüne dokunmaz. Kısalık ölçüt olsaydı ilki kabul edilir, ikincisi ile arasında
fark görülmezdi.
Bölünmüş sürümde altı fonksiyonun beşi tek düzeyde, biri sıfır düzeydedir. Sıfır, gövdenin yalnız çağrılardan oluştuğu anlamına gelir: en üstteki fonksiyon işin sırasını söyler, işin kendisini söylemez. Bir kişi ücretin nasıl hesaplandığını değil hangi adımlardan geçtiğini öğrenmek istiyorsa yalnız o üç satırı okur.
Bölme Nerede Durur
Ölçüt kendi durma koşulunu taşır. Düzey sayısı bire indiğinde ayıklanacak bir şey kalmaz;
bir düzeyi ikiye bölmek yeni bir düzey üretmez, aynı düzeyde iki fonksiyon üretir ve
çağrı sayısını arttırır. bolgeliUcret tek satırlıktır ve daha küçüğe bölünemez.
Ters yönde de bir sınır var. indirimliUcret iki işi yapıyor gibi görünür: indirimi
uygular ve asgari ücrete çeker. Bunlar ayrılabilir, ama ikisi de ücret politikası
düzeyindedir ve sıraları anlamlıdır — indirim asgari ücretten önce uygulanır, tersi
farklı bir sonuç verir. Aynı düzeyde ve sırası bağlayıcı olan iki adım, ayrıldığında
sıranın korunması çağıran tarafın sorumluluğuna geçer. Bölme kararı burada düzey sayısına
değil, sıranın nerede korunacağına bakar.
Aynı Kural Sınıflarda
Sınıf, durumu paylaşan fonksiyonların bir araya getirilmiş hâlidir; ölçüt de aynı ölçütün toplamı olur. Bir sınıfın küçüklüğü yöntem sayısıyla değil, üyelerinin toplamda kaç düzeye yayıldığıyla ölçülür.
// sinif/tek.mjs — tek sinif, uc duzeyin durumunu birlikte tutuyor const HACIMSEL_BOLEN = 3000; const PARA_BIRIMI = "TL"; const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }]; export class GonderiUcreti { constructor(gonderi) { this.gonderi = gonderi; } agirlik() { const hacimsel = (this.gonderi.enCm * this.gonderi.boyCm * this.gonderi.yukseklikCm) / HACIMSEL_BOLEN; return Math.max(this.gonderi.agirlikKg, hacimsel); } ucret() { const kg = this.agirlik(); return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg).kiloBasiUcret * kg; } satir() { return `${this.ucret().toFixed(2)} ${PARA_BIRIMI}`; } }
Bu sınıfın her yöntemi tek düzeydedir; fonksiyon ölçütü hiçbir kusur görmez. Kusur toplamdadır: üç yöntem üç ayrı düzeye ait olduğu için sınıfın üç ayrı değişme nedeni vardır. Ayrılmış sürümde her birim tek düzeyde kalır.
// sinif/ayri.mjs — ayni is, her sinif tek duzeyde const HACIMSEL_BOLEN = 3000; const PARA_BIRIMI = "TL"; const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }]; export class Olcum { constructor(gonderi) { this.gonderi = gonderi; } ucretlendirilenKg() { const hacimsel = (this.gonderi.enCm * this.gonderi.boyCm * this.gonderi.yukseklikCm) / HACIMSEL_BOLEN; return Math.max(this.gonderi.agirlikKg, hacimsel); } } export class Tarife { kademeUcreti(kg) { return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg).kiloBasiUcret * kg; } } export function ucretSatiri(ucret) { return `${ucret.toFixed(2)} ${PARA_BIRIMI}`; }
node duzey-olcer.mjs sinif/tek.mjs sinif/ayri.mjs
sinif/tek.mjs GonderiUcreti.constructor satir=1 duzey=0 aciklik=0 [yalniz cagri] GonderiUcreti.agirlik satir=2 duzey=1 aciklik=0 [olcu] GonderiUcreti.ucret satir=2 duzey=1 aciklik=0 [ucret] GonderiUcreti.satir satir=1 duzey=1 aciklik=0 [sunum] = GonderiUcreti uye=4 duzey=3 aciklik=2 [olcu, ucret, sunum] sinif/ayri.mjs Olcum.constructor satir=1 duzey=0 aciklik=0 [yalniz cagri] Olcum.ucretlendirilenKg satir=2 duzey=1 aciklik=0 [olcu] = Olcum uye=2 duzey=1 aciklik=0 [olcu] Tarife.kademeUcreti satir=1 duzey=1 aciklik=0 [ucret] = Tarife uye=1 duzey=1 aciklik=0 [ucret] ucretSatiri satir=1 duzey=1 aciklik=0 [sunum]
= ile başlayan satırlar sınıfın toplamıdır. GonderiUcreti dört üyeyle üç düzeye
yayılıyor; Olcum iki üyeyle bir düzeyde, Tarife bir üyeyle bir düzeyde. Üye sayısı
küçüldüğü için değil, düzey sayısı bire indiği için küçüldüler. Dört yöntemi de ölçü
düzeyinde olan bir sınıf, üç yöntemi üç düzeye yayılan bir sınıftan küçüktür.
Özet
- Bölmenin ölçütü satır sayısı değil, bir gövdenin kaç soyutlama düzeyinin sözcüğünü taşıdığıdır; ücretlendirme kitaplığının üç düzeyi ölçü, ücret politikası ve sunumdur.
- Ölçüm parametreleri ve yerel değişkenleri saymaz; sayılan şey gövdenin kendi dışından uzandığı adlardır.
- İki satırlık
kisaOzetüç düzey taşıyor, üç satırlıkucretSatiriUrethiçbir düzeye dokunmuyor: kısalık ile düzey sayısı bağımsız ölçütlerdir. - Bölünmüş sürümde altı fonksiyonun beşi tek düzeyde, biri yalnız çağrıdan oluşuyor; iki sürüm aynı satırı üretiyor.
- Düzey sayısı bire indiğinde bölme durur; aynı düzeydeki iki adımın sırası bağlayıcıysa ayırma kararı sıranın nerede korunacağına bakar.
- Sınıfın küçüklüğü yöntem sayısıyla değil üyelerinin toplam düzey sayısıyla ölçülür: üç düzeye yayılan dört üyeli sınıf, tek düzeyde kalan iki sınıfa bölündü.
Sonraki Adım
Bölünmüş sürümde her fonksiyon tek düzeyde konuşuyor, ama dosyanın kendisi hakkında henüz
bir şey söylenmedi: satırların nerede biteceği, hangi bildirimlerin yan yana duracağı,
girintinin ne kadar olacağı. Bunlar üslup tercihi sayılır ve tartışması genellikle zevke
bağlanır. Oysa biçim kararının ölçülebilir bir sonucu vardır: aynı içerik değişikliği,
farklı biçimlerde farklı sayıda fark satırı üretir. Sonraki ders tarife tablosunu iki ayrı
biçimde yazıp aynı değişikliği ikisine de uygular ve diff çıktısındaki satır sayısını
karşılaştırır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.