---
title: 'Temiz Kod'
source: 'https://academia.sh/tr/kurslar/temiz-kod'
language: tr
updated: '2026-08-17T04:42:34+00:00'
license: 'CC BY-SA 4.0'
lessons: 16
outcomes:
    - 'Adlandırma ve yapı kararlarını okunabilirlik ölçütüyle verir'
    - 'Bir fonksiyonun tek sorumluluk taşıyıp taşımadığını değerlendirir'
    - 'Yeniden düzenlemeyi küçük ve güvenli adımlarla uygular'
---

# Temiz Kod

Çalışan kod ile iyi tasarlanmış kod arasındaki farkın ölçülmesi: niyeti açıklayan adlandırma, tek soyutlama düzeyi, çevrimsel karmaşıklık, boş değer ve bayrak parametresi gibi arayüz kararları, kodun değişim nedenine göre düzenlenmesi ve yinelemenin küçük adımlarla giderilmesi.

## Adlandırma ve Yapı

Niyeti açıklayan adların çağrı yerinde ölçülmesi, yorumun koda çevrilmesi, tek soyutlama düzeyi kuralı, kod biçiminin fark okumasına maliyeti, çevrimsel karmaşıklığın hesaplanması ve koşul zinciri, arama tablosu ile çok biçimlilik arasında yapı seçimi.

### Anlamlı Adlar

Source: https://academia.sh/tr/kurslar/temiz-kod/anlamli-adlar

Adlandırmanın ölçülebilir bir tasarım kararı olarak ele alınması: aynı gönderi ücreti hesabının kısaltılmış ve niyeti açıklayan iki sürümünde çağrı yerindeki opak argümanların sayılması, kısaltmaların kaç aday karşılığa çözümlendiğinin çıkarılması ve ad uzunluğunun kapsamla ilişkisi.

Bir dil müfredatı tamamlandığında elde edilen yetenek şudur: istenen davranışı üreten kod
yazmak. JavaScript ve TypeScript kursları bunu sağladı; testler geçiyorsa program doğrudur.
Bu müfredat farklı bir soruyla başlıyor. Aynı davranışı üreten iki program arasında bir
tercih yapmak gerekirse, tercih neye dayanır?

Soru kaçınılmazdır, çünkü kod bir kez yazılıp bırakılmaz. Bir gönderi ücretlendirme
kitaplığına altı ay sonra yeni bir bölge katsayısı eklenir, bir yıl sonra hacimsel ağırlık
böleni değişir. Bu değişiklikleri yapan kişi, kodu önce **okur**. Okuma maliyeti tasarımın
asıl ölçüsüdür ve "daha okunabilir" gibi bir iddia, sayıya bağlanmadığı sürece tartışmayı
bitirmez. Bu ders adlandırmayı iki sayıya bağlıyor: çağrı yerinde ne söylediği anlaşılmayan
argüman sayısı ve bir kısaltmanın kaç farklı alan sözcüğüne çözümlenebildiği.

Kurs boyunca tek bir kitaplık geliştirilecek: gönderi ücretlendirme ve yönlendirme. İlk
işlevi, bir gönderinin ağırlığından, hacminden ve bölgesinden ücretini hesaplamak.

#### Ölçüt: Çağrı Yerinde Cevaplanabilen Soru

Bir adın taşıdığı bilgi, o adın **kullanıldığı yerde** ölçülür. Bir çağrı satırına bakan
kişi şu soruları sorar: bu argüman hangi parametreyi dolduruyor, değeri ne anlama geliyor,
sonuç neyin ücreti? Cevap için başka bir dosyayı açmak gerekiyorsa ad bilgi taşımıyordur.

Bunu mekanik hâle getirmek için bir argümanı **etiketli** sayalım: çağrı metninin kendisi
hangi parametreye karşılık geldiğini söylüyorsa (adlandırılmış bir alan ya da en az üç
harfli bir değişken adı) etiketlidir. Değilse **opak**tır: çıplak bir sayı, bir doğruluk
değeri ya da tek harfli bir ad. Opak argüman sayısı, çağrı yerinde cevaplanamayan soru
sayısının alt sınırıdır.

#### Kısaltılmış Sürüm

İlk sürüm çalışıyor ve testleri geçiyor. Hacimsel ağırlığı hesaplar, ücretlendirilen
ağırlığı bulur, kademeyi seçer, bölge katsayısını uygular, indirimi düşer ve asgari ücretin
altına inmez.

```sh
mkdir -p surum-a surum-b
```

```js
// surum-a/veri.mjs — ornek gonderi ve tarife
export const g = { a: 2.4, e: 30, by: 24, y: 18 };
export const t = { k: [{ s: 1, u: 38 }, { s: 5, u: 26 }, { s: 10, u: 21 }, { s: Infinity, u: 17 }],
                   bk: { 1: 1, 2: 1.35, 3: 1.8 }, as: 45 };
```

```js
// surum-a/ucret.mjs — gonderi ucreti, kisaltilmis adlarla
export function hsp(g, t, b, x) {
  const d = (g.e * g.by * g.y) / 3000;
  const a = Math.max(g.a, d);
  const k = t.k.find((i) => a <= i.s);
  const u = k.u * a * t.bk[b];
  return Math.max(u * (1 - x), t.as);
}
```

```js
// surum-a/cagri.mjs — cagri yeri
import { hsp } from "./ucret.mjs";
import { g, t } from "./veri.mjs";
console.log(hsp(g, t, 2, 0.12).toFixed(2));
```

Çağrı satırındaki dört argümandan hiçbiri kendini açıklamıyor. `2` bir bölge kodu mu, bir
kademe indisi mi, bir adet mi? `0.12` bir indirim oranı mı, bir vergi oranı mı? Cevap
`ucret.mjs` dosyasında; okuyan kişi ikinci dosyayı açmak zorunda.

#### Adlandırılmış Sürüm

İkinci sürüm aynı aritmetiği yapar. Değişen tek şey adlar, parametre biçimi ve sayıların
nereden geldiği.

```js
// surum-b/veri.mjs — ayni veri, alan adlariyla
export const BOLGE = { AYNI_SEHIR: 1, KOMSU_BOLGE: 2, UZAK_BOLGE: 3 };
export const SOZLESMELI_MUSTERI_INDIRIMI = 0.12;
export const ornekGonderi = { agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18 };
export const yurticiTarife = {
  agirlikKademeleri: [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 },
                      { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: Infinity, kiloBasiUcret: 17 }],
  bolgeKatsayilari: { 1: 1, 2: 1.35, 3: 1.8 },
  asgariUcret: 45,
};
```

```js
// surum-b/ucret.mjs — ayni hesap, niyeti aciklayan adlarla
const HACIMSEL_BOLEN = 3000;

export function gonderiUcretiHesapla({ gonderi, tarife, bolgeKodu, indirimOrani }) {
  const hacimselAgirlikKg = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
  const ucretlendirilenAgirlikKg = Math.max(gonderi.agirlikKg, hacimselAgirlikKg);
  const kademe = tarife.agirlikKademeleri.find((k) => ucretlendirilenAgirlikKg <= k.ustSinirKg);
  const bolgeliUcret = kademe.kiloBasiUcret * ucretlendirilenAgirlikKg * tarife.bolgeKatsayilari[bolgeKodu];
  return Math.max(bolgeliUcret * (1 - indirimOrani), tarife.asgariUcret);
}
```

```js
// surum-b/cagri.mjs — cagri yeri
import { gonderiUcretiHesapla } from "./ucret.mjs";
import { ornekGonderi, yurticiTarife, BOLGE, SOZLESMELI_MUSTERI_INDIRIMI } from "./veri.mjs";
console.log(gonderiUcretiHesapla({ gonderi: ornekGonderi, tarife: yurticiTarife,
  bolgeKodu: BOLGE.KOMSU_BOLGE, indirimOrani: SOZLESMELI_MUSTERI_INDIRIMI }).toFixed(2));
```

İki sürümün aynı sonucu ürettiği doğrulanır.

```sh
node surum-a/cagri.mjs
node surum-b/cagri.mjs
```

```
133.44
133.44
```

Davranış aynı. Fark yalnızca okuma maliyetinde ve bu maliyet sayılabilir.

#### Ölçüm

Aşağıdaki araç iki şeyi sayar. Birincisi, verilen çağrı yerindeki opak argümanları.
İkincisi, gerçekleştirimdeki iki harfe kadar kısa adları alıp her birinin alan sözlüğündeki
kaç sözcüğe çözümlenebileceğini — yani kısaltmanın belirsizliğini.

```js
// ad-olcer.mjs — cagri yerindeki opak argumanlari ve kisaltmalarin belirsizligini sayar
import { readFileSync } from "node:fs";

// Kitapligin alan sozlugu: adlarin tasimasi beklenen sozcukler.
const SOZLUK = ["agirlik", "asgari", "bolen", "bolge", "desi", "gonderi", "hacim",
  "indirim", "kademe", "katsayi", "kilo", "oran", "sinir", "tarife", "ucret"];

// Bir arguman, cagri yerinde hangi parametreyi doldurdugunu kendisi soyluyorsa etiketlidir.
const etiketli = (arg) => /^[A-Za-z_$][\w$]{2,}\s*:/.test(arg) || /^[A-Za-z_$][\w$]{2,}$/.test(arg);

function opakSay(dosya, fnAdi) {
  const govde = readFileSync(dosya, "utf8");
  const cagri = govde.match(new RegExp(`\\b${fnAdi}\\(([^()]*)\\)`));
  const args = cagri[1].replace(/[{}]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
  return { toplam: args.length, opak: args.filter((a) => !etiketli(a)).length };
}

function kisaltmalar(dosya) {
  const govde = readFileSync(dosya, "utf8").replace(/^\/\/.*$/gm, "");
  const adlar = [...new Set(govde.match(/[A-Za-z_$][\w$]*/g))].filter((a) => a.length <= 2);
  return adlar.map((ad) => ({
    ad,
    adaylar: SOZLUK.filter((s) => s.startsWith(ad[0].toLowerCase())),
  }));
}

for (const [dosya, fn] of [["surum-a/cagri.mjs", "hsp"], ["surum-b/cagri.mjs", "gonderiUcretiHesapla"]]) {
  const s = opakSay(dosya, fn);
  console.log(`${dosya.padEnd(20)} ${fn.padEnd(21)} arguman=${s.toplam}  opak=${s.opak}`);
}

console.log("\nkisaltma  aday sayisi  adaylar");
const kisa = kisaltmalar("surum-a/ucret.mjs");
for (const k of kisa) console.log(`${k.ad.padEnd(9)} ${String(k.adaylar.length).padEnd(12)} ${k.adaylar.join(", ") || "-"}`);
const tek = kisa.filter((k) => k.adaylar.length === 1).length;
console.log(`\nsurum-a kisa ad sayisi = ${kisa.length}, tek adayla cozumlenen = ${tek}`);
console.log(`surum-b kisa ad sayisi = ${kisaltmalar("surum-b/ucret.mjs").length}`);
```

```sh
node ad-olcer.mjs
```

```
surum-a/cagri.mjs    hsp                   arguman=4  opak=4
surum-b/cagri.mjs    gonderiUcretiHesapla  arguman=4  opak=0

kisaltma  aday sayisi  adaylar
g         1            gonderi
t         1            tarife
b         2            bolen, bolge
x         0            -
d         1            desi
e         0            -
by        2            bolen, bolge
y         0            -
a         2            agirlik, asgari
k         3            kademe, katsayi, kilo
i         1            indirim
s         1            sinir
u         1            ucret
bk        2            bolen, bolge
as        2            agirlik, asgari

surum-a kisa ad sayisi = 15, tek adayla cozumlenen = 6
surum-b kisa ad sayisi = 1
```

Dört opak argüman sıfıra indi. Kısaltma tablosu ikinci sonucu veriyor: on beş kısa addan yalnız
altısı tek bir alan sözcüğüne çözümleniyor. Üçü (`x`, `e`, `y`) hiçbir sözcüğe karşılık
gelmiyor; okuyan kişi bunları ancak kodu izleyerek çözebilir. Altısı birden çok aday taşıyor:
`a` hem ağırlık hem asgari, `k` hem kademe hem katsayı hem kilo olabilir. Gerçekleştirimde `a`
ağırlığı, `as` asgari ücreti tutuyor — iki farklı kavram bir harf farkıyla ayrılmış.

Adlandırılmış sürümde tek bir kısa ad kaldı: `find` çağrısına verilen `k` parametresi. Bu
kalıntı rastlantı değil; sonraki bölümün konusu.

#### Ad Uzunluğu Kapsama Bağlıdır

Kısa ad kendiliğinden kusur değildir. Ölçüt, adın **yaşadığı kapsamdır**. Bir satırlık bir
geri çağırma içindeki `k` ile modül düzeyinde dışa aktarılan `t` arasındaki fark, okuyanın
tanımı bulmak için gezmesi gereken mesafedir. Birincisinde tanım aynı satırda; ikincisinde
tanım başka bir dosyada.

Buradan pratik bir kural çıkar: adın uzunluğu, adın görünür olduğu kod miktarıyla birlikte
artar. Döngü sayacı `i` üç satırlık bir döngüde savunulabilir, dışa aktarılan bir sabit
olarak savunulamaz. Aynı gerekçe tersine de işler: dar kapsamda gereksiz uzun ad
(`gecerliGonderininHacimselAgirligiDegeri`) bilgi eklemeden satırı uzatır.

#### Adın Vermesi Gereken Bilgi

Ölçüm ne kadarının eksik olduğunu söyler, hangi bilginin eklenmesi gerektiğini söylemez.
Bunun için dört ölçüt işe yarar.

**Birim ve tür adın içinde durur.** `agirlikKg` ile `agirlik` arasındaki fark, gramla
kilogramı karıştıran bir hatanın derleme öncesinde görünmesidir. `enCm`, `ustSinirKg`,
`indirimOrani` adlarının sonundaki ekler bu işi yapar; `indirimOrani` ile `indirimTutari`
birbirinin yerine geçemez.

**Ad yanlış bilgi vermemelidir.** `bolgeListesi` adlı bir değişkenin gerçekte bir eşleme
tutması, hiç ad vermemekten kötüdür: okuyan kişi yanlış bir varsayımla ilerler. Yanlış
adın maliyeti, hata bulunana kadar yapılan bütün yorumlardır.

**Kavram başına tek sözcük.** Aynı kitaplıkta `getTarife`, `tarifeAl` ve `tarifeGetir`
birlikte bulunuyorsa okuyan kişi üç farklı işlem olduğunu varsayar. Sözlük kurs boyunca
sabittir: bir gönderinin ücretlendirilen ağırlığı her yerde `ucretlendirilenAgirlikKg`
adıyla geçer.

**Aranabilirlik.** `3000` sayısı kod tabanında yüzlerce yerde geçer; `HACIMSEL_BOLEN` bir
kez geçer. Sayının değişmesi gerektiğinde aranacak şey addır. Çıplak sayıların isimsiz
bırakılması bu yüzden yalnız okuma değil, değiştirme maliyetidir.

#### Özet

- Adlandırma bir üslup tercihi değil, ölçülebilir bir karardır: ölçüt, çağrı yerinde
  cevaplanamayan soruların alt sınırı olan opak argüman sayısıdır.
- Kısaltılmış sürümde dört argümanın dördü opaktı; adlandırılmış sürümde hiçbiri opak
  değil. İki sürüm aynı sonucu (133.44) üretiyor, fark yalnızca okuma maliyetinde.
- Kısaltma çözümleme tablosu ikinci ölçüyü verdi: on beş kısa addan altısı tek bir alan
  sözcüğüne çözümleniyor, üçü hiçbirine, altısı birden çoğuna.
- Ad uzunluğu adın yaşadığı kapsamla birlikte artar; bir satırlık geri çağırmadaki kısa ad
  ile dışa aktarılan kısa ad aynı şey değildir.
- İyi ad birimi ve türü taşır, yanlış bilgi vermez, kavram başına tek sözcük kullanır ve
  aranabilir; çıplak sayıya ad vermek okuma değil değiştirme maliyetini düşürür.

#### Sonraki Adım

Adlandırılmış sürümde bir şey dikkat çekiyor: `HACIMSEL_BOLEN` sabiti bir yorum satırına
ihtiyaç duymadı, çünkü adı ne olduğunu söylüyor. Kısaltılmış sürümde aynı bilgi ancak bir
yorumla verilebilirdi. Bu gözlem genelleşir mi? Bir yorumun varlığı, kodun söyleyemediği
bir şeyi söylemek zorunda kalmasının işareti midir? Sonraki ders bu soruyu ölçerek yanıtlar:
yorumların ne kadarı ada çevrilebilir, çevrilemeyenler hangileridir ve yorumla kodun
birbirinden ayrı düşmesi hangi noktada sessiz bir yanlışa dönüşür.

### Yorum Yerine Ad

Source: https://academia.sh/tr/kurslar/temiz-kod/yorum-yerine-ad

Yorumun derlenmeyen ve sınanmayan bir metin olmasından doğan uyumsuzluğun ölçülmesi: gönderi ücreti modülünde yorumdaki sayı iddialarının koddaki karşılığının denetlenmesi, iddiaları ada çeviren yeniden düzenleme ve ada çevrilemeyen yorum türleri.

Önceki derste adlandırılmış sürüm bir gözlem bıraktı: `HACIMSEL_BOLEN` sabiti hiçbir
açıklama satırına ihtiyaç duymadı, çünkü adı ne olduğunu söylüyordu. Aynı bilgi kısaltılmış
sürümde ancak bir yorumla verilebilirdi. Bu gözlem genelleşiyor mu? Bir yorumun varlığı,
kodun söyleyemediği bir şeyi söylemek zorunda kalmasının işareti midir?

Soruyu sayıya bağlamak için yorumun teknik özelliğinden başlamak gerekir. Yorum derlenmez,
çalıştırılmaz, sınanmaz. Kod her değişiklikte davranışıyla kendini savunur; yorum hiçbir
şey yapmadan yerinde durur. Bu asimetri zamanla bir ayrışma üretir: kod bir yöne, yorum
başka bir yöne gider ve okuyan kişi yorumu okur.

#### Altı Ay Sonraki Dosya

Ücretlendirme kitaplığının ücret modülü bir süre yaşadı. Bu arada tarife değişti: dördüncü
bir ağırlık kademesi eklendi, asgari ücret yükseldi, sözleşmeli müşteri indirimi yeniden
görüşüldü. Değişiklikleri yapan kişi kodu güncelledi.

```sh
mkdir -p yorumlu adli
```

```js
// yorumlu/ucret.mjs
// Ucret hesabi.
// Hacimsel bolen 3000'dir.
// Uc agirlik kademesi vardir: 1, 5, 10 kg.
// Asgari ucret 45'tir.
// Sozlesmeli musteri indirimi %12'dir.
const B = 3000;
const A = 52;
const S = 0.15;
const K = [1, 5, 10, 30];
const U = [38, 26, 21, 17];

export function hesapla(gonderi, bolgeKatsayisi) {
  // hacimsel agirlik
  const d = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / B;
  // buyuk olani ucretlendirilir
  const a = Math.max(gonderi.agirlikKg, d);
  // kademeyi bul
  let i = 0;
  while (i < K.length - 1 && a > K[i]) i += 1;
  // indirim asgariden once uygulanir
  return Math.max(U[i] * a * bolgeKatsayisi * (1 - S), A);
}
```

Yorumlar iki işi birden yapıyor. Bir kısmı kodun **ne yaptığını** tekrar ediyor
(`// hacimsel agirlik`, `// kademeyi bul`); bunlar adın taşıması gereken bilgiyi yorum
satırına taşımış. Bir kısmı da alan bilgisi iddia ediyor: kademe sayısı, asgari ücret,
indirim oranı. İkinci grup denetlenebilir.

#### Uyumsuzluğun Ölçülmesi

Aşağıdaki araç bir dosyanın yorumlarını ayırır, yorumlardaki sayıları çıkarır ve her birinin
kodun sayısal değişmezleri arasında bulunup bulunmadığına bakar. Kodda karşılığı olmayan bir
sayı, yorumun kodla ayrı düştüğünün kanıtıdır. Dosyanın ilk satırı dosya adını taşıyan
işaret olduğu için sayıma girmez.

```js
// yorum-denetle.mjs — yorum sayisini ve yorumdaki sayi iddialarinin koddaki karsiligini denetler
import { readFileSync } from "node:fs";

const sayilar = (metin) => metin.match(/\d+(?:\.\d+)?/g) ?? [];

function denetle(dosya) {
  // Ilk satir dosya adi isaretidir, yorum sayilmaz.
  const satirlar = readFileSync(dosya, "utf8").split("\n").slice(1);
  const yorumlar = satirlar.map((s) => s.match(/\/\/(.*)$/)?.[1]).filter((s) => s !== undefined);
  const kod = satirlar.map((s) => s.replace(/\/\/.*$/, "")).join("\n");
  const koddakiler = new Set(sayilar(kod));
  const iddialar = yorumlar.flatMap(sayilar);
  return { yorum: yorumlar.length, iddia: iddialar.length,
           uyumsuz: iddialar.filter((s) => !koddakiler.has(s)) };
}

for (const dosya of process.argv.slice(2)) {
  const r = denetle(dosya);
  console.log(`${dosya.padEnd(18)} yorum=${r.yorum}  sayi iddiasi=${r.iddia}  kodda yok=[${r.uyumsuz.join(", ")}]`);
}
```

```sh
node yorum-denetle.mjs yorumlu/ucret.mjs
```

```
yorumlu/ucret.mjs  yorum=9  sayi iddiasi=6  kodda yok=[45, 12]
```

Dokuz yorum satırı, altı sayı iddiası, iki tanesi kodda yok. Yorum "asgari ücret 45" diyor,
kod 52 uyguluyor. Yorum "indirim %12" diyor, kod 0.15 uyguluyor. Kademe sayısını sayıyla
yazmadığı için "üç kademe" iddiası bu araçla yakalanmıyor; kod dört kademe taşıyor.
Denetimin sınırı da böylece görünüyor: araç yalnız sayısal iddiaları karşılaştırır, düz
sözcükle yazılmış iddiaları karşılaştıramaz.

#### Sapmanın Bedeli

İki uyumsuz sayı soyut bir kusur değil. Yorumu okuyan bir kişi, bir gönderinin ücretini
elle doğrulamak istediğinde yorumun verdiği değerleri kullanır.

```js
// sapmayi-goster.mjs — yorumun anlattigi hesap ile kodun yaptigi hesap
import { hesapla as yorumlu } from "./yorumlu/ucret.mjs";

const gonderi = { agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18 };
const BOLGE_KATSAYISI = 1.35;

// Yorumun soyledigi degerlerle elle yapilan hesap: asgari 45, indirim %12.
const yorumaGore = Math.max(26 * 4.32 * BOLGE_KATSAYISI * (1 - 0.12), 45);

console.log(`yoruma gore = ${yorumaGore.toFixed(2)}`);
console.log(`kodun sonucu = ${yorumlu(gonderi, BOLGE_KATSAYISI).toFixed(2)}`);
```

```sh
node sapmayi-goster.mjs
```

```
yoruma gore = 133.44
kodun sonucu = 128.89
```

Aradaki 4,55'lik fark bir hata mesajı üretmez. Program doğru çalışıyor, testler geçiyor;
yanlış olan tek şey belgedir ve belge kodun içinde durduğu için doğru sanılır. Yanlış
yorumun maliyeti, yorumsuz kodun maliyetinden büyüktür.

#### Yorumu Ada Çevirmek

Yeniden düzenlemenin adımları mekaniktir. Kodun ne yaptığını anlatan her yorum, o işi yapan
parçanın adına dönüşür: `// hacimsel agirlik` yorumu `hacimselAgirlikKg` adlı bir fonksiyon
olur. Alan bilgisi iddia eden her yorum, o bilgiyi tutan sabitin adına dönüşür: `// asgari
ucret` yorumu `ASGARI_UCRET` sabiti olur. Böylece iddia ile değer aynı satırda durur ve
ayrı düşmeleri olanaksız hâle gelir.

```js
// adli/ucret.mjs — yorumlarin ada cevrilmis hali
const HACIMSEL_BOLEN = 3000; // tasiyici sozlesmesinden gelir, burada karar verilmez
const ASGARI_UCRET = 52;
const SOZLESMELI_MUSTERI_INDIRIMI = 0.15;
const AGIRLIK_KADEMELERI = [
  { ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 },
  { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 },
];

const hacimselAgirlikKg = (g) => (g.enCm * g.boyCm * g.yukseklikCm) / HACIMSEL_BOLEN;
const ucretlendirilenAgirlikKg = (g) => Math.max(g.agirlikKg, hacimselAgirlikKg(g));
const kademeSec = (agirlikKg) =>
  AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg) ?? AGIRLIK_KADEMELERI.at(-1);
const indirimiAsgariUcretOncesindeUygula = (ucret) =>
  Math.max(ucret * (1 - SOZLESMELI_MUSTERI_INDIRIMI), ASGARI_UCRET);

export function hesapla(gonderi, bolgeKatsayisi) {
  const agirlikKg = ucretlendirilenAgirlikKg(gonderi);
  return indirimiAsgariUcretOncesindeUygula(
    kademeSec(agirlikKg).kiloBasiUcret * agirlikKg * bolgeKatsayisi);
}
```

Kademe sınırı ile o kademenin kilo başına ücreti artık aynı nesnenin içinde. Önceki sürümde
bunlar iki ayrı dizide, indis eşleşmesiyle tutuluyordu; bir kademe eklendiğinde iki diziyi
birden güncellemeyi unutmak sessiz bir yanlış üretiyordu. Ad değişikliği yapıyı da düzeltti.

```sh
node yorum-denetle.mjs yorumlu/ucret.mjs adli/ucret.mjs
```

```
yorumlu/ucret.mjs  yorum=9  sayi iddiasi=6  kodda yok=[45, 12]
adli/ucret.mjs     yorum=1  sayi iddiasi=0  kodda yok=[]
```

Dokuz yorum bire indi, altı sayı iddiası sıfıra. Sıfır iddia, uyumsuzluk olasılığının da
sıfır olması demektir: ada çevrilen bir iddia kodla birlikte değişir.

#### Kalan Yorum

Bir yorum kaldı ve kalması gerekiyor:

```
const HACIMSEL_BOLEN = 3000; // tasiyici sozlesmesinden gelir, burada karar verilmez
```

Bu yorum kodun ne yaptığını değil, **neden öyle olduğunu** söylüyor. Sayının nereden
geldiğini hiçbir ad taşıyamaz, çünkü bilgi kodun dışındadır. Yorumu silen kişi 3000'i
iyileştirilebilir bir seçim sanır ve değiştirmeye kalkışır.

Ada çevrilemeyen yorum türleri sınırlıdır ve hepsi kodun dışına işaret eder:

- **Gerekçe:** bir değerin ya da kararın kaynağı, kod tabanının dışındaki bir sözleşme,
  standart ya da kurum kararıdır.
- **Uyarı:** bir işlemin ilk bakışta görünmeyen bir sonucu vardır; bir çağrının sırası
  önemlidir ya da bir sınırın aşılması pahalıdır.
- **Bilinçli sapma:** kod bir yerde beklenenden farklı davranıyordur ve bu bilerek
  yapılmıştır; yorum sapmayı ve nedenini kaydeder.
- **Yasal ve lisans metni:** dosyanın başında bulunması zorunlu olan metinler.

Bu dört türün dışında kalan her yorum bir adaydır: ya bir ada, ya bir fonksiyona, ya da bir
teste çevrilebilir. "Bu fonksiyon negatif ağırlık kabul etmez" yorumu bir teste ya da bir
koruma cümlesine çevrildiğinde iddia denetlenir hâle gelir; yorum olarak kaldığında
denetlenmez.

#### Özet

- Yorum derlenmez ve sınanmaz; kod değişirken yorumun yerinde kalması sessiz bir yanlış
  üretir. Uyumsuzluk, yorumdaki sayı iddialarının kodla karşılaştırılmasıyla ölçülür.
- Ücret modülünün yorumlu sürümünde dokuz yorum, altı sayı iddiası ve kodda karşılığı
  olmayan iki iddia vardı; yoruma göre yapılan hesap 133,44, kodun sonucu 128,89.
- Yorumu ada çeviren yeniden düzenlemeden sonra yorum sayısı bire, sayı iddiası sıfıra
  indi; ada çevrilen iddia kodla birlikte değiştiği için ayrı düşemez.
- Ad değişikliği yapıyı da düzeltti: indis eşleşmesiyle tutulan iki dizi tek bir kademe
  nesnesine dönüştü.
- Ada çevrilemeyen yorumlar kodun dışına işaret edenlerdir: gerekçe, uyarı, bilinçli sapma
  ve yasal metin. Kalan her yorum bir ada, bir fonksiyona ya da bir teste adaydır.

#### Sonraki Adım

Yorumları ada çevirirken tek bir fonksiyon dört parçaya bölündü: hacimsel ağırlık, ücretlendirilen
ağırlık, kademe seçimi ve indirim uygulaması. Bölme işini yorumların yerleri belirledi; bu
rastlantısal bir ölçüttür. Fonksiyon ne zaman bölünmelidir? Satır sayısı bir ölçüt müdür,
yoksa on satırlık bir fonksiyon da bölünmesi gereken bir şey içerebilir mi? Sonraki ders
ölçütü satır sayısından çıkarıp **tek soyutlama düzeyi** kuralına bağlar: bir fonksiyonun
çağırdığı adların düzeylerini sayan bir çözümleyici yazılır ve ücret hesabının iki sürümü
bu sayıyla karşılaştırılır.

### Küçük Fonksiyonlar ve Sınıflar

Source: https://academia.sh/tr/kurslar/temiz-kod/kucuk-fonksiyonlar-ve-siniflar

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ı.

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.

```sh
mkdir -p buyuk bolunmus sinif
```

```js
// 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.

```js
// 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.

```js
// 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.

```js
// 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

```sh
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.

```js
// 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.

```js
// 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}`;
}
```

```sh
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ık `ucretSatiriUret` hiç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.

### Girinti ve Kod Biçimi

Source: https://academia.sh/tr/kurslar/temiz-kod/girinti-ve-kod-bicimi

Kod biçiminin üslup tercihi olmaktan çıkarılıp fark maliyetiyle ölçülmesi: aynı içeriğin iki girinti düzeninde ürettiği fark satırları, tarife tablosunun hizalanmış ve düz biçimlerine uygulanan aynı revizyonun fark satırı sayıları ve içerik taşımayan fark satırlarının ayıklanması.

Önceki derste her fonksiyon tek soyutlama düzeyine indirildi, ama dosyanın kendisi
hakkında bir şey söylenmedi: satırların nerede biteceği, hangi bildirimlerin yan yana
duracağı, girintinin ne kadar olacağı. Bu kararlar genellikle üslup sayılır ve tartışması
zevke bağlanır.

Biçimin ölçülebilir bir sonucu vardır. Kod tabanına yapılan her değişiklik bir **fark**
üretir; farkı bir başkası okur ve onaylar. Aynı içerik değişikliği, farklı biçimlerde
farklı sayıda fark satırı üretir ve bu satırların bir kısmı hiçbir bilgi taşımaz. Bu ders
biçimi iki sayıya bağlıyor: bir değişikliğin ürettiği fark satırı sayısı ve bunların
içinde içerik taşımayanların sayısı.

#### Girinti Yapının Kendisini Gösterir

Girinti, iç içe geçmiş blokların derinliğini göze taşıyan tek işarettir. Yapı ile girinti
ayrı düştüğünde okuyan kişi bloğun nerede bittiğini süslü parantezleri sayarak bulmak
zorunda kalır. Aşağıdaki iki dosya aynı sözcük dizisini taşır; yalnız girintileri farklıdır.

```sh
mkdir -p duzenli karisik duz hizali
```

```js
// duzenli/ucret.mjs — girinti yapisal derinlikle uyusuyor
const ASGARI_UCRET = 52;
const AGIRLIK_KADEMELERI = [
  { ustSinirKg: 5, kiloBasiUcret: 26 },
  { ustSinirKg: 30, kiloBasiUcret: 17 },
];

export function kademeUcreti(agirlikKg, bolgeKatsayisi) {
  const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
  if (kademe === undefined) {
    throw new RangeError("kademe disi agirlik");
  }
  return Math.max(kademe.kiloBasiUcret * agirlikKg * bolgeKatsayisi, ASGARI_UCRET);
}
```

```js
// karisik/ucret.mjs — ayni icerik, girinti yapiyla uyusmuyor
const ASGARI_UCRET = 52;
const AGIRLIK_KADEMELERI = [
    { ustSinirKg: 5, kiloBasiUcret: 26 },
  { ustSinirKg: 30, kiloBasiUcret: 17 },
];

export function kademeUcreti(agirlikKg, bolgeKatsayisi) {
    const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
      if (kademe === undefined) {
    throw new RangeError("kademe disi agirlik");
      }
  return Math.max(kademe.kiloBasiUcret * agirlikKg * bolgeKatsayisi, ASGARI_UCRET);
}
```

İkinci dosyada `throw` satırı, içinde bulunduğu koşuldan daha az girintili. Bir okuyucu
o satıra baktığında koşulun içinde mi dışında mı olduğunu girintiden anlayamaz. Aynı
belirsizlik `AGIRLIK_KADEMELERI` dizisinin ilk ögesinde de var: iki öge aynı düzeyde
olduğu hâlde farklı sütunlarda duruyor.

İki dosya arasındaki fark ölçülebilir. Dosyaların ilk satırı dosya adını taşıyan işaret
olduğu için karşılaştırmaya girmez.

```sh
diff <(tail -n +2 duzenli/ucret.mjs) <(tail -n +2 karisik/ucret.mjs)
```

```
3c3
<   { ustSinirKg: 5, kiloBasiUcret: 26 },
---
>     { ustSinirKg: 5, kiloBasiUcret: 26 },
8,9c8,9
<   const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
<   if (kademe === undefined) {
---
>     const kademe = AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
>       if (kademe === undefined) {
11c11
<   }
---
>       }
```

Sekiz fark satırı var ve hiçbiri bir davranış değişikliği anlatmıyor. Bu, biçim
uyuşmazlığının ilk maliyetidir: girintiyi düzelten kişi, hiçbir içerik değiştirmediği
hâlde sekiz satırlık bir fark üretir ve o farkı bir başkası okumak zorunda kalır.

#### Tarife Tablosunun İki Biçimi

İkinci ölçüm daha somut bir soruyu yanıtlar: aynı **içerik** değişikliği iki farklı
biçimde kaç satır fark üretir? Tarife modülü iki biçimde yazıldı. Birincisinde eşittir
işaretleri hizalanmış ve kademe tablosu satır başına iki öge alacak şekilde
sıkıştırılmış; ikincisinde bildirimler tek boşluklu ve her kademe kendi satırında.

```js
// hizali/tarife.mjs — hizalanmis bildirimler, sikistirilmis kademe tablosu
export const HACIMSEL_BOLEN              = 3000;
export const ASGARI_UCRET                = 52;
export const SOZLESMELI_MUSTERI_INDIRIMI = 0.15;
export const PARA_BIRIMI                 = "TL";

export const AGIRLIK_KADEMELERI = [
  { ustSinirKg:  1, kiloBasiUcret: 38 }, { ustSinirKg:  5, kiloBasiUcret: 26 },
  { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 },
];
```

```js
// duz/tarife.mjs — tek bosluklu bildirimler, satir basina bir kademe
export const HACIMSEL_BOLEN = 3000;
export const ASGARI_UCRET = 52;
export const SOZLESMELI_MUSTERI_INDIRIMI = 0.15;
export const PARA_BIRIMI = "TL";

export const AGIRLIK_KADEMELERI = [
  { ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 },
  { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 },
];
```

Taşıyıcı tarifeyi gözden geçirdi ve üç şey değişti: beş kilogram kademesinin kilo başı
ücreti 26'dan 25'e indi, elli kilograma kadar yeni bir kademe açıldı,
`SOZLESMELI_MUSTERI_INDIRIMI` sabiti `SOZLESME_INDIRIMI` adına çevrildi. Aynı üç
değişiklik iki dosyaya da uygulanır.

```js
// hizali/tarife-yeni.mjs — ayni revizyon, hizalamasi korunmus hali
export const HACIMSEL_BOLEN    = 3000;
export const ASGARI_UCRET      = 52;
export const SOZLESME_INDIRIMI = 0.15;
export const PARA_BIRIMI       = "TL";

export const AGIRLIK_KADEMELERI = [
  { ustSinirKg:  1, kiloBasiUcret: 38 }, { ustSinirKg:  5, kiloBasiUcret: 25 },
  { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 },
  { ustSinirKg: 50, kiloBasiUcret: 14 },
];
```

```js
// duz/tarife-yeni.mjs — tarife revizyonu uygulanmis hali
export const HACIMSEL_BOLEN = 3000;
export const ASGARI_UCRET = 52;
export const SOZLESME_INDIRIMI = 0.15;
export const PARA_BIRIMI = "TL";

export const AGIRLIK_KADEMELERI = [
  { ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 25 },
  { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 },
  { ustSinirKg: 50, kiloBasiUcret: 14 },
];
```

Hizalanmış sürümde ad kısaldığı için hizalama sütunu kaydı ve dört bildirimin dördü de
yeniden yazıldı. Düz sürümde yalnız adı değişen bildirim yeniden yazıldı.

#### Fark Ölçer

Aşağıdaki araç her dosya çifti için farkı alır ve fark satırlarını ikiye ayırır. Çıkan bir
satırın boşlukları sadeleştirilmiş hâli giren satırlardan birinde de bulunuyorsa o çift
**yalnız biçim** farkıdır; içerik taşımaz. Kalanlar içerik farkıdır.

```js
// fark-olc.mjs — iki dosya arasindaki fark satirlarinin kaci icerik tasiyor
import { execSync } from "node:child_process";

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

const CIFTLER = [
  ["duzenli/ucret.mjs", "karisik/ucret.mjs"],
  ["hizali/tarife.mjs", "hizali/tarife-yeni.mjs"],
  ["duz/tarife.mjs", "duz/tarife-yeni.mjs"],
];

for (const [eski, yeni] of CIFTLER) {
  const satirlar = fark(eski, yeni);
  const cikan = satirlar.filter((s) => s.startsWith("< ")).map(icerik);
  const giren = satirlar.filter((s) => s.startsWith("> ")).map(icerik);
  const esli = cikan.filter((s) => giren.includes(s)).length;
  console.log(`${eski.padEnd(18)} -> ${yeni.padEnd(22)} degisen=${cikan.length + giren.length}` +
    `  yalniz bicim=${esli * 2}  icerik=${cikan.length + giren.length - esli * 2}`);
}
```

```
duzenli/ucret.mjs  -> karisik/ucret.mjs      degisen=8  yalniz bicim=8  icerik=0
hizali/tarife.mjs  -> hizali/tarife-yeni.mjs degisen=11  yalniz bicim=6  icerik=5
duz/tarife.mjs     -> duz/tarife-yeni.mjs    degisen=5  yalniz bicim=0  icerik=5
```

Üç satırın anlattığı şey şu. Girinti düzeltmesi sekiz satır fark üretiyor ve sekizi de
biçim; içerik farkı sıfır. Tarife revizyonunun içerik farkı iki biçimde de aynı: beş
satır. Hizalanmış biçimde bu beş satırın yanına altı satır daha ekleniyor. Aynı bilgi,
iki katından fazla fark satırıyla anlatılıyor.

#### Farkı Okuyan Kişi

Sayıların anlamı, farkı okuyan kişinin işidir. On bir satırlık farkta beş satır bir karar
taşır, altı satır taşımaz; okuyan kişi hangisinin hangisi olduğunu önceden bilmez ve
hepsini karşılaştırmak zorundadır. Beş satırlık farkta böyle bir ayıklama yoktur.

Sıkıştırılmış tablonun ikinci bir etkisi de var. Hizalanmış sürümün fark çıktısında
değişen tablo satırı iki kademe taşıyor: biri değişti, biri değişmedi. Okuyan kişi
satırın hangi yerinde değişiklik olduğunu karakter karakter aramak zorunda. Düz sürümde
değişen satır tek kademe taşıdığı için karşılaştırma satırın kendisiyle biter.

Buradan biçim kararının ölçütü çıkar: **bir satır tek bir bilgi taşımalı ve bir bilginin
biçimi komşusuna bağlı olmamalı.** Hizalama bu ikinci koşulu bozar; bir adın uzunluğu
komşu satırların boşluk sayısını belirlediği için, adı değişen bir bildirim komşularını
da değiştirir. Satır başına birden çok öge yerleştirmek birinci koşulu bozar.

#### Biçim Kararı Bir Kez Verilir

Ölçüm hangi biçimin seçileceğini tam olarak söylemez; girintinin iki mi dört boşluk
olduğu bu sayılara yansımaz. Söylediği şey, kararın **proje düzeyinde bir kez verilmesi**
ve dosya dosya yeniden tartışılmamasıdır. Aynı projede iki girinti alışkanlığı bir arada
yaşadığında her dosya, üzerinde en son çalışan kişinin alışkanlığına dönüşür ve her
dönüşüm içerik taşımayan fark satırı üretir.

Bu yüzden biçim kararı bir tartışma konusu değil, bir yapılandırma dosyası konusudur.
Biçimlendirici sınıfından bir araç kuralları kaynağa mekanik olarak uygular; karar bir
kez verilir, uygulama kişiye bırakılmaz. Aracın seçtiği biçim kimsenin tercihiyle birebir
uyuşmayabilir; ölçülen maliyet, tercih uyuşmazlığının maliyetinden büyüktür.

#### Özet

- Biçim kararının ölçüsü, ürettiği fark satırı sayısıdır: her değişiklik bir fark üretir
  ve farkı bir başkası okur.
- Yalnız girintisi bozulmuş bir dosya sekiz fark satırı üretti ve sekizinin de içerik
  farkı sıfırdı; biçim uyuşmazlığı içerik değişmeden maliyet doğurur.
- Aynı tarife revizyonu düz biçimde beş, hizalanmış biçimde on bir fark satırı üretti;
  içerik farkı ikisinde de beş satır, aradaki altı satır yalnız hizalamadan geliyor.
- Bir satır tek bilgi taşımalı ve bir bilginin biçimi komşu satırlara bağlı olmamalı;
  hizalama ikinci koşulu, satır başına çok öge birinci koşulu bozar.
- Biçim kararı proje düzeyinde bir kez verilir ve mekanik olarak uygulanır; dosya başına
  yeniden verilen karar her seferinde içerik taşımayan fark üretir.

#### Sonraki Adım

Biçim, kodun göze nasıl göründüğünü belirliyor; yapının kendisini değil. Düzgün
girintilenmiş bir fonksiyon da okunması güç olabilir: iç içe geçmiş koşullar, birden çok
çıkış noktası ve birleşik mantıksal ifadeler girintiyle düzelmez. Bu güçlüğün de bir
sayısı var. Sonraki ders bir fonksiyonun karar noktalarını sayan bir ölçer yazar,
ücretlendirme fonksiyonunun iki sürümünde çevrimsel karmaşıklığı ve bağımsız yol sayısını
karşılaştırır, ardından sayıyı düşüren dönüşümleri uygular.

### Çevrimsel Karmaşıklığı Azaltma

Source: https://academia.sh/tr/kurslar/temiz-kod/cevrimsel-karmasikligi-azaltma

Dallanma sayısının ölçülmesi ve düşürülmesi: karar noktalarını sayan bir ölçerin yazılması, ücretlendirme fonksiyonunun iç içe geçmiş ve sadeleştirilmiş sürümlerinde çevrimsel karmaşıklığın karşılaştırılması, koruma cümlesi ve arama tablosu dönüşümleriyle dallanmanın veriye taşınması.

Biçim kararları kodun göze nasıl göründüğünü belirliyor, yapısını değil. Düzgün
girintilenmiş bir fonksiyon da okunması güç olabilir: iç içe geçmiş koşullar, birden çok
çıkış noktası ve birleşik mantıksal ifadeler girintiyle düzelmez. Bu güçlüğün ölçüsü,
denetim akışının kaç yere ayrıldığıdır.

**Çevrimsel karmaşıklık** (cyclomatic complexity) bu sayıdır. Bir fonksiyonun denetim
akış grafiğinde birbirinden bağımsız kaç yol bulunduğunu verir ve tek bir kuralla
hesaplanır: karar noktası sayısı artı bir. Karar noktası, akışı ikiye ayıran her yapıdır —
`if`, `for`, `while`, `case`, `catch`, `&&`, `||`, `??` ve üçlü koşul. Sayı iki şeye
karşılık gelir: fonksiyonu okuyan kişinin aklında tutması gereken durum sayısı ve tüm
dalları geçen bir sınama kümesinin alt sınırı.

#### Kararların Tek Fonksiyonda Toplandığı Sürüm

Ücretlendirme fonksiyonu zamanla üç iş daha üstlendi: girdi doğrulama, müşteri türüne
göre indirim ve bölgeye göre katsayı. Hepsi aynı fonksiyonda, iç içe.

```sh
mkdir -p karmasik sade
```

```js
// karmasik/ucret.mjs — dogrulama, indirim ve bolge kararlari ic ice
const HACIMSEL_BOLEN = 3000;

export function ucretHesapla(gonderi, tarife, musteri) {
  if (gonderi.agirlikKg > 0) {
    if (gonderi.enCm > 0 && gonderi.boyCm > 0 && gonderi.yukseklikCm > 0) {
      const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
      const agirlik = gonderi.agirlikKg > hacimsel ? gonderi.agirlikKg : hacimsel;
      let ucret = 0;
      for (const kademe of tarife.agirlikKademeleri) {
        if (ucret === 0 && agirlik <= kademe.ustSinirKg) {
          ucret = kademe.kiloBasiUcret * agirlik;
        }
      }
      if (ucret === 0) {
        throw new RangeError("kademe disi agirlik");
      }
      if (musteri.tur === "sozlesmeli") {
        ucret = ucret * 0.85;
      } else if (musteri.tur === "kurumsal") {
        ucret = ucret * 0.92;
      }
      if (gonderi.bolgeKodu === 3 || gonderi.bolgeKodu === 4) {
        ucret = ucret * 1.8;
      } else if (gonderi.bolgeKodu === 2) {
        ucret = ucret * 1.35;
      }
      return ucret < tarife.asgariUcret ? tarife.asgariUcret : ucret;
    } else {
      throw new RangeError("olculer pozitif olmali");
    }
  } else {
    throw new RangeError("agirlik pozitif olmali");
  }
}
```

#### Sadeleştirilmiş Sürüm

İkinci sürüm üç dönüşüm uygular. **Koruma cümlesi:** doğrulama koşulları ters çevrilip
başa alınır, geçerli olmayan girdi erkenden atılır ve asıl hesap girinti düzeyinin
dibinden çıkar. **Arama tablosu:** indirim ve bölge kararları koşul zincirinden veri
yapısına taşınır. **Ayıklama:** kalan işler kendi fonksiyonlarına alınır.

```js
// sade/ucret.mjs — ayni davranis, kararlar tabloya ve koruma cumlelerine tasinmis
const HACIMSEL_BOLEN = 3000;
const INDIRIM_ORANLARI = { sozlesmeli: 0.15, kurumsal: 0.08 };
const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8, 4: 1.8 };

function olculeriDogrula(gonderi) {
  if (gonderi.agirlikKg <= 0) throw new RangeError("agirlik pozitif olmali");
  const boyutlar = [gonderi.enCm, gonderi.boyCm, gonderi.yukseklikCm];
  if (boyutlar.some((o) => o <= 0)) throw new RangeError("olculer pozitif olmali");
}

function ucretlendirilenAgirlikKg(gonderi) {
  const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
  return Math.max(gonderi.agirlikKg, hacimsel);
}

function kademeUcreti(agirlikKg, tarife) {
  const kademe = tarife.agirlikKademeleri.find((k) => agirlikKg <= k.ustSinirKg);
  if (kademe === undefined) throw new RangeError("kademe disi agirlik");
  return kademe.kiloBasiUcret * agirlikKg;
}

export function ucretHesapla(gonderi, tarife, musteri) {
  olculeriDogrula(gonderi);
  const agirlik = ucretlendirilenAgirlikKg(gonderi);
  const indirim = 1 - (INDIRIM_ORANLARI[musteri.tur] ?? 0);
  const katsayi = BOLGE_KATSAYILARI[gonderi.bolgeKodu] ?? 1;
  return Math.max(kademeUcreti(agirlik, tarife) * indirim * katsayi, tarife.asgariUcret);
}
```

Karşılaştırmanın anlamlı olması için iki sürümün aynı davranışı sürdürdüğü gösterilir.
Yedi durum sınanır: dört geçerli hesap ve üç hata.

```js
// karsilastir.mjs — iki surumun ayni girdilerde ayni sonucu verdigi dogrulanir
import { ucretHesapla as karmasik } from "./karmasik/ucret.mjs";
import { ucretHesapla as sade } from "./sade/ucret.mjs";

const tarife = {
  agirlikKademeleri: [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 },
    { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }],
  asgariUcret: 52,
};
const kutu = { enCm: 30, boyCm: 24, yukseklikCm: 18 };
const DURUMLAR = [
  ["sozlesmeli, bolge 2", { ...kutu, agirlikKg: 2.4, bolgeKodu: 2 }, "sozlesmeli"],
  ["kurumsal, bolge 3", { ...kutu, agirlikKg: 6, bolgeKodu: 3 }, "kurumsal"],
  ["bireysel, bolge 1", { enCm: 10, boyCm: 10, yukseklikCm: 10, agirlikKg: 0.4, bolgeKodu: 1 }, "bireysel"],
  ["kurumsal, bolge 4", { ...kutu, agirlikKg: 12, bolgeKodu: 4 }, "kurumsal"],
  ["agirlik sifir", { ...kutu, agirlikKg: 0, bolgeKodu: 1 }, "bireysel"],
  ["en sifir", { ...kutu, enCm: 0, agirlikKg: 2, bolgeKodu: 1 }, "bireysel"],
  ["kademe disi", { ...kutu, agirlikKg: 40, bolgeKodu: 1 }, "bireysel"],
];

const sonuc = (fn, gonderi, tur) => {
  try {
    return fn(gonderi, tarife, { tur }).toFixed(2);
  } catch (hata) {
    return `hata: ${hata.message}`;
  }
};

for (const [ad, gonderi, tur] of DURUMLAR) {
  const a = sonuc(karmasik, gonderi, tur);
  const b = sonuc(sade, gonderi, tur);
  console.log(`${ad.padEnd(20)} ${a === b ? "ayni  " : "FARKLI"} ${a}`);
}
```

```
sozlesmeli, bolge 2  ayni   128.89
kurumsal, bolge 3    ayni   208.66
bireysel, bolge 1    ayni   52.00
kurumsal, bolge 4    ayni   337.82
agirlik sifir        ayni   hata: agirlik pozitif olmali
en sifir             ayni   hata: olculer pozitif olmali
kademe disi          ayni   hata: kademe disi agirlik
```

#### Ölçer

Ölçer kaynağı fonksiyonlara ayırır, her gövdede karar noktası örüntülerini sayar ve
karmaşıklığı karar sayısının bir fazlası olarak verir. Modül satırı, fonksiyon başına
hesaplanan değerlerin toplamıdır.

```js
// karmasiklik-olc.mjs — her fonksiyonun karar noktalarini ve cevrimsel karmasikligini sayar
import { readFileSync } from "node:fs";

// Denetim akisini dallandiran her yapi bir karar noktasidir.
const KARAR = [
  ["if", /\bif\s*\(/g], ["for", /\bfor\s*\(/g], ["while", /\bwhile\s*\(/g],
  ["case", /\bcase\s+/g], ["catch", /\bcatch\s*\(/g],
  ["&&", /&&/g], ["||", /\|\|/g], ["??", /\?\?/g], ["?:", /\?(?!\?)[^:\n]*:/g],
];

function fonksiyonlar(kaynak) {
  const cikti = [];
  let acik = null, derinlik = 0;
  for (const satir of kaynak.split("\n")) {
    if (acik === null) {
      const m = satir.match(/^(?:export\s+)?function\s+(\w+)/);
      if (m === null) continue;
      [acik, derinlik] = [{ ad: m[1], govde: [] }, 0];
    }
    acik.govde.push(satir);
    derinlik += (satir.match(/{/g) ?? []).length - (satir.match(/}/g) ?? []).length;
    if (derinlik === 0) { cikti.push({ ad: acik.ad, govde: acik.govde.join("\n") }); acik = null; }
  }
  return cikti;
}

for (const dosya of process.argv.slice(2)) {
  console.log(dosya);
  let toplam = 0;
  for (const { ad, govde } of fonksiyonlar(readFileSync(dosya, "utf8"))) {
    const sayilar = KARAR.map(([etiket, oruntu]) => [etiket, (govde.match(oruntu) ?? []).length]);
    const karar = sayilar.reduce((t, [, n]) => t + n, 0);
    const dokum = sayilar.filter(([, n]) => n > 0).map(([e, n]) => `${e}:${n}`).join(" ");
    toplam += karar + 1;
    console.log(`  ${ad.padEnd(24)} karar=${String(karar).padEnd(3)}M=${String(karar + 1).padEnd(3)}${dokum || "-"}`);
  }
  console.log(`  ${"= modul".padEnd(24)} toplam M=${toplam}`);
}
```

```sh
node karmasiklik-olc.mjs karmasik/ucret.mjs sade/ucret.mjs
```

```
karmasik/ucret.mjs
  ucretHesapla             karar=15 M=16 if:8 for:1 &&:3 ||:1 ?::2
  = modul                  toplam M=16
sade/ucret.mjs
  olculeriDogrula          karar=2  M=3  if:2
  ucretlendirilenAgirlikKg karar=0  M=1  -
  kademeUcreti             karar=1  M=2  if:1
  ucretHesapla             karar=2  M=3  ??:2
  = modul                  toplam M=9
```

Tek fonksiyonda toplanan on beş karar noktası, on altı bağımsız yol demektir. Sadeleşmiş
sürümde en yüksek değer üçtür ve modül toplamı dokuza inmiştir. İki sayı ayrı şeyler
söylüyor: modül toplamı işin bütününde kaç dal bulunduğunu, fonksiyon başına düşen değer
ise bir kişinin tek seferde ne kadarını aklında tutmak zorunda olduğunu.

#### Dallar Aynı Fonksiyonda Çarpılır

Toplam neden dokuza inebiliyor? Bir kısmı dallanmanın veriye taşınmasından geliyor:
indirim zinciri iki koşuldan iki tablo satırına, bölge zinciri iki koşuldan dört tablo
satırına dönüştü. Kalan kazanç ise bölmenin kendisinden.

Aynı fonksiyonda duran dallar **çarpılır**. Karmaşık sürümde doğrulama geçtikten sonra
indirim üç sonuç veriyor (sözleşmeli, kurumsal, ikisi de değil), bölge üç sonuç veriyor
(uzak, komşu, aynı şehir), asgari ücret karşılaştırması iki sonuç veriyor. Bunlar
birbirinden bağımsız olduğu için fonksiyonun sonunu belirleyen birleşim sayısı
3 × 3 × 2 = 18'dir; hepsi tek bir gövdenin içinde bulunur.

Aynı üç karar ayrı fonksiyonlara dağıtıldığında birleşim sayısı **toplanır**: her fonksiyon
kendi dallarıyla ayrı ayrı sınanır, çünkü aralarındaki bağ yalnız bir değerin geçirilmesidir.
Tablo dönüşümü bu adımı daha da ileri götürür: `INDIRIM_ORANLARI` ve `BOLGE_KATSAYILARI`
birer veri satırı olduğu için hiç dal üretmezler. Yeni bir müşteri türü eklemek karmaşık
sürümde bir `else if` dalı, sadeleşmiş sürümde bir tablo satırı ekler; birincisi
karmaşıklığı bir arttırır, ikincisi arttırmaz.

#### Sayının Söylemediği

Çevrimsel karmaşıklık ölçüsünün sınırları var ve bunları bilmeden kullanmak yanlış
kararlar ürettirir.

**Sayı ile anlaşılırlık aynı şey değildir.** Uzun bir `switch` bloğu yüksek bir değer
üretir ama tek bakışta okunabilir; iki iç içe döngü içindeki tek koşul daha düşük bir
değer üretir ve daha zor izlenir. Ölçü, akışın kaça ayrıldığını sayar; ayrılan yolların
birbirine ne kadar benzediğini saymaz.

**Sayı bölerek düşürülür ama bölme bedavaya gelmez.** On beş karar noktasını on beş
fonksiyona dağıtan bir kod, ölçünün gözünde kusursuzdur ve okunması güçtür: okuyan kişi
bu kez çağrı zincirini izlemek zorunda kalır. Önceki dersin ölçütü burada devreye girer;
bölme yerleri soyutlama düzeyine göre seçilir, sayıyı düşürmek için seçilmez.

**Yüksek değer bir kusur değil, bir işarettir.** Ölçüm bir fonksiyonu işaretlediğinde sorulacak
soru "bu nasıl bölünür" değil, "bu kararların hepsi gerçekten burada mı verilmeli" sorusudur.
Karmaşık sürümdeki sekiz `if` bunun örneğiydi: üçü doğrulamaya, ikisi indirime, ikisi bölgeye
aitti; kalan biri döngü içindeki kademe seçimiydi — ücretin hesaplandığı yerin kendisi.

#### Özet

- Çevrimsel karmaşıklık, karar noktası sayısının bir fazlasıdır ve fonksiyonun denetim
  akışındaki bağımsız yol sayısını verir; karar noktaları `if`, `for`, `while`, `case`,
  `catch`, `&&`, `||`, `??` ve üçlü koşuldur.
- İç içe sürümde tek fonksiyon on beş karar noktası ve M = 16 taşıyordu; koruma cümlesi,
  arama tablosu ve ayıklama dönüşümlerinden sonra en yüksek fonksiyon değeri 3, modül
  toplamı 9 oldu.
- İki sürüm yedi durumda da aynı sonucu üretiyor; ölçüm davranış değişmeden yapıldı.
- Aynı gövdedeki bağımsız dallar çarpılır (3 × 3 × 2 = 18 birleşim), ayrı fonksiyonlara
  dağıtıldığında toplanır; tabloya taşınan karar hiç dal üretmez.
- Ölçü akışın kaça ayrıldığını sayar, yolların anlaşılırlığını saymaz; yüksek değer
  bölünmesi gereken bir yeri değil, sorgulanması gereken bir karar yığılmasını gösterir.

#### Sonraki Adım

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 mudur? Tablo, anahtara karşılık bir değer döndürür; ya karşılık bir değer
değil de bir davranışsa, ya her tarife türü ücreti başka bir yöntemle hesaplıyorsa? O
noktada üçüncü bir seçenek belirir: çok biçimlilik. Sonraki ders aynı davranışın koşul
zinciri, arama tablosu ve çok biçimlilikle üç sürümünü yazar ve seçimi ölçülebilir bir
soruya bağlar: yeni bir tarife türü eklendiğinde kaç dosyaya, kaç satıra dokunulur.

### Doğru Yapıyı Seçmek

Source: https://academia.sh/tr/kurslar/temiz-kod/dogru-yapiyi-secmek

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.

Ö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.

```sh
mkdir -p kosul tablo cokbicim
```

```js
// 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.

```js
// 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.

```js
// 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.

```js
// 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); }
}
```

```js
// 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.

```js
// 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.

```sh
cp -r kosul kosul-yeni && cp -r tablo tablo-yeni && cp -r cokbicim cokbicim-yeni
```

```js
// 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}`);
}
```

```js
// 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;
```

```js
// cokbicim-yeni/soguk-zincir.mjs — yeni tur, kendi dosyasinda
export class SogukZincir {
  etiket = "Soguk Zincir";
  gun = 2;
  hesap(temel) { return temel * 2.2 + 40; }
}
```

```js
// 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.

```js
// 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.

```sh
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.

```js
// 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.

## Arayüz ve Veri Kararları

Bir fonksiyonun dışarıya söylediklerinin tasarımı: boşluğun sözleşmede görünmesi, mantıksal bayrak parametresinin iki ayrı fonksiyona bölünmesi, en dar görünürlük ilkesi, kodun değişim nedenine göre aktörlere ayrılması ve aynı işin projede tek biçimde yapılması.

### Boş Değer Geçirmemek

Source: https://academia.sh/tr/kurslar/temiz-kod/bos-deger-gecirmemek

Boşluğun sözleşmede görünmesinin ölçülmesi: boş değer döndüren bir çağrı zincirinde hatanın kaç çağrı sonra ortaya çıktığı, boşluğu sonuç türünde gösteren sürümde aynı mesafenin sıfıra inmesi ve boş değeri argüman olarak geçiren imzaların boş toplamayla değiştirilmesi.

Buraya kadarki kararlar kodun iç yapısı üzerineydi: adlar, soyutlama düzeyleri, biçim,
dallanma ve seçenek yapısı. Bir fonksiyonun dışarıya **söylediği** şey henüz ele alınmadı.
Önceki dersteki `tarife` fonksiyonu bilinmeyen bir tür için hata atıyordu; aynı sorunun
yaygın çözümü boş bir değer döndürmektir.

Boş değer döndüren bir imza çağırana hiçbir şey söylemez. `tasiyiciBul(bolgeKodu)`
imzasına bakan kişi sonucun her zaman bir taşıyıcı olduğunu varsayar; boş dönebileceğini
ancak gerçekleştirimi okuyarak öğrenir. Varsayım yanlış çıktığında hata, boşluğun
üretildiği yerde değil kullanıldığı yerde ortaya çıkar. Aradaki mesafe ölçülebilir.

#### Boş Değerin Yolculuğu

Yönlendirme modülü bir gönderi için taşıyıcı seçer, aktarma sayısını belirler ve teslim
tarihini hesaplar. Her fonksiyon çağrıldığında adını bir ize yazıyor; bu iz ölçüm için
kullanılacak.

```sh
mkdir -p bos sozlesme
```

```js
// bos/rota.mjs — tasiyici bulunamazsa bos deger dondurulur
export const IZ = [];
const iz = (ad) => IZ.push(ad);

const TASIYICILAR = { 1: { ad: "sehirici", teslimGunu: 1 }, 2: { ad: "bolgesel", teslimGunu: 3 },
  3: { ad: "uzak", teslimGunu: 5 } };

export function tasiyiciBul(bolgeKodu) {
  iz("tasiyiciBul");
  return TASIYICILAR[bolgeKodu] ?? null;
}

export function rotaSec(gonderi) {
  iz("rotaSec");
  return { tasiyici: tasiyiciBul(gonderi.bolgeKodu), aktarmaSayisi: gonderi.bolgeKodu > 2 ? 1 : 0 };
}

export function aktarmaGecikmesi(rota) {
  iz("aktarmaGecikmesi");
  return rota.aktarmaSayisi * 2;
}

export function tasiyiciGecikmesi(rota) {
  iz("tasiyiciGecikmesi");
  return rota.tasiyici.teslimGunu;
}

export function teslimTarihi(rota, bugun) {
  iz("teslimTarihi");
  return bugun + aktarmaGecikmesi(rota) + tasiyiciGecikmesi(rota);
}

export function teslimatPlani(gonderi, bugun) {
  iz("teslimatPlani");
  const rota = rotaSec(gonderi);
  return { rota, tarih: teslimTarihi(rota, bugun) };
}

export function siparisiIsle(gonderi, bugun) {
  iz("siparisiIsle");
  return `${gonderi.kod}: ${teslimatPlani(gonderi, bugun).tarih}. gun`;
}
```

Tanımlı olmayan bir bölge kodu geldiğinde `tasiyiciBul` boş döner, `rotaSec` bu boşluğu
bir alan olarak paketler ve döndürür, `teslimatPlani` rotayı alır, `teslimTarihi` iki
gecikme fonksiyonunu çağırır ve hata bunlardan ikincisinde ortaya çıkar. Boşluk, üretildiği
yerden çıkıp bir veri yapısının içine yerleşerek yolculuk eder.

#### Sözleşmede Görünen Boşluk

İkinci sürümde tek bir şey değişiyor: taşıyıcı arayan fonksiyonun **sonuç türü**. Artık
ya bir taşıyıcı ya da bulunamama bilgisi dönüyor ve ikisi birbirinden ayırt edilebiliyor.
Çağıran taraf sonucu kullanmadan önce hangisi olduğuna bakmak zorunda.

```js
// sozlesme/rota.mjs — boslugun sonuc turunde gorunmesi
export const IZ = [];
const iz = (ad) => IZ.push(ad);

const TASIYICILAR = { 1: { ad: "sehirici", teslimGunu: 1 }, 2: { ad: "bolgesel", teslimGunu: 3 },
  3: { ad: "uzak", teslimGunu: 5 } };

// Sonuc iki bicimden biridir: { bulundu: true, tasiyici } ya da { bulundu: false, bolgeKodu }.
export function tasiyiciSec(bolgeKodu) {
  iz("tasiyiciSec");
  const tasiyici = TASIYICILAR[bolgeKodu];
  return tasiyici === undefined ? { bulundu: false, bolgeKodu } : { bulundu: true, tasiyici };
}

export function rotaSec(gonderi) {
  iz("rotaSec");
  const sonuc = tasiyiciSec(gonderi.bolgeKodu);
  if (sonuc.bulundu === false) {
    throw new RangeError(`bolge ${sonuc.bolgeKodu} icin tasiyici tanimli degil`);
  }
  return { tasiyici: sonuc.tasiyici, aktarmaSayisi: gonderi.bolgeKodu > 2 ? 1 : 0 };
}

export function aktarmaGecikmesi(rota) {
  iz("aktarmaGecikmesi");
  return rota.aktarmaSayisi * 2;
}

export function tasiyiciGecikmesi(rota) {
  iz("tasiyiciGecikmesi");
  return rota.tasiyici.teslimGunu;
}

export function teslimTarihi(rota, bugun) {
  iz("teslimTarihi");
  return bugun + aktarmaGecikmesi(rota) + tasiyiciGecikmesi(rota);
}

export function teslimatPlani(gonderi, bugun) {
  iz("teslimatPlani");
  const rota = rotaSec(gonderi);
  return { rota, tarih: teslimTarihi(rota, bugun) };
}

export function siparisiIsle(gonderi, bugun) {
  iz("siparisiIsle");
  return `${gonderi.kod}: ${teslimatPlani(gonderi, bugun).tarih}. gun`;
}
```

Aşağı akış — gecikme hesapları, teslim tarihi, plan ve özet — iki sürümde birebir aynı.
Değişen tek şey boşluğun nerede karşılandığıdır.

#### Ölçüm

Ölçer aynı geçersiz gönderiyi iki sürüme de verir ve üç şeyi kaydeder: çağrı izi, boşluğun
üretildiği çağrı ile hatanın görüldüğü çağrı arasındaki mesafe, hata anındaki yığıt izinde
(stack trace) modülün hangi fonksiyonlarının bulunduğu.

```js
// olc.mjs — bos degerin uretildigi cagri ile hatanin goruldugu cagri arasindaki mesafe
import { IZ as bosIz, siparisiIsle as bosIsle } from "./bos/rota.mjs";
import { IZ as sozIz, siparisiIsle as sozIsle } from "./sozlesme/rota.mjs";

const gonderi = { kod: "GN-4172", bolgeKodu: 9 };

function olc(baslik, isle, iz, ureten) {
  console.log(baslik);
  try {
    console.log(`  sonuc: ${isle(gonderi, 12)}`);
  } catch (hata) {
    const cerceve = hata.stack.split("\n").slice(1)
      .map((s) => s.match(/at (\w+)/)?.[1]).filter((a) => iz.includes(a));
    console.log(`  cagri izi: ${iz.join(" -> ")}`);
    console.log(`  boslugu ureten cagri #${iz.indexOf(ureten)}, hatanin goruldugu cagri #${iz.length - 1},` +
      ` aradaki cagri = ${iz.length - 1 - iz.indexOf(ureten)}`);
    console.log(`  yigit izindeki modul cerceveleri: ${cerceve.join(", ")}`);
    console.log(`  hata: ${hata.constructor.name}: ${hata.message}`);
  }
}

olc("bos deger donduren surum", bosIsle, bosIz, "tasiyiciBul");
olc("sozlesmede gorunen surum", sozIsle, sozIz, "tasiyiciSec");
```

```
bos deger donduren surum
  cagri izi: siparisiIsle -> teslimatPlani -> rotaSec -> tasiyiciBul -> teslimTarihi -> aktarmaGecikmesi -> tasiyiciGecikmesi
  boslugu ureten cagri #3, hatanin goruldugu cagri #6, aradaki cagri = 3
  yigit izindeki modul cerceveleri: tasiyiciGecikmesi, teslimTarihi, teslimatPlani, siparisiIsle
  hata: TypeError: Cannot read properties of null (reading 'teslimGunu')
sozlesmede gorunen surum
  cagri izi: siparisiIsle -> teslimatPlani -> rotaSec -> tasiyiciSec
  boslugu ureten cagri #3, hatanin goruldugu cagri #3, aradaki cagri = 0
  yigit izindeki modul cerceveleri: rotaSec, teslimatPlani, siparisiIsle
  hata: RangeError: bolge 9 icin tasiyici tanimli degil
```

Mesafe üçten sıfıra indi. Sayının anlamı, hatayı arayan kişinin kat etmesi gereken yoldur:
boş değer döndüren sürümde hata `tasiyiciGecikmesi` içinde görünüyor ve o fonksiyonda
hiçbir kusur yok. Yığıt izi de yardım etmiyor, çünkü boşluğu üreten `tasiyiciBul` çoktan
dönmüş; izde adı bile geçmiyor. Arayan kişi, listelenen dört çerçevenin hiçbirinde
bulunmayan bir nedeni bulmak zorunda.

Hata iletileri de ölçümün bir parçası. Birincisi bir tür hatasıdır ve alan hakkında hiçbir
şey söylemez; ikincisi bölge kodunu ve eksik olan şeyi adıyla söyler.

#### Boş Değeri Geçirmek

Aynı sorunun ters yönü, boş değerin **argüman olarak** geçirilmesidir. İndirim hesabı buna
örnek: indirim yoksa boş değer geçmek, imzada iki anlam üretir — "indirim nesnesi" ve
"indirim yok".

```js
// bos/indirim.mjs — indirim ya da kampanya yoksa bos deger geciriliyor
export function ucretHesapla(temelUcret, indirim, kampanya) {
  const oran = indirim === null ? 0 : indirim.oran;
  const ek = kampanya === null ? 0 : kampanya.ekIndirim;
  return temelUcret * (1 - oran - ek);
}

export const ORNEKLER = [
  ["indirimsiz", ucretHesapla(100, null, null)],
  ["sozlesmeli", ucretHesapla(100, { oran: 0.15 }, null)],
  ["iki indirim", ucretHesapla(100, { oran: 0.15 }, { ekIndirim: 0.05 })],
];
```

Boşluğun doğal karşılığı bir **boş toplama**dır. İndirim sayısı sıfır, bir ya da daha çok
olabiliyorsa parametre tekil bir nesne değil bir listedir; indirim yokluğu boş liste olarak
temsil edilir ve aynı işlemleri destekler.

```js
// sozlesme/indirim.mjs — boslugu bos dizi tasir, imza bos deger kabul etmez
export function ucretHesapla(temelUcret, indirimler) {
  const toplam = indirimler.reduce((t, i) => t + i.oran, 0);
  return temelUcret * (1 - toplam);
}

export const ORNEKLER = [
  ["indirimsiz", ucretHesapla(100, [])],
  ["sozlesmeli", ucretHesapla(100, [{ oran: 0.15 }])],
  ["iki indirim", ucretHesapla(100, [{ oran: 0.15 }, { oran: 0.05 }])],
];
```

```js
// indirim-olc.mjs — iki imzanin ayni sonuclari verdigi ve bos deger sayisi
import { ORNEKLER as bosOrnek } from "./bos/indirim.mjs";
import { ORNEKLER as sozOrnek } from "./sozlesme/indirim.mjs";

for (const [i, [ad, deger]] of bosOrnek.entries()) {
  const oteki = sozOrnek[i][1];
  console.log(`${ad.padEnd(12)} bos=${deger.toFixed(2)}  sozlesme=${oteki.toFixed(2)}` +
    `  ${deger.toFixed(2) === oteki.toFixed(2) ? "ayni" : "FARKLI"}`);
}
```

```
indirimsiz   bos=100.00  sozlesme=100.00  ayni
sozlesmeli   bos=85.00  sozlesme=85.00  ayni
iki indirim  bos=80.00  sozlesme=80.00  ayni
```

```sh
for d in bos sozlesme; do echo "$d/indirim.mjs: $(grep -o null $d/indirim.mjs | wc -l | tr -d ' ') bos deger"; done
```

```
bos/indirim.mjs: 5 bos deger
sozlesme/indirim.mjs: 0 bos deger
```

Aynı üç sonuç, beş boş değer yerine sıfır boş değerle üretiliyor. Kazanç yalnız sayıda
değil: boş toplamalı imza yeni bir indirim türü eklendiğinde değişmez, boş değerli imza
ise her yeni indirim için bir parametre ve bir denetim daha ister.

#### Boşluğu Görünür Kılmanın Yolları

Ölçüm hangi tekniğin seçileceğini söylemez; üç seçenek de mesafeyi sıfıra indirir ve
farkları başka yerdedir.

**Erken hata.** Boşluk bir programlama hatasının belirtisiyse, üretildiği yerde atılır.
`rotaSec` bunu yapıyor: tanımlı olmayan bölge kodu bir veri kusurudur, hesaba devam etmek
anlamsızdır. Bu seçenek çağıran tarafa karar bırakmaz.

**Boş toplama.** Boşluk "hiç yok" anlamındaysa ve işlem sıfır ögeyle de anlamlıysa boş
liste, boş eşleme ya da sıfır kullanılır. İndirim örneği budur; çağıran taraf hiçbir
denetim yazmaz, çünkü boşluk zaten desteklenen bir durumdur.

**Açık sonuç türü.** Boşluk beklenen bir sonuçsa ve ne yapılacağına çağıran taraf karar
vermeliyse, sonuç iki biçimden birini alır ve hangisi olduğu okunabilir. `tasiyiciSec`
bunu yapıyor: bir çağıran hata atmayı, başka bir çağıran varsayılan taşıyıcıya düşmeyi
seçebilir.

Üçünün ortak yanı, boşluğun **imzanın parçası** olmasıdır. Boş değer döndürmek ise
boşluğu imzadan gizler; gerçekleştirimi okumayan hiç kimse varlığını bilmez.

#### Özet

- Boş değer döndüren bir imza boşluğu gizler; çağıran taraf varsayımını gerçekleştirimi
  okumadan doğrulayamaz.
- Ölçümde boşluk üretildikten üç çağrı sonra hata verdi; hata `tasiyiciGecikmesi` içinde
  göründü ve boşluğu üreten fonksiyon yığıt izinde yer almıyordu.
- Boşluğu sonuç türünde gösteren sürümde mesafe sıfıra indi ve hata iletisi tür hatası
  yerine bölge kodunu adıyla anan bir alan hatasına dönüştü.
- Boş değer argüman olarak geçirildiğinde her parametre bir denetim ister; boş toplamalı
  imza aynı üç sonucu beş boş değer yerine sıfır boş değerle üretti.
- Boşluğu görünür kılmanın üç yolu erken hata, boş toplama ve açık sonuç türüdür; üçü de
  boşluğu imzanın parçası yapar.

#### Sonraki Adım

`ucretHesapla` imzası boş değerlerden kurtuldu, ama benzer bir sorun başka bir kılıkta
geri geliyor. Ücretlendirme kitaplığında iade gönderileri de hesaplanıyor ve iki hesap
büyük ölçüde ortak; aradaki farkı taşımak için imzaya bir doğruluk değeri eklemek akla
yakın görünür. Böyle bir parametre çağrı yerinde tek bir sözcük bile söylemez ve fonksiyonun
içinde bir dallanma açar. Sonraki ders bunun bedelini sınamayla ölçer: bayraklı tek
fonksiyonun ve bölünmüş iki fonksiyonun sınanması için kaç durum gerekiyor.

### Mantıksal Bayrak Parametreleri

Source: https://academia.sh/tr/kurslar/temiz-kod/mantiksal-bayrak-parametreleri

Doğruluk değeri taşıyan parametrelerin bedelinin sınamayla ölçülmesi: iki bayraklı tek fonksiyonun ve bölünmüş iki fonksiyonun dallarını kapsayan sınama sayılarının node --test ile karşılaştırılması, bayrağın gövdede açtığı dallanma sayısı ve çağrı yerindeki opak argümanlar.

Önceki derste imzadan boş değerler ayıklandı. Benzer bir sorun başka bir kılıkta geri
geliyor. Ücretlendirme kitaplığı iade gönderilerini de hesaplıyor ve iki hesap büyük
ölçüde ortak: aynı kademe seçimi, aynı ağırlık, farklı çarpan ve farklı asgari ücret.
Farkı taşımak için imzaya bir doğruluk değeri eklemek akla yakın görünür.

Bir mantıksal bayrak (boolean flag) parametresi, fonksiyonun **iki farklı iş** yaptığını
imzada ilan eder. Bedeli iki yerde ortaya çıkar: gövdede açtığı dallanmalarda ve çağrı
yerinde hiçbir şey söylemeyen argümanda. İkisi de sayılabilir.

#### İki Bayraklı Sürüm

Fonksiyon iki bayrak alıyor: gönderinin iade olup olmadığı ve sigortalı olup olmadığı.

```sh
mkdir -p bayrakli bolunmus
```

```js
// bayrakli/ucret.mjs — iade ve sigorta kararlari iki bayrakla tasiniyor
export function ucretHesapla(gonderi, tarife, iade, sigortali) {
  const kademe = tarife.agirlikKademeleri.find((k) => gonderi.agirlikKg <= k.ustSinirKg);
  const temel = kademe.kiloBasiUcret * gonderi.agirlikKg;
  const ucret = iade ? temel * 0.6 : temel;
  const sigorta = sigortali && iade === false ? gonderi.beyanDegeri * 0.01 : 0;
  const asgari = iade ? tarife.asgariIadeUcreti : tarife.asgariUcret;
  return Math.max(ucret + sigorta, asgari);
}
```

Bayrak imzada bir kez geçer, gövdede üç yerde daha karşımıza çıkar: ücret çarpanında,
sigortanın bastırılmasında ve asgari ücret seçiminde.

```sh
tail -n +2 bayrakli/ucret.mjs | grep -c iade
```

```
4
```

Sayı, bayrağın tek bir anahtar olmadığını gösteriyor. `iade` bir açma kapama düğmesi
değil, fonksiyonun gövdesini baştan sona ikiye bölen bir ayrımdır. İkinci bayrak birinciye
bağımlı: sigorta yalnız iade olmayan gönderilerde uygulanıyor, yani `iade === false`
koşulu sigortanın da içine giriyor.

#### Bölünmüş Sürüm

İkinci sürümde iki bayrak da kayboluyor. `iade` bayrağı iki ayrı fonksiyona bölünüyor;
`sigortali` bayrağı ise zaten veride bulunan bir bilgiyi tekrar ediyordu — beyan değeri
sıfır olan bir gönderinin sigorta ücreti de sıfırdır.

```js
// bolunmus/ucret.mjs — iki ayri islem, iki ayri fonksiyon
function temelUcret(gonderi, tarife) {
  const kademe = tarife.agirlikKademeleri.find((k) => gonderi.agirlikKg <= k.ustSinirKg);
  return kademe.kiloBasiUcret * gonderi.agirlikKg;
}

export function gonderiUcreti(gonderi, tarife) {
  const sigorta = gonderi.beyanDegeri * 0.01;
  return Math.max(temelUcret(gonderi, tarife) + sigorta, tarife.asgariUcret);
}

export function iadeUcreti(gonderi, tarife) {
  return Math.max(temelUcret(gonderi, tarife) * 0.6, tarife.asgariIadeUcreti);
}
```

"İade gönderisine sigorta uygulanmaz" kuralı artık bir koşul değil, `iadeUcreti`
fonksiyonunun kendisidir. Kuralı bozmak için fonksiyonu değiştirmek gerekir; bayraklı
sürümde bir çağrıda yanlış birleşim geçirmek yetiyordu.

#### Sınamayla Ölçüm

İki sürüm de aynı tarifeyle ve aynı iki gönderiyle sınanıyor. Ölçüt sabit: her dalın en az
bir sınamayla geçilmesi. Bayraklı sürümde iki bayrağın dört birleşimi var ve her birleşim
asgari ücretin devrede olduğu ve olmadığı durumda ayrı ayrı sınanmalı.

```js
// tarife.mjs — iki sinamanin paylastigi tarife ve gonderiler
export const TARIFE = {
  agirlikKademeleri: [{ ustSinirKg: 1, kiloBasiUcret: 38 }, { ustSinirKg: 5, kiloBasiUcret: 26 },
    { ustSinirKg: 10, kiloBasiUcret: 21 }, { ustSinirKg: 30, kiloBasiUcret: 17 }],
  asgariUcret: 52,
  asgariIadeUcreti: 40,
};
export const BUYUK = { agirlikKg: 4, beyanDegeri: 2000 };
export const KUCUK = { agirlikKg: 0.5, beyanDegeri: 0 };
```

```js
// test-bayrakli.mjs — iki bayragin dort birlesimi, her birinde asgari ucret siniri
import { test } from "node:test";
import assert from "node:assert/strict";
import { ucretHesapla } from "./bayrakli/ucret.mjs";
import { TARIFE, BUYUK, KUCUK } from "./tarife.mjs";

test("gonderi, sigortasiz, asgari uzeri", () => {
  assert.equal(ucretHesapla(BUYUK, TARIFE, false, false), 104);
});
test("gonderi, sigortasiz, asgari altinda", () => {
  assert.equal(ucretHesapla(KUCUK, TARIFE, false, false), 52);
});
test("gonderi, sigortali, asgari uzeri", () => {
  assert.equal(ucretHesapla(BUYUK, TARIFE, false, true), 124);
});
test("gonderi, sigortali, asgari altinda", () => {
  assert.equal(ucretHesapla(KUCUK, TARIFE, false, true), 52);
});
test("iade, sigortasiz, asgari uzeri", () => {
  assert.equal(ucretHesapla(BUYUK, TARIFE, true, false), 62.4);
});
test("iade, sigortasiz, asgari altinda", () => {
  assert.equal(ucretHesapla(KUCUK, TARIFE, true, false), 40);
});
test("iade, sigortali, sigorta bastirilir", () => {
  assert.equal(ucretHesapla(BUYUK, TARIFE, true, true), 62.4);
});
test("iade, sigortali, asgari altinda", () => {
  assert.equal(ucretHesapla(KUCUK, TARIFE, true, true), 40);
});
```

Bölünmüş sürümde birleşim yok; her fonksiyonun tek bir sınırı var.

```js
// test-bolunmus.mjs — iki fonksiyon, her birinde asgari ucret siniri
import { test } from "node:test";
import assert from "node:assert/strict";
import { gonderiUcreti, iadeUcreti } from "./bolunmus/ucret.mjs";
import { TARIFE, BUYUK, KUCUK } from "./tarife.mjs";

test("gonderi ucreti, asgari uzeri", () => {
  assert.equal(gonderiUcreti(BUYUK, TARIFE), 124);
});
test("gonderi ucreti, asgari altinda", () => {
  assert.equal(gonderiUcreti(KUCUK, TARIFE), 52);
});
test("iade ucreti, asgari uzeri", () => {
  assert.equal(iadeUcreti(BUYUK, TARIFE), 62.4);
});
test("iade ucreti, asgari altinda", () => {
  assert.equal(iadeUcreti(KUCUK, TARIFE), 40);
});
```

Sayım, çalıştırıcının kendi özetinden okunur. Süre alanları çalıştırmadan çalıştırmaya
değiştiği için yalnız sayaç satırları alınıyor.

```sh
for t in test-bayrakli.mjs test-bolunmus.mjs; do
  echo "== $t"
  node --test "$t" 2>&1 | grep -E '(tests|pass|fail) [0-9]+$'
done
```

```
== test-bayrakli.mjs
ℹ tests 8
ℹ pass 8
ℹ fail 0
== test-bolunmus.mjs
ℹ tests 4
ℹ pass 4
ℹ fail 0
```

Sekiz sınamaya karşı dört sınama. İki sürüm aynı dört sonucu (104, 124, 62,4 ve 40 ile
asgari ücret sınırları) üretiyor; fark, bayrakların birleşimlerinin ayrıca sınanmak
zorunda olmasından geliyor.

İkinci ölçüm çağrı yerinde. İlk derste tanımlanan opak argüman ölçütü doğruluk değerleri
için doğrudan geçerlidir: çıplak bir `true` çağrı satırında hangi parametreyi doldurduğunu
söylemez.

```sh
for t in test-bayrakli.mjs test-bolunmus.mjs; do
  echo "$t: $(grep -o 'true\|false' $t | wc -l | tr -d ' ') opak dogruluk argumani"
done
```

```
test-bayrakli.mjs: 16 opak dogruluk argumani
test-bolunmus.mjs: 0 opak dogruluk argumani
```

`ucretHesapla(BUYUK, TARIFE, true, false)` satırını okuyan kişi ikinci `false` değerinin
sigortayı mı yoksa başka bir seçeneği mi kapattığını bilemez; imzayı açmak zorundadır.
`iadeUcreti(BUYUK, TARIFE)` satırında sorulacak bir soru kalmaz.

#### Bayrak Sayısı Arttığında

İki bayrak dört birleşim üretti. Üçüncü bir bayrak eklendiğinde birleşim sayısı sekize
çıkar; sınama sayısı da aynı oranda artar. Bölünmüş tasarımda üçüncü bir iş üçüncü bir
fonksiyondur ve toplam iki değil bir artar. Bayraklı imza çarpar, bölünmüş imza toplar.

Bu, önceki derste ölçülen dallanma davranışının aynısıdır: aynı gövdede duran bağımsız
kararlar çarpılır. Bayrak parametresi bu çarpımı fonksiyonun **dışına** taşır; artık
birleşimleri üreten şey gövdedeki koşullar değil, çağıran tarafın geçirdiği değerlerdir.

#### Bayrağın Meşru Olduğu Yer

Her doğruluk değeri parametresi kusur değildir. Ayırt edici soru şudur: bayrak
fonksiyonun **ne yaptığını** mı yoksa **nasıl yaptığını** mı değiştiriyor?

`iade` bayrağı ne yapıldığını değiştiriyordu — iki ayrı iş vardı ve ikisi tek imzaya
sıkıştırılmıştı. Buna karşılık bir biçimlendirme fonksiyonuna geçirilen "kuruş kısmını
göster" bayrağı aynı işin bir ayrıntısını belirler; iş tektir. Böyle bir parametre yine de
çağrı yerinde opaktır ve adlandırılmış bir alan olarak geçirildiğinde
(`{ kurusGoster: true }`) opaklık ortadan kalkar.

İkinci ölçüt, bayrağın veriyle çakışıp çakışmadığıdır. `sigortali` bayrağı bunun örneğiydi:
beyan değeri zaten sigortanın uygulanıp uygulanmayacağını söylüyordu. Aynı bilgiyi iki
yerde tutmak, ikisinin ayrı düşmesine izin verir — sigortalı işaretlenmiş ama beyan değeri
sıfır olan bir gönderi tutarsızdır ve bayraklı imza bu tutarsızlığı kabul eder.

#### Özet

- Mantıksal bayrak parametresi fonksiyonun iki iş yaptığını ilan eder; bedeli gövdedeki
  dallanmalarda ve çağrı yerindeki opak argümanda ortaya çıkar.
- `iade` bayrağı imzada bir kez geçiyor, dosyada dört satırda görünüyordu: ücret çarpanı,
  sigortanın bastırılması ve asgari ücret seçimi ayrı ayrı ona bağlıydı.
- İki bayrağın dallarını kapsamak sekiz sınama gerektirdi; aynı davranışı iki ayrı
  fonksiyon olarak sınamak dört sınamayla tamamlandı.
- Çağrı yerlerinde on altı opak doğruluk argümanı sıfıra indi; bölünmüş imzalar çağrı
  satırında soru bırakmıyor.
- Bayrak sayısı arttıkça birleşimler çarpılır, ayrı fonksiyonlar toplanır; bayrak yalnız
  işin ne olduğunu değil nasıl yapıldığını belirlediğinde ve veriyle çakışmadığında
  savunulabilir.

#### Sonraki Adım

İmza kararları bir fonksiyonun dışarıya ne söylediğini belirliyor. Aynı soru modül
düzeyinde de sorulabilir: bir modül dışarıya kaç ad açıyor? `bolunmus/ucret.mjs`
dosyasında `temelUcret` dışa aktarılmadı, iki ücret fonksiyonu aktarıldı. Bu karar
keyfî değil; dışa açılan her ad, o modülün değiştirilemez yüzeyine eklenen bir maddedir.
Sonraki ders bu yüzeyi ölçer: bir modülün dışa açtığı adların kaçı gerçekten kullanılıyor
ve açılan her ad modülün değişme özgürlüğünü ne kadar daraltıyor.

### Kapsam ve Görünürlük

Source: https://academia.sh/tr/kurslar/temiz-kod/kapsam-ve-gorunurluk

En dar görünürlük ilkesinin ithal grafiğiyle ölçülmesi: aynı ücret paketinin her adı dışa açan ve yalnız iki ad açan iki sürümünde dışa açılan yüzeyin, bağımlı dosya sayısının, ölü yüzeyin ve serbestçe değiştirilebilir ad sayısının karşılaştırılması.

İmza kararları bir fonksiyonun dışarıya ne söylediğini belirliyor. Aynı soru modül
düzeyinde de sorulur: bir modül dışarıya kaç ad açıyor? Önceki dersteki
`bolunmus/ucret.mjs` dosyasında `temelUcret` dışa aktarılmamış, iki ücret fonksiyonu
aktarılmıştı. Bu karar keyfî değil.

**Kapsam** (scope), bir adın görülebildiği kod bölgesidir. Bir modül dosyasında tanımlanan
her ad, varsayılan olarak yalnız o dosyada görünür; `export` sözcüğü o adı dosyanın
dışına taşır. Dışa açılan her ad, modülün geri alamayacağı bir sözdür: başka bir dosya o
adı kullandığı anda ad artık modülün kendi malı değildir. Bu ders o sözün bedelini
sayıyor.

#### Her Adı Açan Paket

Ücret paketi dört dosyadan oluşuyor: hesabın kendisi ve onu kullanan üç modül — rapor,
etiket, fatura. Birinci sürümde hesap modülü her adını dışa açıyor.

```sh
mkdir -p genis dar
```

```js
// genis/ucret.mjs — modulun her adi disa acilmis
export const HACIMSEL_BOLEN = 3000;
export const ASGARI_UCRET = 52;
export const BOLGE_KATSAYILARI = { 1: 1, 2: 1.35, 3: 1.8 };
export const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 10, kiloBasiUcret: 21 },
  { ustSinirKg: 30, kiloBasiUcret: 17 }];

export function hacimselAgirlikKg(gonderi) {
  return (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
}

export function ucretlendirilenAgirlikKg(gonderi) {
  return Math.max(gonderi.agirlikKg, hacimselAgirlikKg(gonderi));
}

export function kademeSec(agirlikKg) {
  return AGIRLIK_KADEMELERI.find((k) => agirlikKg <= k.ustSinirKg);
}

export function kademeUcreti(agirlikKg) {
  return kademeSec(agirlikKg).kiloBasiUcret * agirlikKg;
}

export function ucretHesapla(gonderi) {
  const agirlikKg = ucretlendirilenAgirlikKg(gonderi);
  return Math.max(kademeUcreti(agirlikKg) * BOLGE_KATSAYILARI[gonderi.bolgeKodu], ASGARI_UCRET);
}
```

Açık olan her ad, kullanılabilir olduğu için kullanılır.

```js
// genis/rapor.mjs — iki ic ada ve bir sabite dogrudan basvuruyor
import { ucretHesapla, kademeSec, AGIRLIK_KADEMELERI } from "./ucret.mjs";

export function rapor(gonderiler) {
  const satirlar = gonderiler.map((g) =>
    `${g.kod} kademe<=${kademeSec(g.agirlikKg).ustSinirKg}kg ucret=${ucretHesapla(g).toFixed(2)}`);
  return satirlar.concat(`kademe sayisi=${AGIRLIK_KADEMELERI.length}`);
}
```

```js
// genis/etiket.mjs — hacimsel bolene dogrudan basvuruyor
import { ucretlendirilenAgirlikKg, HACIMSEL_BOLEN } from "./ucret.mjs";

export function etiket(gonderi) {
  return `${gonderi.kod} ${ucretlendirilenAgirlikKg(gonderi).toFixed(2)}kg (bolen ${HACIMSEL_BOLEN})`;
}
```

```js
// genis/fatura.mjs — kademe ucretini ayrica hesaplatiyor
import { ucretHesapla, kademeUcreti } from "./ucret.mjs";

export function fatura(gonderi) {
  const agirlikKg = Math.max(gonderi.agirlikKg,
    (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / 3000);
  return `${gonderi.kod} kademe=${kademeUcreti(agirlikKg).toFixed(2)} toplam=${ucretHesapla(gonderi).toFixed(2)}`;
}
```

`fatura.mjs` dikkat çekici: hacimsel bölen dışa açık olmasına rağmen o dosyada `3000`
sayısı elle yazılmış. Yüzeyin geniş olması tutarlılığı garanti etmiyor, yalnız seçenek
sayısını arttırıyor.

#### İki Adı Açan Paket

İkinci sürümde hesap modülü iki ad açıyor: ücretin kendisi ve hesabın dökümü. Kullanan
modüllerin ihtiyacı olan ara değerler dökümde adlarıyla veriliyor; modülün sabitleri ve
ara fonksiyonları dışarı çıkmıyor.

```js
// dar/ucret.mjs — iki ad disa acik, gerisi modulun ici
const HACIMSEL_BOLEN = 3000;
const ASGARI_UCRET = 52;
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 hacimselAgirlikKg(gonderi) {
  return (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
}

function agirlikKg(gonderi) {
  return Math.max(gonderi.agirlikKg, hacimselAgirlikKg(gonderi));
}

function kademeSec(kg) {
  return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg);
}

function kademeUcreti(kg) {
  return kademeSec(kg).kiloBasiUcret * kg;
}

export function ucretHesapla(gonderi) {
  const kg = agirlikKg(gonderi);
  return Math.max(kademeUcreti(kg) * BOLGE_KATSAYILARI[gonderi.bolgeKodu], ASGARI_UCRET);
}

export function ucretDokumu(gonderi) {
  const kg = agirlikKg(gonderi);
  return { agirlikKg: kg, hacimselBolen: HACIMSEL_BOLEN, kademeSayisi: AGIRLIK_KADEMELERI.length,
    kademeUstSinirKg: kademeSec(kg).ustSinirKg, kademeUcreti: kademeUcreti(kg) };
}
```

```js
// dar/rapor.mjs — yalnizca dokum ve ucret uzerinden
import { ucretHesapla, ucretDokumu } from "./ucret.mjs";

export function rapor(gonderiler) {
  const satirlar = gonderiler.map((g) =>
    `${g.kod} kademe<=${ucretDokumu(g).kademeUstSinirKg}kg ucret=${ucretHesapla(g).toFixed(2)}`);
  return satirlar.concat(`kademe sayisi=${ucretDokumu(gonderiler[0]).kademeSayisi}`);
}
```

```js
// dar/etiket.mjs — bolen degeri dokumden geliyor
import { ucretDokumu } from "./ucret.mjs";

export function etiket(gonderi) {
  const dokum = ucretDokumu(gonderi);
  return `${gonderi.kod} ${dokum.agirlikKg.toFixed(2)}kg (bolen ${dokum.hacimselBolen})`;
}
```

```js
// dar/fatura.mjs — kademe ucreti dokumden geliyor
import { ucretHesapla, ucretDokumu } from "./ucret.mjs";

export function fatura(gonderi) {
  return `${gonderi.kod} kademe=${ucretDokumu(gonderi).kademeUcreti.toFixed(2)}` +
    ` toplam=${ucretHesapla(gonderi).toFixed(2)}`;
}
```

İki paketin aynı çıktıyı ürettiği doğrulanır.

```js
// karsilastir.mjs — iki paketin ayni ciktiyi urettigi dogrulanir
import { rapor as genisRapor } from "./genis/rapor.mjs";
import { etiket as genisEtiket } from "./genis/etiket.mjs";
import { fatura as genisFatura } from "./genis/fatura.mjs";
import { rapor as darRapor } from "./dar/rapor.mjs";
import { etiket as darEtiket } from "./dar/etiket.mjs";
import { fatura as darFatura } from "./dar/fatura.mjs";

const GONDERILER = [
  { kod: "GN-4172", agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18, bolgeKodu: 2 },
  { kod: "GN-4173", agirlikKg: 0.4, enCm: 10, boyCm: 10, yukseklikCm: 10, bolgeKodu: 1 },
];

const genis = [...genisRapor(GONDERILER), ...GONDERILER.map(genisEtiket), ...GONDERILER.map(genisFatura)];
const dar = [...darRapor(GONDERILER), ...GONDERILER.map(darEtiket), ...GONDERILER.map(darFatura)];

for (const [i, satir] of genis.entries()) {
  console.log(`${satir === dar[i] ? "ayni  " : "FARKLI"} ${satir}`);
}
```

```
ayni   GN-4172 kademe<=5kg ucret=151.63
ayni   GN-4173 kademe<=1kg ucret=52.00
ayni   kademe sayisi=4
ayni   GN-4172 4.32kg (bolen 3000)
ayni   GN-4173 0.40kg (bolen 3000)
ayni   GN-4172 kademe=112.32 toplam=151.63
ayni   GN-4173 kademe=15.20 toplam=52.00
```

#### Yüzey Ölçer

Ölçer bir dizindeki modülleri okur, her modülün dışa açtığı ve içeride tuttuğu adları
çıkarır, sonra ithal satırlarından hangi adın hangi dosyalarca kullanıldığını sayar. Son
sütun, modülün kendi başına değiştirebileceği ad sayısıdır: dışarıdan bağımlısı olmayan
her ad bu sayıya girer.

```js
// yuzey-olc.mjs — modullerin disa actigi ad sayisi, bu adlarin bagimlilari ve degisim ozgurlugu
import { readFileSync, readdirSync } from "node:fs";

const acikAdlar = (m) => [...m.matchAll(/^export\s+(?:const|function|class)\s+(\w+)/gm)].map((e) => e[1]);
const icAdlar = (m) => [...m.matchAll(/^(?:const|function|class)\s+(\w+)/gm)].map((e) => e[1]);
const ithaller = (m) => [...m.matchAll(/import\s*{([^}]*)}\s*from\s*"\.\/([\w.-]+)"/g)]
  .map((e) => [e[2], e[1].split(",").map((s) => s.trim()).filter(Boolean)]);

for (const dizin of process.argv.slice(2)) {
  const dosyalar = readdirSync(dizin).filter((a) => a.endsWith(".mjs")).sort();
  const metin = Object.fromEntries(dosyalar.map((a) => [a, readFileSync(`${dizin}/${a}`, "utf8")]));
  const kullanim = new Map();
  for (const m of Object.values(metin)) {
    for (const [hedef, adlar] of ithaller(m)) {
      for (const ad of adlar) kullanim.set(`${hedef}:${ad}`, (kullanim.get(`${hedef}:${ad}`) ?? 0) + 1);
    }
  }
  console.log(dizin);
  for (const dosya of dosyalar) {
    const acik = acikAdlar(metin[dosya]);
    const ic = icAdlar(metin[dosya]);
    const bagimli = acik.map((ad) => [ad, kullanim.get(`${dosya}:${ad}`) ?? 0]);
    const bagli = bagimli.filter(([, n]) => n > 0);
    console.log(`  ${dosya.padEnd(11)} disa acik=${acik.length}  ic ad=${ic.length}` +
      `  bagimlisi olan=${bagli.length}  olu yuzey=${acik.length - bagli.length}` +
      `  serbestce degistirilebilir=${acik.length + ic.length - bagli.length}`);
    for (const [ad, n] of bagli) console.log(`    ${ad.padEnd(24)} bagimli dosya=${n}`);
  }
}
```

```sh
node yuzey-olc.mjs genis dar
```

```
genis
  etiket.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  fatura.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  rapor.mjs   disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  ucret.mjs   disa acik=9  ic ad=0  bagimlisi olan=6  olu yuzey=3  serbestce degistirilebilir=3
    HACIMSEL_BOLEN           bagimli dosya=1
    AGIRLIK_KADEMELERI       bagimli dosya=1
    ucretlendirilenAgirlikKg bagimli dosya=1
    kademeSec                bagimli dosya=1
    kademeUcreti             bagimli dosya=1
    ucretHesapla             bagimli dosya=2
dar
  etiket.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  fatura.mjs  disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  rapor.mjs   disa acik=1  ic ad=0  bagimlisi olan=0  olu yuzey=1  serbestce degistirilebilir=1
  ucret.mjs   disa acik=2  ic ad=8  bagimlisi olan=2  olu yuzey=0  serbestce degistirilebilir=8
    ucretHesapla             bagimli dosya=2
    ucretDokumu              bagimli dosya=3
```

Hesap modülünün yüzeyi dokuzdan ikiye indi. Bundan daha önemlisi son sütun: geniş sürümde
modül kendi adlarının üçünü, dar sürümde sekizini kimseye sormadan değiştirebilir. Aradaki
fark, altı adın dışarıdan bağlanmış olmasıdır.

Üç kullanıcı modülün `olu yuzey=1` görünmesi ölçümün sınırıdır: bunlar paketin dış
yüzüdür, kendi içinde kimse onları kullanmaz. Ölçü yalnız verilen dizindeki bağları görür.

#### Yeniden Adlandırma Maliyeti

Değişim özgürlüğü soyut bir kavram değil. Bir adı yeniden adlandırmanın maliyeti, o adın
geçtiği dosya sayısıdır.

```sh
for d in genis dar; do
  echo "$d: kademeSec $(grep -rl kademeSec $d | wc -l | tr -d ' ') dosyada," \
    "HACIMSEL_BOLEN $(grep -rl HACIMSEL_BOLEN $d | wc -l | tr -d ' ') dosyada"
done
```

```
genis: kademeSec 2 dosyada, HACIMSEL_BOLEN 2 dosyada
dar: kademeSec 1 dosyada, HACIMSEL_BOLEN 1 dosyada
```

Fark iki dosyaya karşı bir dosya gibi görünüyor, çünkü paket küçük. Ölçünün asıl anlamı
oranda: dar sürümde bu iki ad **modülün içinde**dir ve dışarıdan kaç dosya olursa olsun
sayı bir kalır. Geniş sürümde sayı, o adı ithal eden dosya sayısıyla birlikte büyür.

Kademe seçiminin `Map` tabanlı bir yapıya dönüştürülmesi, hacimsel bölenin taşıyıcıya göre
değişen bir alana taşınması, asgari ücretin tarifeden okunması — bunların hepsi dar
sürümde modülün iç işidir. Geniş sürümde her biri dışarıdaki dosyaları da değiştirmeyi
gerektirir.

#### En Dar Görünürlük İlkesi

İlke tek cümledir: **bir ad, işini görebilecek en dar kapsamda tanımlanır.** Uygulaması üç
adımdır.

**Varsayılan kapalıdır.** Yeni bir ad önce dosyanın içinde kalır. Dışa açma, bir kullanıcı
gerçekten ortaya çıktığında ve o kullanıcının başka yolu olmadığında yapılır.

**Genişletmek kolay, daraltmak zordur.** Kapalı bir adı açmak tek satırlık bir
değişikliktir ve hiçbir dosyayı bozmaz. Açık bir adı kapatmak, onu kullanan her dosyayı
değiştirmeyi gerektirir; ölçümdeki `bagimli dosya` sütunu tam olarak bu maliyettir.

**Yüzey, ara değerleri değil kararları taşır.** `ucretDokumu` bunun örneğiydi: kullanan
modüllerin ihtiyacı ara fonksiyonlar değil, hesabın sonuçlarıydı. Dışarıya bir sabit
vermek yerine o sabitin belirlediği sonucu vermek, sabitin değişme özgürlüğünü korur.

Aynı ilke fonksiyon içinde de işler. Bir değişken kullanıldığı bloğun dışında
tanımlandığında, o bloğun dışındaki her satır onu değiştirebilir hâle gelir; okuyan kişi
de değerin ne olduğunu anlamak için aradaki satırların hepsini izlemek zorunda kalır.

#### Özet

- Dışa açılan her ad geri alınması pahalı bir sözdür; ölçüsü, o adı ithal eden dosya
  sayısıdır.
- Aynı davranışı üreten iki pakette hesap modülünün yüzeyi dokuz ve iki addı; ölü yüzey
  üçten sıfıra indi.
- Modülün kimseye sormadan değiştirebileceği ad sayısı üçten sekize çıktı; aradaki fark,
  dışarıdan bağlanmış altı addır.
- Ara değerleri değil kararları dışa açmak sabitlerin değişme özgürlüğünü korur;
  `ucretDokumu` sabitleri değil sonuçları veriyor.
- Genişletmek tek satırlık bir değişikliktir, daraltmak her bağımlı dosyayı değiştirmeyi
  gerektirir; bu yüzden varsayılan kapalıdır.

#### Sonraki Adım

Görünürlük kararı bir modülün dışarıya ne verdiğini belirliyor. Bir soru daha kalıyor: bu
modüller hangi ölçüte göre ayrıldı? Ücret hesabı bir dosyada, rapor başka bir dosyada
duruyor; ayrım işlevsel görünüyor. Ancak bir kod tabanını asıl zorlayan şey, farklı
kişilerin farklı nedenlerle aynı dosyaya dokunmasıdır. Muhasebenin istediği bir yuvarlama
değişikliği ile operasyonun istediği bir kademe değişikliği aynı dosyada buluşuyorsa,
iki karar birbirini bekler. Sonraki ders bu buluşmayı ölçer: iki farklı değişim nedeni
aynı dosyaya kaç kez dokunuyor ve kod aktörlere göre düzenlendiğinde bu sayı ne oluyor.

### Kodun Aktöre Göre Düzenlenmesi

Source: https://academia.sh/tr/kurslar/temiz-kod/kodun-aktore-gore-duzenlenmesi

Kodun teknik katmana göre değil değişim nedenine göre gruplanmasının ölçülmesi: aynı ücret paketinin iki düzeninde bir dosyada kaç aktörün kararının tanımlı olduğu, her aktörün kaç dosyaya yayıldığı ve tek bir muhasebe değişikliğinin iki düzende kaç dosyaya dokunduğu.

Görünürlük kararı bir modülün dışarıya ne verdiğini belirliyor. Bir soru daha kalıyor: bu
modüller hangi ölçüte göre ayrıldı? Ücret hesabı bir dosyada, biçimlendirme başka bir
dosyada; ayrım teknik görünüyor ve makul.

Bir kod tabanını asıl zorlayan şey, farklı kişilerin farklı nedenlerle aynı dosyaya
dokunmasıdır. Muhasebenin istediği bir yuvarlama değişikliği ile operasyonun istediği bir
kademe değişikliği aynı dosyada buluşuyorsa, iki karar birbirini bekler; biri diğerinin
sınamasını bozar. Bu ders, bir dosyanın kaç ayrı **aktör**e hizmet ettiğini sayıyor.

#### Aktör Nedir

Aktör, bir değişiklik isteğinin kaynağıdır: bir rol, bir birim, bir paydaş. Ücretlendirme
kitaplığında üçü var.

- **Operasyon**, taşımanın nasıl yapıldığına karar verir: ağırlık kademeleri, hacimsel
  bölen, aktarma sayısı, teslim süresi.
- **Muhasebe**, paranın nasıl sayıldığına karar verir: vergi oranı, yuvarlama kuralı,
  fatura satırının biçimi.
- **Müşteri iletişimi**, müşteriye ne söylendiğine karar verir: etiket metni, teslim
  mesajı.

Üçü birbirinden bağımsız değişir. Vergi oranı değiştiğinde ağırlık kademelerinin
değişmesi için bir neden yoktur. Ölçüt buradan çıkar: bir dosya tek bir aktörün
isteğiyle değişmelidir.

#### Katmana Göre Düzen

Birinci düzen dosyaları teknik işlevine göre ayırıyor: hesap, akış, biçim. Bu ayrım
Veri Erişim Katmanı ve İş Mantığı kursunda kurulan katman sorumluluklarına benzer
görünür, ama orada ayrımı belirleyen şey bağımlılık yönüydü; burada yalnız işin türü.

```sh
mkdir -p katman aktor
```

```js
// katman/hesap.mjs — operasyonun kademe kurallari ile muhasebenin vergi kurallari bir arada
export const HACIMSEL_BOLEN = 3000;
export const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 30, kiloBasiUcret: 17 }];
export const KDV_ORANI = 0.2;

export function ucretlendirilenAgirlikKg(gonderi) {
  const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
  return Math.max(gonderi.agirlikKg, hacimsel);
}

export function kademeSec(kg) {
  return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg);
}

export function netUcret(gonderi) {
  const kg = ucretlendirilenAgirlikKg(gonderi);
  return kademeSec(kg).kiloBasiUcret * kg;
}

export function yuvarla(tutar) {
  return Math.round(tutar * 100) / 100;
}

export function kdv(net) {
  return yuvarla(net * KDV_ORANI);
}
```

```js
// katman/akis.mjs — operasyonun aktarma kurali ile musterinin teslim mesaji bir arada
export function aktarmaSayisi(gonderi) {
  return gonderi.bolgeKodu > 2 ? 2 : 1;
}

export function teslimGunu(gonderi) {
  return 1 + aktarmaSayisi(gonderi);
}

export function teslimMesaji(gonderi) {
  return `${gonderi.kod} kargonuz ${teslimGunu(gonderi)} gun icinde teslim edilecek`;
}
```

```js
// katman/bicim.mjs — muhasebenin fatura bicimi ile musterinin etiket metni bir arada
import { netUcret, kdv, yuvarla, ucretlendirilenAgirlikKg } from "./hesap.mjs";
import { aktarmaSayisi } from "./akis.mjs";

export function faturaSatiri(gonderi) {
  const net = yuvarla(netUcret(gonderi));
  const vergi = kdv(netUcret(gonderi));
  return `${gonderi.kod} net=${net.toFixed(2)} kdv=${vergi.toFixed(2)} toplam=${yuvarla(net + vergi).toFixed(2)}`;
}

export function etiketMetni(gonderi) {
  return `${gonderi.kod} ${ucretlendirilenAgirlikKg(gonderi).toFixed(2)}kg ${aktarmaSayisi(gonderi)} aktarma`;
}
```

Her dosya kendi içinde tutarlı. Yine de `hesap.mjs` hem kademeleri hem vergiyi,
`bicim.mjs` hem faturayı hem etiketi, `akis.mjs` hem aktarmayı hem müşteri mesajını
taşıyor.

#### Aktöre Göre Düzen

İkinci düzen aynı fonksiyonları aktörlerine göre topluyor.

```js
// aktor/operasyon.mjs — kademe, hacim ve aktarma kararlari
export const HACIMSEL_BOLEN = 3000;
export const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 30, kiloBasiUcret: 17 }];

export function ucretlendirilenAgirlikKg(gonderi) {
  const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
  return Math.max(gonderi.agirlikKg, hacimsel);
}

export function kademeSec(kg) {
  return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg);
}

export function netUcret(gonderi) {
  const kg = ucretlendirilenAgirlikKg(gonderi);
  return kademeSec(kg).kiloBasiUcret * kg;
}

export function aktarmaSayisi(gonderi) {
  return gonderi.bolgeKodu > 2 ? 2 : 1;
}

export function teslimGunu(gonderi) {
  return 1 + aktarmaSayisi(gonderi);
}
```

```js
// aktor/muhasebe.mjs — vergi, yuvarlama ve fatura bicimi
import { netUcret } from "./operasyon.mjs";

export const KDV_ORANI = 0.2;

export function yuvarla(tutar) {
  return Math.round(tutar * 100) / 100;
}

export function kdv(net) {
  return yuvarla(net * KDV_ORANI);
}

export function faturaSatiri(gonderi) {
  const net = yuvarla(netUcret(gonderi));
  const vergi = kdv(netUcret(gonderi));
  return `${gonderi.kod} net=${net.toFixed(2)} kdv=${vergi.toFixed(2)} toplam=${yuvarla(net + vergi).toFixed(2)}`;
}
```

```js
// aktor/musteri.mjs — musteriye gorunen metinler
import { ucretlendirilenAgirlikKg, aktarmaSayisi, teslimGunu } from "./operasyon.mjs";

export function etiketMetni(gonderi) {
  return `${gonderi.kod} ${ucretlendirilenAgirlikKg(gonderi).toFixed(2)}kg ${aktarmaSayisi(gonderi)} aktarma`;
}

export function teslimMesaji(gonderi) {
  return `${gonderi.kod} kargonuz ${teslimGunu(gonderi)} gun icinde teslim edilecek`;
}
```

```js
// karsilastir.mjs — iki duzenin ayni ciktiyi urettigi dogrulanir
import { faturaSatiri as kFatura, etiketMetni as kEtiket } from "./katman/bicim.mjs";
import { teslimMesaji as kMesaj } from "./katman/akis.mjs";
import { faturaSatiri as aFatura } from "./aktor/muhasebe.mjs";
import { etiketMetni as aEtiket, teslimMesaji as aMesaj } from "./aktor/musteri.mjs";

const gonderi = { kod: "GN-4172", agirlikKg: 2.4, enCm: 30, boyCm: 24, yukseklikCm: 18, bolgeKodu: 3 };
for (const [katman, aktor] of [[kFatura, aFatura], [kEtiket, aEtiket], [kMesaj, aMesaj]]) {
  const a = katman(gonderi), b = aktor(gonderi);
  console.log(`${a === b ? "ayni  " : "FARKLI"} ${a}`);
}
```

```
ayni   GN-4172 net=112.32 kdv=22.46 toplam=134.78
ayni   GN-4172 4.32kg 2 aktarma
ayni   GN-4172 kargonuz 3 gun icinde teslim edilecek
```

#### Ölçüm: Bir Dosyada Kaç Aktör

Ölçer her aktörün sözcük dağarcığını alır ve her dosyada hangi aktörlerin kararlarının
**tanımlı** olduğuna bakar. İthal edilen bir ad o dosyanın kararı değildir; yalnız
tanımlar sayılır.

```js
// aktor-olc.mjs — her dosyada hangi aktorun kararlari tanimli, her aktor kac dosyaya yayilmis
import { readFileSync, readdirSync } from "node:fs";

const AKTOR = {
  operasyon: ["HACIMSEL_BOLEN", "AGIRLIK_KADEMELERI", "ucretlendirilenAgirlikKg", "kademeSec",
    "netUcret", "aktarmaSayisi", "teslimGunu"],
  muhasebe: ["KDV_ORANI", "yuvarla", "kdv", "faturaSatiri"],
  musteri: ["etiketMetni", "teslimMesaji"],
};

// Yalnizca tanimlar sayilir; baska bir dosyadan ithal edilen ad o dosyanin karari degildir.
const tanimlar = (m) => [...m.matchAll(/^export\s+(?:const|function|class)\s+(\w+)/gm)].map((e) => e[1]);

for (const dizin of process.argv.slice(2)) {
  const dosyalar = readdirSync(dizin).filter((a) => a.endsWith(".mjs")).sort();
  const yayilim = Object.fromEntries(Object.keys(AKTOR).map((a) => [a, 0]));
  let cakisan = 0;
  console.log(dizin);
  for (const dosya of dosyalar) {
    const adlar = new Set(tanimlar(readFileSync(`${dizin}/${dosya}`, "utf8")));
    const sahipler = Object.entries(AKTOR)
      .filter(([, sozcukler]) => sozcukler.some((s) => adlar.has(s))).map(([a]) => a);
    for (const a of sahipler) yayilim[a] += 1;
    if (sahipler.length > 1) cakisan += 1;
    console.log(`  ${dosya.padEnd(14)} aktor=${sahipler.length}  [${sahipler.join(", ")}]`);
  }
  const dokum = Object.entries(yayilim).map(([a, n]) => `${a}=${n}`).join("  ");
  console.log(`  cakisan dosya=${cakisan}   aktor basina dosya: ${dokum}`);
}
```

```sh
node aktor-olc.mjs katman aktor
```

```
katman
  akis.mjs       aktor=2  [operasyon, musteri]
  bicim.mjs      aktor=2  [muhasebe, musteri]
  hesap.mjs      aktor=2  [operasyon, muhasebe]
  cakisan dosya=3   aktor basina dosya: operasyon=2  muhasebe=2  musteri=2
aktor
  muhasebe.mjs   aktor=1  [muhasebe]
  musteri.mjs    aktor=1  [musteri]
  operasyon.mjs  aktor=1  [operasyon]
  cakisan dosya=0   aktor basina dosya: operasyon=1  muhasebe=1  musteri=1
```

Katman düzeninde üç dosyanın üçü de çakışma noktası; her biri iki ayrı aktörün isteğiyle
değişebilir. Aktör düzeninde çakışma yok. İkinci sayı da aynı şeyi tersinden söylüyor:
katman düzeninde her aktörün kararları iki dosyaya yayılmış, aktör düzeninde bir dosyada
toplanmış.

#### Gerçek Bir Değişiklik

Sayıların iddiası bir istekle sınanır. Muhasebe iki şey değiştiriyor: katma değer vergisi
oranı yüzde yirmiden yüzde ona iniyor ve tutarlar kuruşa değil tam liraya yuvarlanıyor.
İkinci karar fatura satırının biçimini de etkiliyor, çünkü kuruş hanesi artık anlamsız.

```sh
cp -r katman katman-yeni && cp -r aktor aktor-yeni
```

```js
// katman-yeni/hesap.mjs — vergi orani ve yuvarlama kurali degisti
export const HACIMSEL_BOLEN = 3000;
export const AGIRLIK_KADEMELERI = [{ ustSinirKg: 1, kiloBasiUcret: 38 },
  { ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 30, kiloBasiUcret: 17 }];
export const KDV_ORANI = 0.1;

export function ucretlendirilenAgirlikKg(gonderi) {
  const hacimsel = (gonderi.enCm * gonderi.boyCm * gonderi.yukseklikCm) / HACIMSEL_BOLEN;
  return Math.max(gonderi.agirlikKg, hacimsel);
}

export function kademeSec(kg) {
  return AGIRLIK_KADEMELERI.find((k) => kg <= k.ustSinirKg);
}

export function netUcret(gonderi) {
  const kg = ucretlendirilenAgirlikKg(gonderi);
  return kademeSec(kg).kiloBasiUcret * kg;
}

export function yuvarla(tutar) {
  return Math.round(tutar);
}

export function kdv(net) {
  return yuvarla(net * KDV_ORANI);
}
```

```js
// katman-yeni/bicim.mjs — fatura satirindan kurus hanesi kalkti
import { netUcret, kdv, yuvarla, ucretlendirilenAgirlikKg } from "./hesap.mjs";
import { aktarmaSayisi } from "./akis.mjs";

export function faturaSatiri(gonderi) {
  const net = yuvarla(netUcret(gonderi));
  const vergi = kdv(netUcret(gonderi));
  return `${gonderi.kod} net=${net.toFixed(0)} kdv=${vergi.toFixed(0)} toplam=${yuvarla(net + vergi).toFixed(0)}`;
}

export function etiketMetni(gonderi) {
  return `${gonderi.kod} ${ucretlendirilenAgirlikKg(gonderi).toFixed(2)}kg ${aktarmaSayisi(gonderi)} aktarma`;
}
```

```js
// aktor-yeni/muhasebe.mjs — ayni iki karar, tek dosyada
import { netUcret } from "./operasyon.mjs";

export const KDV_ORANI = 0.1;

export function yuvarla(tutar) {
  return Math.round(tutar);
}

export function kdv(net) {
  return yuvarla(net * KDV_ORANI);
}

export function faturaSatiri(gonderi) {
  const net = yuvarla(netUcret(gonderi));
  const vergi = kdv(netUcret(gonderi));
  return `${gonderi.kod} net=${net.toFixed(0)} kdv=${vergi.toFixed(0)} toplam=${yuvarla(net + vergi).toFixed(0)}`;
}
```

```sh
for d in katman aktor; do
  n=0
  for f in "$d"/*.mjs; do
    diff -q <(tail -n +2 "$f") <(tail -n +2 "$d-yeni/$(basename "$f")") > /dev/null || n=$((n + 1))
  done
  echo "$d duzeni: muhasebe degisikligi $n dosyaya dokundu"
done
```

```
katman duzeni: muhasebe degisikligi 2 dosyaya dokundu
aktor duzeni: muhasebe degisikligi 1 dosyaya dokundu
```

Katman düzeninde `bicim.mjs` de değişti ve o dosyada müşteri iletişiminin sahip olduğu
`etiketMetni` de duruyor. Muhasebenin isteğiyle açılan bir değişiklik, müşteri
iletişiminin dosyasına dokunmuş oldu. Aktör düzeninde `musteri.mjs` hiç açılmadı.

Etiket metnindeki ağırlığın iki haneli kalması da bunu gösteriyor: kuruş kararı paraya
aitti, ağırlığa değil. Katman düzeninde iki biçim aynı dosyada durduğu için ayrımı yapan
şey dosya değil, değişikliği yapan kişinin dikkatidir.

#### Ayrımın Sınırı

Aktöre göre gruplama, teknik ayrımların yerine geçmez; onlarla dik açıda durur. Bir
uygulamada hem katman ayrımı (bağımlılık yönünü belirler) hem aktör ayrımı (değişim
nedenini belirler) bulunur. İkisi çakıştığında seçim, hangi ayrımın daha sık değiştiğine
göre yapılır: yılda üç kez vergi değişiyor ve yılda bir kez veri kaynağı değişiyorsa,
üstteki bölme aktöre göre olur.

İkinci sınır, aktörlerin de zamanla değişmesidir. Muhasebe ile fiyatlandırma bugün aynı
birimse `muhasebe.mjs` ikisini birden taşır; ayrıldıkları gün dosya da bölünür. Ölçüm bu
durumu erken gösterir: bir dosyanın aktör sayısı ikiye çıktığında bölme zamanı gelmiştir.

Üçüncü sınır, ölçünün sözlüğe bağlı olmasıdır. Aktör sözlüğü elle yazılır ve yanlış
yazılırsa ölçüm de yanlış çıkar. Sözlüğü doğru tutan şey koddan değil, değişiklik
isteklerinin geldiği yerden okunur.

#### Özet

- Aktör, bir değişiklik isteğinin kaynağıdır; ölçüt, bir dosyanın tek bir aktörün
  isteğiyle değişmesidir.
- Katman düzeninde üç dosyanın üçü de iki aktörün kararını taşıyordu; aktör düzeninde
  çakışan dosya sayısı sıfır.
- Aktör başına dosya sayısı ikiden bire indi; her aktörün kararları tek bir yerde toplandı.
- Vergi oranı ve yuvarlama değişikliği katman düzeninde iki, aktör düzeninde bir dosyaya
  dokundu; katman düzeninde dokunulan ikinci dosya müşteri iletişiminin metnini de
  taşıyordu.
- Aktör ayrımı katman ayrımının yerine geçmez; ikisi çakıştığında daha sık değişen ayrım
  üstte durur ve bir dosyanın aktör sayısı ikiye çıktığında bölme zamanı gelmiştir.

#### Sonraki Adım

Aktör düzeninde `faturaSatiri` bir dosyaya taşındı, ama kitaplıkta hâlâ aynı işin birden
çok biçimde yapıldığı yerler var: bir yerde tutar `toFixed` ile, başka bir yerde elle
biçimlendiriliyor; bir yerde hata atılıyor, başka bir yerde boş sonuç dönüyor. Bunların
hiçbiri yanlış değil, yalnız farklı. Farklılığın da bir bedeli var ve sayılabilir.
Sonraki ders aynı işin kaç ayrı biçimde yapıldığını bulan bir tarama yazar, tekilleştirme
sonrası ölçümü tekrarlar.

### Tutarlılık

Source: https://academia.sh/tr/kurslar/temiz-kod/tutarlilik

Aynı işin projede tek biçimde yapılmasının ölçülmesi: üç ayrı işi üç ayrı biçimde yapan bir pakette biçim sayısını bulan tarama, tekilleştirmeden sonra ölçümün tekrarlanması ve biçim birleştirmenin çıktılarda ve hata davranışında yol açtığı değişikliklerin sayılması.

Aktör düzeninde her kararın bir sahibi oldu. Kitaplıkta yine de rahatsız edici bir şey
kalıyor: bir dosyada tutar `toFixed` ile yazılıyor, başka bir dosyada elle yuvarlanıp
başına para birimi ekleniyor; bir yerde bulunamama hata atarak, başka bir yerde boş değer
dönerek bildiriliyor.

Bunların hiçbiri yanlış değil. Üçü de çalışıyor, üçü de sınamalardan geçiyor. Yalnız
**farklılar** ve farklılığın bedeli okuyanın üzerinde birikir: her dosyada aynı sorunun
cevabı yeniden aranır. Bu ders farklılığı sayıya bağlıyor — aynı iş kaç ayrı biçimde
yapılıyor.

#### Ölçüt: İş Başına Biçim Sayısı

Bir kod tabanında tekrarlanan işler vardır: para yazmak, bulunamama durumunu bildirmek,
bir modülü dışa açmak. Her iş için o kod tabanında kullanılan farklı yazım biçimlerinin
sayısı, tutarlılığın ölçüsüdür. Hedef her zaman birdir; hangi biçim olduğu ikinci
sorudur.

Sayı doğrudan bir maliyete karşılık gelir. Bir iş üç biçimde yapılıyorsa, o işe yeni bir
yerde ihtiyaç duyan kişi üç örnek görür ve hangisini izleyeceğine karar vermek zorunda
kalır. Kararı verirken çoğunlukla en son baktığı dosyayı örnek alır; böylece biçim sayısı
azalmaz, dağılım rastlantısallaşır.

#### Üç İş, Üç Ayrı Biçim

Paketteki üç modül üç ayrı iş yapıyor: ücret satırı, etiket satırı, günlük toplam. Üçü de
tutar yazıyor, üçü de bulunamama durumunu bildiriyor, üçü de bir şey dışa açıyor.

```sh
mkdir -p karisik tekil
```

```js
// karisik/ucret.mjs — ucret satiri
export function ucretSatiri(gonderi, tarife) {
  const kademe = tarife.kademeler.find((k) => gonderi.agirlikKg <= k.ustSinirKg);
  if (kademe === undefined) throw new RangeError("kademe bulunamadi");
  return `${gonderi.kod} ${(kademe.kiloBasiUcret * gonderi.agirlikKg).toFixed(2)} TL`;
}
```

```js
// karisik/etiket.mjs — etiket satiri
export const etiketSatiri = (gonderi, bolgeAdlari) => {
  const bolge = bolgeAdlari[gonderi.bolgeKodu];
  if (bolge === undefined) return null;
  return `${gonderi.kod} ${bolge} ${gonderi.beyanDegeri.toFixed(2)}TL`;
};
```

```js
// karisik/rapor.mjs — gunluk toplam
function gunlukToplam(gonderiler, gun) {
  const gunun = gonderiler.filter((g) => g.gun === gun);
  if (gunun.length === 0) return { hata: "o gune ait gonderi yok" };
  const toplam = Math.round(gunun.reduce((t, g) => t + g.ucret, 0) * 100) / 100;
  return `${gun}. gun TL ${toplam}`;
}

export { gunlukToplam };
```

Üç dosya birbirinden bağımsız yazılmış gibi duruyor ve öyle de olabilir: her biri farklı
bir zamanda, farklı bir kişi tarafından, o günkü alışkanlıkla yazılmıştır. Sonuç, aynı
paketin içinde üç ayrı yazım geleneğidir.

#### Tekilleştirilmiş Sürüm

İkinci sürümde her iş için tek bir biçim seçildi: tutar `toFixed(2)` ile yazılıp para
birimi bir boşlukla ekleniyor, bulunamama bir alan hatası atarak bildiriliyor, dışa açma
bildirimin başında yapılıyor.

```js
// tekil/ucret.mjs — ucret satiri
export function ucretSatiri(gonderi, tarife) {
  const kademe = tarife.kademeler.find((k) => gonderi.agirlikKg <= k.ustSinirKg);
  if (kademe === undefined) throw new RangeError("kademe bulunamadi");
  return `${gonderi.kod} ${(kademe.kiloBasiUcret * gonderi.agirlikKg).toFixed(2)} TL`;
}
```

```js
// tekil/etiket.mjs — etiket satiri
export function etiketSatiri(gonderi, bolgeAdlari) {
  const bolge = bolgeAdlari[gonderi.bolgeKodu];
  if (bolge === undefined) throw new RangeError("bolge bulunamadi");
  return `${gonderi.kod} ${bolge} ${gonderi.beyanDegeri.toFixed(2)} TL`;
}
```

```js
// tekil/rapor.mjs — gunluk toplam
export function gunlukToplam(gonderiler, gun) {
  const gunun = gonderiler.filter((g) => g.gun === gun);
  if (gunun.length === 0) throw new RangeError("o gune ait gonderi yok");
  return `${gun}. gun ${gunun.reduce((t, g) => t + g.ucret, 0).toFixed(2)} TL`;
}
```

#### Biçim Tarayıcı

Tarayıcı her iş için tanınacak biçimlerin örüntülerini taşır, dizindeki dosyalarda hangi
biçimlerin göründüğünü bulur ve iş başına kaç ayrı biçim kullanıldığını sayar.

```js
// bicim-tara.mjs — ayni isin kac ayri bicimde yapildigini sayar
import { readFileSync, readdirSync } from "node:fs";

const ISLER = {
  "para bicimi": [["toFixed + bosluk + TL", /toFixed\(2\)\} TL/], ["toFixed + bitisik TL", /toFixed\(2\)\}TL/],
    ["Math.round + onek TL", /Math\.round\(/]],
  "bulunamama": [["hata atma", /throw new \w*Error/], ["bos deger", /return null/],
    ["hata nesnesi", /return \{ hata:/]],
  "disa aktarma": [["export function", /^export function /m], ["export const ok", /^export const \w+ = \(/m],
    ["sonda export bloku", /^export \{/m]],
};

for (const dizin of process.argv.slice(2)) {
  const dosyalar = readdirSync(dizin).filter((a) => a.endsWith(".mjs")).sort();
  const metin = dosyalar.map((a) => [a, readFileSync(`${dizin}/${a}`, "utf8")]);
  console.log(dizin);
  let toplam = 0;
  for (const [is, bicimler] of Object.entries(ISLER)) {
    const bulunan = bicimler
      .map(([ad, oruntu]) => [ad, metin.filter(([, m]) => oruntu.test(m)).map(([d]) => d)])
      .filter(([, dosyalar]) => dosyalar.length > 0);
    toplam += bulunan.length;
    console.log(`  ${is.padEnd(14)} bicim=${bulunan.length}`);
    for (const [ad, d] of bulunan) console.log(`    ${ad.padEnd(22)} ${d.join(", ")}`);
  }
  console.log(`  toplam bicim=${toplam}  (is basina 1 hedefleniyor, hedef=${Object.keys(ISLER).length})`);
}
```

```sh
node bicim-tara.mjs karisik tekil
```

```
karisik
  para bicimi    bicim=3
    toFixed + bosluk + TL  ucret.mjs
    toFixed + bitisik TL   etiket.mjs
    Math.round + onek TL   rapor.mjs
  bulunamama     bicim=3
    hata atma              ucret.mjs
    bos deger              etiket.mjs
    hata nesnesi           rapor.mjs
  disa aktarma   bicim=3
    export function        ucret.mjs
    export const ok        etiket.mjs
    sonda export bloku     rapor.mjs
  toplam bicim=9  (is basina 1 hedefleniyor, hedef=3)
tekil
  para bicimi    bicim=1
    toFixed + bosluk + TL  etiket.mjs, rapor.mjs, ucret.mjs
  bulunamama     bicim=1
    hata atma              etiket.mjs, rapor.mjs, ucret.mjs
  disa aktarma   bicim=1
    export function        etiket.mjs, rapor.mjs, ucret.mjs
  toplam bicim=3  (is basina 1 hedefleniyor, hedef=3)
```

Toplam biçim sayısı dokuzdan üçe indi ve üç, iş sayısına eşit — yani her iş tek biçimde
yapılıyor. Dosya listeleri de tersine döndü: önce her biçimin tek bir dosyası vardı,
şimdi tek biçimin üç dosyası var.

#### Tekilleştirmenin Bedeli

Biçim birleştirmek serbest bir işlem değildir. Para biçimini birleştirmek çıktı metnini,
bulunamama biçimini birleştirmek hata davranışını değiştirir. Değişikliklerin sayılması
gerekir.

```js
// karsilastir.mjs — tekillestirmenin ciktilara ve bulunamama davranisina etkisi
import { ucretSatiri as kUcret } from "./karisik/ucret.mjs";
import { etiketSatiri as kEtiket } from "./karisik/etiket.mjs";
import { gunlukToplam as kToplam } from "./karisik/rapor.mjs";
import { ucretSatiri as tUcret } from "./tekil/ucret.mjs";
import { etiketSatiri as tEtiket } from "./tekil/etiket.mjs";
import { gunlukToplam as tToplam } from "./tekil/rapor.mjs";

const TARIFE = { kademeler: [{ ustSinirKg: 5, kiloBasiUcret: 26 }, { ustSinirKg: 30, kiloBasiUcret: 17 }] };
const BOLGELER = { 1: "Ayni Sehir", 2: "Komsu Bolge" };
const GONDERI = { kod: "GN-4172", agirlikKg: 2.4, bolgeKodu: 2, beyanDegeri: 1500, gun: 3, ucret: 62.4 };
const LISTE = [GONDERI, { ...GONDERI, kod: "GN-4173", ucret: 51.2 }];

const dene = (fn) => {
  try {
    const sonuc = fn();
    return typeof sonuc === "object" && sonuc !== null ? JSON.stringify(sonuc) : String(sonuc);
  } catch (hata) {
    return `${hata.constructor.name}: ${hata.message}`;
  }
};

const DURUMLAR = [
  ["ucret", () => kUcret(GONDERI, TARIFE), () => tUcret(GONDERI, TARIFE)],
  ["etiket", () => kEtiket(GONDERI, BOLGELER), () => tEtiket(GONDERI, BOLGELER)],
  ["toplam", () => kToplam(LISTE, 3), () => tToplam(LISTE, 3)],
  ["etiket/yok", () => kEtiket({ ...GONDERI, bolgeKodu: 9 }, BOLGELER),
    () => tEtiket({ ...GONDERI, bolgeKodu: 9 }, BOLGELER)],
  ["toplam/yok", () => kToplam(LISTE, 7), () => tToplam(LISTE, 7)],
];

for (const [ad, karisik, tekil] of DURUMLAR) {
  const a = dene(karisik), b = dene(tekil);
  console.log(`${ad.padEnd(11)} ${a === b ? "ayni  " : "degisti"} ${a.padEnd(34)} -> ${b}`);
}
```

```
ucret       ayni   GN-4172 62.40 TL                   -> GN-4172 62.40 TL
etiket      degisti GN-4172 Komsu Bolge 1500.00TL      -> GN-4172 Komsu Bolge 1500.00 TL
toplam      degisti 3. gun TL 113.6                    -> 3. gun 113.60 TL
etiket/yok  degisti null                               -> RangeError: bolge bulunamadi
toplam/yok  degisti {"hata":"o gune ait gonderi yok"}  -> RangeError: o gune ait gonderi yok
```

Beş durumun dördü değişti. Üçü sunum kararıdır: para birimi eki, ondalık hane sayısı ve
sıralama artık her yerde aynı. Biri davranış kararıdır: iki fonksiyon artık boş değer ya
da hata nesnesi yerine hata atıyor ve bunları çağıran her yer buna göre güncellenmelidir.

Bu, tutarlılık çalışmasının gerçek maliyetidir. Ölçüm işi mekanikleştirir ama kararı
vermez; tarayıcı kaç biçim olduğunu söyler, hangisinin standart olacağını söylemez.
`3. gun TL 113.6` satırının `3. gun 113.60 TL` olması bir seçimdir ve seçim yapıldıktan
sonra tek başına geri alınamaz.

#### Standardı Belirlemek

Standardın hangisi olacağına dair üç ölçüt işe yarar.

**Yaygınlık.** Kod tabanında zaten en çok kullanılan biçim, en az değişiklikle
standartlaştırılabilendir. Tarayıcının dosya listesi bu bilgiyi doğrudan veriyor.

**Dışarıya bakan yüzeyle uyum.** Bir biçim kullanıcıya ya da başka bir sisteme görünüyorsa,
onun beklentisi iç tercihten önce gelir. Para biçimi fatura çıktısında görünüyorsa
standardı belirleyen şey muhasebenin biçimidir.

**Yanlış kullanıma dirençlilik.** Bulunamama örneğinde hata atmak, boş değer döndürmekten
daha dirençlidir; önceki derslerden birinde ölçülmüştü — boş değer, hatayı üretildiği
yerden uzağa taşıyor.

Ölçütlerin hiçbiri "daha güzel"i içermez. Tutarlılık tartışmasının çıkmaza girdiği yer,
biçimlerin estetik olarak karşılaştırılmasıdır; oysa asıl kazanç biçimin **tek** olmasından
gelir, hangisi olduğundan değil. Karar verildikten sonra iş mekanikleşir: tarayıcı
kod tabanında çalıştırılır, biçim sayısı birden büyük olan her iş listelenir.

Tekilleştirilmiş sürümde bir şey hâlâ eksik. Üç dosya aynı para biçimini üç kez yazıyor;
biçim tek ama yazım üç yerde tekrarlanıyor. Bunlar ayrı sorunlardır: biri aynı işin farklı
biçimlerde yapılması, öteki aynı bilginin birden çok yerde durmasıdır.

#### Özet

- Tutarlılığın ölçüsü, bir işin kod tabanında kaç ayrı biçimde yapıldığıdır; hedef her
  zaman birdir, hangi biçim olduğu ikinci sorudur.
- Üç modüllü pakette para biçimi, bulunamama bildirimi ve dışa aktarma üçer biçimdeydi;
  toplam dokuz biçim üçe indi ve iş sayısına eşitlendi.
- Tekilleştirme bedelsiz değil: beş durumun dördü değişti; üçü sunum kararı, biri hata
  davranışı kararıydı ve çağıran her yeri ilgilendirir.
- Standardı seçerken yaygınlık, dışarıya bakan yüzeyle uyum ve yanlış kullanıma
  dirençlilik ölçütleri kullanılır; estetik karşılaştırma tartışmayı bitirmez.
- Aynı biçimin tek olması ile o biçimin tek yerde yazılması ayrı sorunlardır; ikincisi
  tekilleştirmeden sonra da yerinde duruyor.

#### Sonraki Adım

Bu konudaki kararların hepsi **yerel**di: bir adın ne söylediği, bir imzanın neyi kabul
ettiği, bir modülün neyi dışa açtığı, bir dosyanın kime hizmet ettiği, bir işin hangi
biçimde yapıldığı. Her biri tek bir dosyaya bakılarak verilebilir ve tek bir dosyaya
bakılarak denetlenebilir.

Kod tabanı büyüdükçe bu ölçeğe sığmayan bir maliyet doğar. Aynı bilgi birden çok yerde
durmaya başlar ve bir tanesi güncellendiğinde ötekiler geride kalır. Bugün gerekmeyen bir
esneklik bugünden kurulur ve gerekmediği ortaya çıkana kadar her değişikliğin üzerine
binen bir yük olur. Bir kararın değiştirilmesi tek bir dosyaya değil, kaç yere dokunduğu
önceden bilinmeyen bir kümeye yayılır. Sonraki konu bu üç maliyeti ele alıyor ve
yinelemenin türlerini ayırt etmekle başlıyor: aynı bilginin iki yerde durması ile iki
parçanın tesadüfen birbirine benzemesi aynı şey değildir.

## Sadelik İlkeleri

Bilgi yinelemesi ile tesadüfi benzerliğin ayrılması, tasarımın küçük adımlarla evrilmesi, erken genelleştirmenin ölçülen maliyeti, ayıklama–taşıma–yeniden adlandırma teknikleri ve davranış korumasının testle güvenceye alınması.

### Kendini Tekrar Etme

Source: https://academia.sh/tr/kurslar/temiz-kod/kendini-tekrar-etme

Aynı bilginin iki yerde durmasıyla tesadüfen benzeyen iki kodun ayrılması, ayrışan kademe sınırlarının sayılması ve iki çağrı yerini tek fonksiyonda birleştirmenin ürettiği yanlış soyutlamanın bayrak sayısıyla ölçülmesi.

Arayüz ve Veri Kararları konusu, aynı işin projede tek biçimde yapılmasıyla kapandı.
Buraya kadar verilen kararların ortak yanı **yerel** olmalarıydı: bir adın niyeti
açıklaması, bir imzanın boş değer geçirmemesi, bir fonksiyonun tek soyutlama düzeyinde
durması. Her biri tek bir dosyaya, çoğu zaman tek bir satıra bakarak denetlenebilirdi.

Kod tabanı büyüdükçe bu denetimin yetmediği bir maliyet doğar. Aynı bilgi birden çok
yerde durur ve bir gün biri güncellenip diğeri unutulur; bugün gerekmeyen bir esneklik
bugünden yazılır ve her okuyanın bedelini ödediği bir dallanmaya dönüşür; bir kararı
değiştirmek kaç dosyaya dokunmayı gerektirdiğiyle ölçülür hâle gelir. Bu konu üç ilkeyi
bu maliyet üzerinden ele alır. İlki, aynı bilginin tek bir yerde durmasını isteyen
**kendini tekrar etme** ilkesidir (don't repeat yourself).

İlkenin sık rastlanan okunuşu "aynı satırları iki kez yazma" biçimindedir ve yanlıştır.
Birbirinden ayrılması gereken iki durum vardır: aynı **bilginin** iki yerde durması ile
şu an tesadüfen birbirine **benzeyen** iki kod.

#### Bilgi Yinelemesi

Gönderi ücretlendirme ve yönlendirme kitaplığında ağırlık kademeleri iki modülde
geçiyor. Birincisi taban ücreti hesaplıyor.

```js
// ucret.mjs — agirlik kademesine gore taban ucret (kurus)
const KADEME = [
  { ustGram: 1000, kurus: 4990 },
  { ustGram: 5000, kurus: 7490 },
  { ustGram: 20000, kurus: 12900 },
  { ustGram: Infinity, kurus: 24900 },
];

export function agirlikUcreti(gram) {
  return KADEME.find((k) => gram <= k.ustGram).kurus;
}
```

İkincisi operasyon ekranında gönderinin hangi kademede olduğunu gösteriyor.

```js
// ozet.mjs — operasyon ekranindaki kademe rozeti
const KADEME = [
  { ustGram: 1000, ad: 'kucuk' },
  { ustGram: 5000, ad: 'orta' },
  { ustGram: 20000, ad: 'buyuk' },
  { ustGram: Infinity, ad: 'agir' },
];

export function kademeAdi(gram) {
  return KADEME.find((k) => gram <= k.ustGram).ad;
}
```

İki tablo aynı görünmüyor: birinde kuruş, diğerinde ad var. Yinelenen şey kod değil,
**kademe sınırlarının nerede olduğu bilgisidir**. Bu bilginin tek olup olmadığı
ölçülebilir: iki fonksiyonun çıktısının değiştiği gram değerleri karşılaştırılır.

```js
// sinir.mjs — iki dosyanin kademe sinirlarini karsilastirir
import { agirlikUcreti } from './ucret.mjs';
import { kademeAdi } from './ozet.mjs';

function sinirlar(f) {
  const s = [];
  for (let g = 2; g <= 25000; g++) if (f(g) !== f(g - 1)) s.push(g);
  return s;
}

const ucret = sinirlar(agirlikUcreti);
const rozet = sinirlar(kademeAdi);
const ayrisan = ucret.filter((g) => !rozet.includes(g)).length
  + rozet.filter((g) => !ucret.includes(g)).length;

console.log('ucret sinirlari:', ucret.join(' '));
console.log('rozet sinirlari:', rozet.join(' '));
console.log('ayrisan sinir sayisi:', ayrisan);
```

```
ucret sinirlari: 1001 5001 20001
rozet sinirlari: 1001 5001 20001
ayrisan sinir sayisi: 0
```

Bugün sıfır. Yinelemenin maliyeti bugünde değil, ilk değişikliktedir. Tarifeye 10
kilogramlık bir ara kademe eklendiğini, değişikliği yapan kişinin ücret dosyasını bulup
rozet dosyasını görmediğini varsayalım.

```js
// ucret.mjs — 10 kg kademesi eklendi
const KADEME = [
  { ustGram: 1000, kurus: 4990 },
  { ustGram: 5000, kurus: 7490 },
  { ustGram: 10000, kurus: 9900 },
  { ustGram: 20000, kurus: 12900 },
  { ustGram: Infinity, kurus: 24900 },
];

export function agirlikUcreti(gram) {
  return KADEME.find((k) => gram <= k.ustGram).kurus;
}
```

```sh
node sinir.mjs
```

```
ucret sinirlari: 1001 5001 10001 20001
rozet sinirlari: 1001 5001 20001
ayrisan sinir sayisi: 1
```

Ayrışan sınır sayısı bire çıktı. Bunun alan karşılığı şudur: 5.001 ile 10.000 gram
arasındaki her gönderi için müşteriye 99,00 TL'lik kademe ücreti yansıtılırken operasyon
ekranı gönderiyi "buyuk" rozetiyle gösteriyor. İki modül aynı gönderi hakkında farklı
şey söylüyor ve hiçbir test kırılmadı, çünkü her modülün testi kendi tablosuna göre
yazılmıştı.

Çözüm, bilgiyi tek bir yere taşımaktır.

```js
// tarife.mjs — kademe bilgisinin tek kaynagi
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

export function kademe(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}
```

```js
// ucret.mjs — tarifeden okur
import { kademe } from './tarife.mjs';

export function agirlikUcreti(gram) {
  return kademe(gram).kurus;
}
```

```js
// ozet.mjs — ayni tarifeden okur
import { kademe } from './tarife.mjs';

export function kademeAdi(gram) {
  return kademe(gram).ad;
}
```

```sh
node sinir.mjs
```

```
ucret sinirlari: 1001 5001 10001 20001
rozet sinirlari: 1001 5001 10001 20001
ayrisan sinir sayisi: 0
```

Kazanılan şey satır sayısı değil. Kademe sınırlarını değiştirmek için dokunulan dosya
sayısı ikiden bire indi ve ayrışma imkânı ortadan kalktı: iki modül aynı satırı
okuduğu için tutarsız olamazlar.

#### Tesadüfi Benzerlik

İlkenin ikinci yüzü, birbirine benzeyen ama aynı bilgiyi taşımayan kodlardır. Kitaplıkta
iki belge satırı üretiliyor: birincisi pakete yapıştırılan etiket, ikincisi muhasebeye
giden fatura satırı.

```js
// belge.mjs — surum 0: iki cagri yeri, bugun ayni metin
export function gonderiEtiketi(g) {
  return `${g.no} ${g.bolge} ${(g.kurus / 100).toFixed(2)} TL`;
}

export function faturaSatiri(g) {
  return `${g.no} ${g.bolge} ${(g.kurus / 100).toFixed(2)} TL`;
}
```

İki gövde karakter karakter aynı. Kendini tekrar etme ilkesinin yüzeysel okunuşu bunları
birleştirmeyi söyler. Oysa iki satırın aynı olması bir **bilgi** değil, bir **rastlantı**:
etiketin biçimini depo operasyonu, fatura satırının biçimini muhasebe belirliyor ve bu
iki merci birbirinden habersiz karar veriyor. Kodun Aktöre Göre Düzenlenmesi dersindeki
ölçüt burada geçerlidir: iki parçanın değişme nedenleri farklıysa aynı gövdede
durmamalıdırlar.

Birleştirmenin ne ürettiğini görmek için birleştirilmiş sürümü yazmak gerekir. İlk iki
farklılaşma talebi geldiğinde ortak fonksiyon şu hâli alıyor.

```js
// belge.mjs — surum 1: iki cagri yeri tek fonksiyonda birlestirildi
export function belgeSatiri(g, { kdvEkle = false, rotaGoster = false, isoParaBirimi = false } = {}) {
  const tutar = kdvEkle ? Math.round(g.kurus * 1.2) : g.kurus;
  const alan = rotaGoster ? g.rota : g.bolge;
  const birim = isoParaBirimi ? 'TRY' : 'TL';
  return `${g.no} ${alan} ${(tutar / 100).toFixed(2)} ${birim}`;
}

export const gonderiEtiketi = (g) => belgeSatiri(g, { rotaGoster: true });
export const faturaSatiri = (g) => belgeSatiri(g, { kdvEkle: true, isoParaBirimi: true });
```

Bu, Arayüz ve Veri Kararları konusunda bölünmesi öğütlenen mantıksal bayrak
parametrelerinin üç tanesinin tek imzada toplanmış hâlidir. Bayrak uzayının ne kadarının
gerçekten kullanıldığı sayılabilir.

```js
// yollar.mjs — bayrak uzayi ile gercekten kullanilan yollari karsilastirir
import { belgeSatiri } from './belge.mjs';

const bayraklar = ['kdvEkle', 'rotaGoster', 'isoParaBirimi'];
const gonderi = { no: 'G-1042', bolge: 'B2', rota: 'IST-ANK', kurus: 7490 };

const tumu = new Set();
for (let m = 0; m < 2 ** bayraklar.length; m++) {
  const secenek = Object.fromEntries(bayraklar.map((b, i) => [b, Boolean(m & (1 << i))]));
  tumu.add(belgeSatiri(gonderi, secenek));
}
const kullanilan = new Set([
  belgeSatiri(gonderi, { rotaGoster: true }),
  belgeSatiri(gonderi, { kdvEkle: true, isoParaBirimi: true }),
]);

console.log('bayrak sayisi:', bayraklar.length);
console.log('kombinasyon sayisi:', tumu.size);
console.log('cagri yerlerinin kullandigi:', kullanilan.size);
console.log('hic calismayan kombinasyon:', tumu.size - kullanilan.size);
```

```
bayrak sayisi: 3
kombinasyon sayisi: 8
cagri yerlerinin kullandigi: 2
hic calismayan kombinasyon: 6
```

Sekiz kombinasyondan altısı hiçbir zaman çalışmıyor. Bu altı yol yine de okunuyor, yine
de bakımı yapılıyor ve fonksiyonu okuyan herkes bunların mümkün olduğunu varsaymak
zorunda kalıyor. **Yanlış soyutlama** budur: gövdesi ortak, sözleşmesi ortak olmayan bir
fonksiyon.

#### Yanlış Soyutlamanın Ölçülen Bedeli

Asıl bedel, bayrak sayısında değil, iki çağrı yerinin birbirini kısıtlamasında görünür.
İki çağrı yerinin sözleşmesi testlerle sabitlenmiş durumda.

```js
// belge.test.mjs — iki cagri yerinin sozlesmesi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiEtiketi, faturaSatiri } from './belge.mjs';

const gonderi = { no: 'G-1042', bolge: 'B2', rota: 'IST-ANK', kurus: 7490 };

test('etiket rota kodunu ve KDVsiz tutari yazar', () => {
  assert.equal(gonderiEtiketi(gonderi), 'G-1042 IST-ANK 74.90 TL');
});

test('fatura satiri bolgeyi ve KDVli tutari ISO birimiyle yazar', () => {
  assert.equal(faturaSatiri(gonderi), 'G-1042 B2 89.88 TRY');
});
```

```sh
node --test --test-reporter=tap belge.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - etiket rota kodunu ve KDVsiz tutari yazar
ok 2 - fatura satiri bolgeyi ve KDVli tutari ISO birimiyle yazar
# tests 2
# pass 2
# fail 0
```

Muhasebe yeni bir kural bildiriyor: fatura tutarı tam TL'ye yuvarlanacak. Bu, yalnız
fatura satırını ilgilendiren bir karar. Değişiklik ortak fonksiyondaki tutar hesabına
yazılıyor.

```js
// belge.mjs — surum 2: tam TL yuvarlama ortak yere yazildi
export function belgeSatiri(g, { kdvEkle = false, rotaGoster = false, isoParaBirimi = false } = {}) {
  const ham = kdvEkle ? Math.round(g.kurus * 1.2) : g.kurus;
  const tutar = Math.round(ham / 100) * 100;
  const alan = rotaGoster ? g.rota : g.bolge;
  const birim = isoParaBirimi ? 'TRY' : 'TL';
  return `${g.no} ${alan} ${(tutar / 100).toFixed(2)} ${birim}`;
}

export const gonderiEtiketi = (g) => belgeSatiri(g, { rotaGoster: true });
export const faturaSatiri = (g) => belgeSatiri(g, { kdvEkle: true, isoParaBirimi: true });
```

Fatura testinin beklentisi yeni kurala göre güncelleniyor; etiket testi olduğu gibi
bırakılıyor, çünkü etiket hakkında hiçbir karar değişmedi.

```js
// belge.test.mjs — fatura beklentisi yeni gereksinime gore guncellendi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiEtiketi, faturaSatiri } from './belge.mjs';

const gonderi = { no: 'G-1042', bolge: 'B2', rota: 'IST-ANK', kurus: 7490 };

test('etiket rota kodunu ve KDVsiz tutari yazar', () => {
  assert.equal(gonderiEtiketi(gonderi), 'G-1042 IST-ANK 74.90 TL');
});

test('fatura satiri tam TL yuvarlanmis tutari yazar', () => {
  assert.equal(faturaSatiri(gonderi), 'G-1042 B2 90.00 TRY');
});
```

```sh
node --test --test-reporter=tap belge.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - etiket rota kodunu ve KDVsiz tutari yazar
ok 2 - fatura satiri tam TL yuvarlanmis tutari yazar
# tests 2
# pass 1
# fail 1
```

Muhasebeye ait bir karar, depo operasyonuna ait bir çıktıyı bozdu. Etiketteki tutar
74,90 TL yerine 75,00 TL yazılmaya başladı. Kısıtlama sayısal olarak şudur: yuvarlamanın
yalnız faturaya uygulanması için dördüncü bir bayrak gerekir, bayrak sayısı üçten dörde,
kombinasyon sayısı sekizden on altıya çıkar, gerçekten kullanılan yol sayısı iki
kalır. Her farklılaşma talebi kombinasyon uzayını iki katına çıkarır ve kullanılan yol
sayısını sabit bırakır.

#### Ayırmanın Maliyeti

Doğru hamle birleştirmeyi geri almaktır. İki gövde, ait oldukları kararla birlikte
yeniden ayrılır.

```js
// belge.mjs — surum 3: benzerlik tesadufiydi, iki fonksiyon ayrildi
export function gonderiEtiketi(g) {
  return `${g.no} ${g.rota} ${(g.kurus / 100).toFixed(2)} TL`;
}

export function faturaSatiri(g) {
  const kdvli = Math.round(g.kurus * 1.2);
  const tamTl = Math.round(kdvli / 100) * 100;
  return `${g.no} ${g.bolge} ${(tamTl / 100).toFixed(2)} TRY`;
}
```

```sh
node --test --test-reporter=tap belge.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'ucluk isleci sayisi: %s\n' "$(grep -c ' ? ' belge.mjs)"
```

```
ok 1 - etiket rota kodunu ve KDVsiz tutari yazar
ok 2 - fatura satiri tam TL yuvarlanmis tutari yazar
# tests 2
# pass 2
# fail 0
ucluk isleci sayisi: 0
```

Bayrak sayısı üçten sıfıra, dallanma sayısı üçten sıfıra indi. Yinelenen görünen tek
şey `g.no` ve `toFixed(2)` çağrısı; ikisi de bilgi değil, dilin sözdizimi. İki
fonksiyonun bundan sonra ayrı ayrı değişmesi, birinin diğerini bozma ihtimali olmadan
mümkün.

İki durumu ayıran ölçüt tek bir sorudur: iki parçadan biri değiştiğinde diğerinin de
değişmesi **zorunlu** mu? Kademe sınırlarında zorunluydu; sınır bir yerde kaydığında
diğer yerde de kaymalıydı, kaymadığı için sistem tutarsızlaştı. Belge satırlarında
zorunlu değildi; muhasebe kararı depo çıktısını değiştirmemeliydi, değiştirdiği için
test kırıldı. Yineleme sorusu satırlara değil, **değişim nedenlerine** bakılarak
yanıtlanır.

#### Özet

- Kendini tekrar etme ilkesi satır benzerliğiyle değil bilginin tekliğiyle ilgilidir;
  aynı bilginin iki yerde durması ile tesadüfen benzeyen iki kod ayrı durumlardır.
- Yinelenen bilginin ayrışması ölçülebilir: kademe tablosu iki dosyada tutulduğunda tek
  taraflı bir değişiklik ayrışan sınır sayısını sıfırdan bire çıkardı ve hiçbir test
  kırılmadı.
- Bilgiyi tek kaynağa taşımak dokunulan dosya sayısını ikiden bire indirdi ve iki
  modülün tutarsızlaşma imkânını kaldırdı.
- Tesadüfen benzeyen iki gövdeyi birleştirmek yanlış soyutlama üretir: üç bayrak, sekiz
  kombinasyon ve gerçekten kullanılan iki yol.
- Yanlış soyutlamanın bedeli çağrı yerlerinin birbirini kısıtlamasıdır; muhasebeye ait
  bir yuvarlama kararı etiket testini kırdı ve ayrımı korumak dördüncü bir bayrak
  gerektirdi.
- Ayırma kararı tek soruyla verilir: biri değiştiğinde diğerinin değişmesi zorunlu mu?

#### Sonraki Adım

Bu derste iki değişiklik yapıldı ve ikisi de küçüktü: bir tablo tek dosyaya taşındı, bir
fonksiyon ikiye ayrıldı. Adımların küçük olması rastlantı değil, bir yöntem. Sonraki
ders aynı gereksinimi iki farklı biçimde karşılar — tek seferde tasarlanmış bir çözümle
ve küçük adımlarla evrilen bir çözümle — ve her adımda dokunulan satır sayısı ile
testlerin yeşil kaldığı adım sayısı karşılaştırılır.

### Basit Tut ve Sık Yeniden Düzenle

Source: https://academia.sh/tr/kurslar/temiz-kod/basit-tut-ve-sik-yeniden-duzenle

Aynı ücretlendirme gereksiniminin tek adımda tasarlanmış bir çözümle ve beş küçük adımla evrilen bir çözümle karşılanması, her adımda değişen satır sayısının ölçülmesi ve kırmızı bir testin işaret ettiği şüpheli bölgenin karşılaştırılması.

Önceki derste iki değişiklik yapıldı: bir tablo tek dosyaya taşındı, bir fonksiyon ikiye
ayrıldı. İkisi de küçüktü ve ikisi de mevcut davranışı korudu. Bu ders, adımın küçük
olmasının neyi ucuzlattığını sayıyla gösterir.

İlkenin adı **basit tut** (keep it simple): bir gereksinim, onu karşılayan en az yapıyla
karşılanır; eksik kalan yapı sonradan, gerçekten gerektiğinde eklenir. İlkeye ikinci bir
cümle eşlik eder — **sık yeniden düzenle**. Az yapıyla ilerlemek, biriken yapıyı düzenleme
işini erteleme hakkı vermez; düzenleme her adımın parçasıdır.

Test odaklı geliştirme konusunda adım boyu **geri bildirim süresi** açısından ele
alınmıştı: kırmızıdan yeşile geçen turun uzunluğu ölçülmüştü. Burada aynı büyüklük başka
bir açıdan ölçülür: bir test kırmızıya döndüğünde hatanın **hangi satır kümesinde**
arandığı.

#### Gereksinim

Gönderi ücretlendirme kitaplığına dört kurallı bir ücret hesabı ekleniyor.

1. Taban ücret, ağırlık kademesinden okunur.
2. Taban ücret gönderim bölgesinin katsayısıyla çarpılır.
3. Sonuç asgari ücretin altına düşemez.
4. Hacim ağırlığı gerçek ağırlıktan büyükse kademe hacim ağırlığından okunur.

Kademe tablosu önceki derste kurulmuştu ve değişmeden kullanılıyor.

```js
// tarife.mjs — onceki dersten devralinan kademe tablosu
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

export function kademe(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}
```

#### Tek Adımda Tam Tasarım

Dört kural birden okunduğunda akla gelen ilk çözüm hepsini tek yazımda karşılamaktır.

```js
// ucret.mjs — tek adimda yazilmis tam surum
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function gonderiUcreti(g) {
  const hacimGram = Math.ceil((g.en * g.boy * g.yukseklik) / 3);
  const etkinGram = Math.max(g.gram, hacimGram);
  const taban = kademe(etkinGram).kurus;
  const asgarili = Math.max(taban, ASGARI_KURUS);
  return Math.round(asgarili * BOLGE_KATSAYI[g.bolge]);
}
```

Dört kuralın dördü de birer testle sabitleniyor.

```js
// ucret.test.mjs — dort kuralin tamami
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'ucret.mjs satir sayisi: %s\n' "$(wc -l < ucret.mjs | tr -d ' ')"
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
not ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
# tests 4
# pass 3
# fail 1
ucret.mjs satir sayisi: 13
```

Üçüncü test kırmızı: asgari ücret bölge çarpımından önce uygulanmış, dolayısıyla asgari
tutar da bölge katsayısıyla çarpılmış. Hatanın kendisi tek satırlık, ama koşumun işaret
ettiği şüpheli bölge dosyanın tamamı: dört kural aynı yazımda eklendiği için testin
hangi kural yüzünden düştüğü koşumdan okunamaz. Şüpheli satır sayısı 13.

#### Aynı Gereksinim, Beş Adım

Aynı dört kural, her biri kendi testiyle kapanan adımlara bölünüyor. İlk adım yalnız
birinci kuralı karşılıyor.

```js
// ucret.mjs — adim 1: yalniz agirlik kademesi
import { kademe } from './tarife.mjs';

export function gonderiUcreti(g) {
  return kademe(g.gram).kurus;
}
```

```sh
: > onceki.mjs
cat > ucret.test.mjs <<'SON'
// ucret.test.mjs — adim 1
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});
SON
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
cp ucret.mjs onceki.mjs
```

```
ok 1 - kademe ucreti agirliktan okunur
# tests 1
# pass 1
# fail 0
bu adimda degisen satir: 6
```

İkinci adım bölge katsayısını ekliyor. Değişen tek şey çarpım.

```js
// ucret.mjs — adim 2: bolge katsayisi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

export function gonderiUcreti(g) {
  return Math.round(kademe(g.gram).kurus * BOLGE_KATSAYI[g.bolge]);
}
```

```sh
cat >> ucret.test.mjs <<'SON'

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});
SON
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
cp ucret.mjs onceki.mjs
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
# tests 2
# pass 2
# fail 0
bu adimda degisen satir: 6
```

Üçüncü adım asgari ücreti ekliyor ve bu adımda tam olarak tek adımlık tasarımdaki karar
verilmiş oluyor: asgari ücret çarpımdan önce mi sonra mı uygulanacak? İlk deneme öncekini
seçiyor.

```js
// ucret.mjs — adim 3: asgari ucret (ilk deneme)
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function gonderiUcreti(g) {
  const taban = Math.max(kademe(g.gram).kurus, ASGARI_KURUS);
  return Math.round(taban * BOLGE_KATSAYI[g.bolge]);
}
```

```sh
cat >> ucret.test.mjs <<'SON'

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});
SON
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
not ok 3 - asgari ucret bolge carpimindan sonra uygulanir
# tests 3
# pass 2
# fail 1
bu adimda degisen satir: 6
```

Kırmızıya dönen test aynı, hata aynı. Değişen şey aramanın nerede yapılacağı: bu adımda
değişen satır sayısı 6. Tek adımlık tasarımda aynı kırmızı, 13 satırlık bir dosyanın
tamamını şüpheli bırakıyordu ve o 13 satır içinde geçen dört kural birbirine karışmış
durumdaydı. Burada şüpheli bölge yalnız asgari ücret satırlarıdır.

```js
// ucret.mjs — adim 3: asgari ucret carpimdan sonra
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(g.gram).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
cp ucret.mjs onceki.mjs
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
# tests 3
# pass 3
# fail 0
bu adimda degisen satir: 6
```

Dördüncü adım hacim ağırlığını ekliyor.

```js
// ucret.mjs — adim 4: hacim agirligi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function gonderiUcreti(g) {
  const hacimGram = Math.ceil((g.en * g.boy * g.yukseklik) / 3);
  const etkinGram = Math.max(g.gram, hacimGram);
  const bolgeli = Math.round(kademe(etkinGram).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}
```

```sh
cat >> ucret.test.mjs <<'SON'

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});
SON
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
printf 'gonderiUcreti govdesi: %s satir\n' "$(awk '/^export function gonderiUcreti/,/^}/' ucret.mjs | wc -l | tr -d ' ')"
cp ucret.mjs onceki.mjs
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
# tests 4
# pass 4
# fail 0
bu adimda degisen satir: 6
gonderiUcreti govdesi: 6 satir
```

#### Beşinci Adım: Yeniden Düzenleme

Dört kural karşılandı ve testler yeşil. İlkenin ikinci yarısı burada devreye girer:
biriken yapı, bir sonraki gereksinim gelmeden önce düzenlenir. Ücret fonksiyonu iki ayrı
soyutlama düzeyini taşıyor — ağırlığın nasıl belirlendiği ile ücretin nasıl hesaplandığı.
Ağırlık belirleme kendi adını alıyor.

```js
// ucret.mjs — adim 5: etkin agirlik ayiklandi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
printf 'bu adimda degisen satir: %s\n' "$(diff onceki.mjs ucret.mjs | grep -c '^[<>]')"
printf 'gonderiUcreti govdesi: %s satir\n' "$(awk '/^export function gonderiUcreti/,/^}/' ucret.mjs | wc -l | tr -d ' ')"
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
# tests 4
# pass 4
# fail 0
bu adimda degisen satir: 10
gonderiUcreti govdesi: 4 satir
```

Bu adımda hiçbir kural eklenmedi, hiçbir test değişmedi ve değişen satır sayısı önceki
adımlardan büyük çıktı. Yeniden düzenleme adımlarında bu olağandır: ölçü değişen satır
sayısı değil, koşumun yeşil kalmasıdır. Ücret fonksiyonunun gövdesi altı satırdan dört
satıra indi ve ağırlık belirleme kuralı artık kendi adıyla ayrıca sınanabilir hâle geldi.

#### Adım Boyunun Ölçüsü

İki yol aynı dosyada bitti; ölçülen büyüklükler farklı.

| Ölçüt | Tek adım | Beş adım |
|---|---|---|
| Koşum sayısı | 1 | 6 |
| Yeşil biten koşum | 0 | 5 |
| Kırmızıda şüpheli satır | 13 | 6 |
| Bir adımda değişen satır | 13 | 6 (yeniden düzenlemede 10) |

Tek adımlık yolun tek koşumu kırmızı bitti ve hatanın hangi kuralda olduğu koşumdan
okunamadı; kırmızı olan test asgari ücret testiydi ama asgari ücret satırı, hacim ağırlığı
ve bölge katsayısı satırlarıyla aynı yazımda eklenmişti. Beş adımlık yolda kırmızı tek bir
kuralın eklendiği adımda göründü.

Ölçünün alan karşılığı şudur: bir gereksinim ne kadar çok kural içeriyorsa, hepsini tek
yazımda karşılamanın maliyeti kural sayısıyla değil, kuralların **birbirine göre sıraları**
sayısıyla artar. Dört kuralın uygulanma sırası için birden fazla seçenek vardır ve tek
yazımda bunların hepsi aynı anda seçilir. Adımlara bölmek bu seçimleri birer birer yapılır
hâle getirir.

Basit tut ilkesinin sık yapılan yanlış okunuşu, "yapıyı hiç kurma" biçimindedir. Beşinci
adım bunun tersini gösterir: yapı kurulur, ama gereksinim geldikten **sonra** ve testler
yeşilken kurulur. İlkenin yasakladığı şey yapı değil, gereksinimin önüne geçen yapıdır.

#### Özet

- Basit tut ilkesi bir gereksinimi karşılayan en az yapıyla ilerlemeyi, sık yeniden
  düzenleme ise biriken yapıyı her adımın sonunda düzenlemeyi söyler.
- Aynı dört kural tek yazımda karşılandığında tek koşum kırmızı bitti ve şüpheli bölge 13
  satırdı; beş adıma bölündüğünde aynı hata 6 satırlık bir adımda göründü.
- Adımlara bölmek kuralların uygulanma sırasına ilişkin kararları birer birer alınır hâle
  getirir; tek yazımda bu kararların hepsi aynı anda ve gerekçesiz alınır.
- Yeniden düzenleme adımında değişen satır sayısı büyüyebilir; o adımın ölçüsü satır
  sayısı değil, testlerin yeşil kalmasıdır.
- İlke yapıyı yasaklamaz; gereksinimin önüne geçen yapıyı yasaklar.

#### Sonraki Adım

Bu derste her adım, gelmiş bir gereksinime karşılık geldi. Gelmemiş bir gereksinim için
yazılan yapının ne kadara mal olduğu ölçülmedi. Sonraki ders ücret hesabına bugünden bir
eklenti noktası ve bir yapılandırma katmanı ekler, bunların kaç dallanma ve kaç
çalışmayan yol ürettiğini sayar, sonra gerçekten gelen gereksinimin bu yapıya uymadığını
gösterir.

### İhtiyacın Olmayacak

Source: https://academia.sh/tr/kurslar/temiz-kod/ihtiyacin-olmayacak

Ücret hesabına bugünden eklenen bir eklenti noktasıyla yapılandırma katmanının karar noktası, dal kapsamı ve çalışmayan yol sayısıyla ölçülmesi; gelen gereksinimin bu yapıya uymadığının başarısız koşumla gösterilmesi ve genelleştirmenin atılması.

Önceki derste her adım gelmiş bir gereksinime karşılık geldi ve dört kural beş adımda
karşılandı. Gelmemiş bir gereksinim için bugünden yazılan yapının ne kadara mal olduğu
ölçülmedi. Bu ders ücret hesabına bugünden bir eklenti noktası ile bir yapılandırma
katmanı ekler, ikisinin ürettiği karar noktalarını ve hiç çalışmayan yolları sayar, sonra
gerçekten gelen gereksinimin bu yapıya uymadığını gösterir.

İlkenin adı **ihtiyacın olmayacak** (you aren't gonna need it): bir yetenek, ona duyulan
gereksinim ortaya çıkana kadar yazılmaz. İlke bir tahmin yasağıdır — yazılım geliştiricinin
gelecekteki gereksinimi doğru tahmin edemeyeceğini değil, tahminin **yanlış çıkma
maliyetinin** ölçülebilir olduğunu söyler.

Başlangıç noktası önceki dersin son adımıdır.

```js
// tarife.mjs — kademe bilgisinin tek kaynagi
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

export function kademe(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}
```

```js
// ucret.mjs — onceki dersin son adimindan devralindi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}
```

```js
// ucret.test.mjs — onceki dersten devralinan dort test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});
```

Ölçü için bir sayaç gerekiyor: bir dosyadaki **karar noktalarını**, yani programın iki
ayrı yoldan birini seçtiği yerleri sayan bir betik.

```js
// dal.mjs — verilen dosyalardaki karar noktalarini sayar
import { readFileSync } from 'node:fs';

const ORUNTU = /\bif\b|\bcase\b|\?\?|&&|\|\||\?\.|\?(?![?.])/g;

let toplam = 0;
for (const yol of process.argv.slice(2)) {
  const n = (readFileSync(yol, 'utf8').match(ORUNTU) ?? []).length;
  console.log(`${yol}: ${n} karar noktasi`);
  toplam += n;
}
console.log(`toplam: ${toplam}`);
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
# tests 4
# pass 4
# fail 0
ucret.mjs: 0 karar noktasi
toplam: 0
```

#### Bugünden Yazılan İki Esneklik

Tarife kuralları zamanla değişecek. Bu doğru bir gözlem ve yanlış bir gerekçe: değişimin
geleceğini bilmek, hangi biçimde geleceğini bilmek anlamına gelmez. Yine de iki yapı
bugünden yazılıyor. Birincisi, hesabın her ayrıntısını dışarıdan seçilebilir kılan bir
yapılandırma katmanı.

```js
// yapilandirma.mjs — bugunden yazilan yapilandirma katmani
export const VARSAYILAN = {
  hacimUygula: true,
  hacimBolen: 3,
  asgariUygula: true,
  asgariKurus: 6990,
  yuvarlama: 'yakin',
  katsayiKaynagi: 'sabit',
};

export function yapilandir(secenek = {}) {
  return { ...VARSAYILAN, ...secenek };
}
```

İkincisi, ileride gelecek ücret kurallarının hesaba dışarıdan takılmasını sağlayan bir
**eklenti noktası** (extension point).

```js
// eklenti.mjs — ucret kurallari icin eklenti noktasi
const kurallar = [];

export function kuralEkle(ad, oncelik, uygula) {
  kurallar.push({ ad, oncelik, uygula });
  kurallar.sort((a, b) => a.oncelik - b.oncelik);
}

export function kurallariSifirla() {
  kurallar.length = 0;
}

export function kurallariUygula(kurus, g, yap) {
  let sonuc = kurus;
  for (const k of kurallar) {
    sonuc = k.uygula(sonuc, g, yap);
  }
  return sonuc;
}
```

Ücret hesabı ikisini de kullanacak biçimde yeniden yazılıyor. Davranış aynı kalıyor:
varsayılan yapılandırmayla ve kayıtlı kural olmadan sonuçlar değişmez.

```js
// ucret.mjs — bugunden genellestirilmis surum
import { kademe } from './tarife.mjs';
import { yapilandir } from './yapilandirma.mjs';
import { kurallariUygula } from './eklenti.mjs';

const SABIT_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

function katsayi(g, yap) {
  if (yap.katsayiKaynagi === 'dis') return yap.katsayiTablosu?.[g.bolge] ?? 1;
  return SABIT_KATSAYI[g.bolge] ?? 1;
}

function yuvarla(kurus, yap) {
  if (yap.yuvarlama === 'yok') return kurus;
  if (yap.yuvarlama === 'yukari') return Math.ceil(kurus);
  return Math.round(kurus);
}

export function etkinAgirlik(g, yap) {
  if (!yap.hacimUygula) return g.gram;
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / yap.hacimBolen));
}

export function gonderiUcreti(g, secenek = {}) {
  const yap = yapilandir(secenek);
  const bolgeli = yuvarla(kademe(etkinAgirlik(g, yap)).kurus * katsayi(g, yap), yap);
  const asgarili = yap.asgariUygula ? Math.max(bolgeli, yap.asgariKurus) : bolgeli;
  return kurallariUygula(asgarili, g, yap);
}
```

#### Ödenen Bedelin Ölçülmesi

Testler değişmedi ve yeşil kalıyor. Değişen şey testlerin **kapsayabildiği** yapının
büyüklüğü. Karar noktaları ile birlikte, koşumun gerçekten hangi yolları geçtiğini veren
dal kapsamı da ölçülüyor.

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs yapilandirma.mjs eklenti.mjs
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
# tests 4
# pass 4
# fail 0
ucret.mjs: 8 karar noktasi
yapilandirma.mjs: 0 karar noktasi
eklenti.mjs: 0 karar noktasi
toplam: 8
# start of coverage report
# -----------------------------------------------------------------
# file             | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------------
# eklenti.mjs      |  63.16 |    66.67 |   33.33 | 5-7 10-11 16-17
# tarife.mjs       | 100.00 |   100.00 |  100.00 | 
# ucret.mjs        | 100.00 |    45.45 |  100.00 | 
# yapilandirma.mjs | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------------
# all files        |  90.41 |    63.16 |   80.00 | 
# -----------------------------------------------------------------
# end of coverage report
```

Üç sayı okunmaya değer. Karar noktası sayısı sıfırdan sekize çıktı; bunların hiçbiri bir
alan kuralından gelmiyor, hepsi bir seçeneğin hangi değerde olduğunu sınıyor. Ücret
dosyasının satır kapsamı yüzde 100 ama dal kapsamı yüzde 45,45: her satır çalışıyor,
yolların yarısından fazlası çalışmıyor. Eklenti dosyasında `kuralEkle` ve `kurallariSifirla`
hiç çağrılmadığı için işlev kapsamı yüzde 33,33'te kalıyor ve kapsanmayan satırlar tek tek
listeleniyor.

Bu yolların bakımsız kalmasının bir yolu yok: dal kapsamını yüzde 100'e çıkarmak, altı ek
karar noktasının her biri için en az bir test daha yazmak demek. Bu testlerin sınayacağı
davranışı bugün hiçbir çağrı yeri istemiyor. Yapılandırma katmanı altı alan taşıyor ve
çağrı yerlerinin geçtiği seçenek sayısı sıfır; eklenti listesine kayıtlı kural sayısı da
sıfır.

#### Gelen Gereksinim

Sözleşmeli müşteriler için bir kural geliyor: bir fatura döneminde elliden çok gönderisi
olan müşterinin **o dönemdeki bütün gönderilerine** yüzde on iki indirim uygulanacak.
Eklenti noktası tam bu tür kurallar için yazılmıştı; kural oraya takılmayı deniyor.

```js
// donem.mjs — donem indirimini eklenti noktasi uzerinden yazma denemesi
import { gonderiUcreti } from './ucret.mjs';
import { kuralEkle, kurallariSifirla } from './eklenti.mjs';

const ESIK = 50;
const ORAN = 0.88;

export function donemUcreti(gonderiler) {
  kurallariSifirla();
  let sayac = 0;
  kuralEkle('donem-indirimi', 10, (kurus) => {
    sayac += 1;
    return sayac > ESIK ? Math.round(kurus * ORAN) : kurus;
  });
  return gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
}
```

```js
// donem.test.mjs — gelen gereksinimin tek testi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { donemUcreti } from './donem.mjs';

const donem = Array.from({ length: 60 }, () => ({
  en: 20, boy: 15, yukseklik: 10, gram: 3000, bolge: 'B1',
}));

test('esigi asan donemde butun gonderiler indirimli toplanir', () => {
  assert.equal(donemUcreti(donem), Math.round(60 * 7490 * 0.88));
});
```

```sh
node --test --test-reporter=tap donem.test.mjs \
  | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
```

```
not ok 1 - esigi asan donemde butun gonderiler indirimli toplanir
  expected: 395472
  actual: 440410
# tests 1
# pass 0
# fail 1
```

Aradaki 44.938 kuruşluk fark, eklenti noktasının **yanlış eksende** genelleştirdiğini
gösteriyor. Kural imzası `(kurus, gonderi, yapilandirma)` biçimindedir: kural tek bir
gönderiyi görür. Gelen gereksinim ise bir gönderinin ücretini **kümenin büyüklüğüne** göre
belirliyor; elli birinci gönderi işlenirken önceki ellisinin ücreti çoktan toplanmış
oluyor. Eklenti bunu ancak kümeyi iki kez dolaşarak karşılayabilir, o zaman da toplamayı
yapan çağıran taraf işin tamamını üstlenmiş olur; eklenti noktası bir işe yaramaz.

Genelleştirme, gelecekteki değişimin **gönderi başına bir kural** biçiminde geleceğini
varsaymıştı. Gerçekte gelen değişim gönderi başına değil dönem başına. Yapılandırma
katmanında da benzeri geçerli: altı alanın hiçbiri indirim eşiğini ya da oranını
karşılamıyor.

#### Genelleştirmeyi Atmak

Atılan yapının büyüklüğü sayılabilir.

```sh
printf 'atilan dosyalarin satir sayisi: %s\n' \
  "$(cat yapilandirma.mjs eklenti.mjs donem.mjs | wc -l | tr -d ' ')"
rm yapilandirma.mjs eklenti.mjs donem.mjs donem.test.mjs
```

```
atilan dosyalarin satir sayisi: 48
```

Yerine gelen gereksinim doğrudan karşılanıyor: dönem ücretini hesaplayan bir fonksiyon,
tek eşik ve tek oran.

```js
// ucret.mjs — genellestirme atildi, gelen gereksinim dogrudan karsilandi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  if (gonderiler.length <= INDIRIM_ESIGI) return toplam;
  return Math.round(toplam * INDIRIM_ORANI);
}
```

```js
// ucret.test.mjs — dort eski test ve donem indiriminin iki testi
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti, donemUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };
const donem = (adet) => Array.from({ length: adet }, () => ({ ...kutu, gram: 3000, bolge: 'B1' }));

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});

test('esigi asan donemde butun gonderiler indirimli toplanir', () => {
  assert.equal(donemUcreti(donem(60)), Math.round(60 * 7490 * 0.88));
});

test('esigin altindaki donemde indirim uygulanmaz', () => {
  assert.equal(donemUcreti(donem(10)), 10 * 7490);
});
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node dal.mjs ucret.mjs
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
ok 5 - esigi asan donemde butun gonderiler indirimli toplanir
ok 6 - esigin altindaki donemde indirim uygulanmaz
# tests 6
# pass 6
# fail 0
ucret.mjs: 1 karar noktasi
toplam: 1
# start of coverage report
# -----------------------------------------------------------
# file       | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------
# tarife.mjs | 100.00 |   100.00 |  100.00 | 
# ucret.mjs  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# all files  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# end of coverage report
```

Tek karar noktası kaldı ve o karar noktası bir alan kuralına karşılık geliyor: dönem
eşiğin altında mı, değil mi. Dal kapsamı yüzde 100; kod tabanında koşumun geçmediği yol
yok. Ücret dosyası bir modül ithal ediyor, önceki sürüm üçünü ithal ediyordu.

| Ölçüt | Genelleştirilmiş | Doğrudan |
|---|---|---|
| Karar noktası | 8 | 1 |
| Ücret dosyasının dal kapsamı | %45,45 | %100 |
| Bütün dosyaların satır kapsamı | %90,41 | %100 |
| İthal edilen modül | 3 | 1 |
| Gelen gereksinimi karşılıyor mu | hayır | evet |

#### İlkenin Ne Yasakladığı

Bu ders ile önceki ders farklı şeyler ölçtü. Basit tut ilkesi **gelmiş** bir gereksinim
için en az yapıyı ister; ihtiyacın olmayacak ilkesi **gelmemiş** bir gereksinim için
yapı yazılmasını yasaklar. Bir tanesi bugünkü işin biçimiyle, diğeri yarınki iş hakkındaki
tahminle ilgilidir.

İlkenin sınırı, atmanın maliyetindedir. Burada atılan 48 satır kitaplığın içinde kaldı;
hiçbir dış tüketiciyi etkilemedi, `git` deposundaki tek bir işlemeyle geri alınabilirdi.
Aynı hesap, dışarıya yayımlanmış bir arayüzde ya da geriye dönük dönüştürme gerektiren
kalıcı bir veri biçiminde başka sonuç verir: orada yanlış tahminin bedeli tahmini
yapmamanın bedelinden büyük olabilir. İlkeyi uygulanabilir kılan soru "bu ileride gerekir
mi" değil, **"gerektiğinde eklemek bugün eklemekten ne kadar pahalı"** sorusudur. Ücret
kitaplığında bu fark sıfıra yakındı: dönem indirimi, hiçbir eklenti noktası olmadan altı
satırda yazıldı.

#### Özet

- İhtiyacın olmayacak ilkesi gelmemiş bir gereksinim için bugünden yapı yazılmasını
  yasaklar; ölçülen büyüklük tahminin yanlış çıkma maliyetidir.
- Bugünden yazılan yapılandırma katmanı ile eklenti noktası, ücret dosyasının karar
  noktası sayısını sıfırdan sekize çıkardı ve dal kapsamını yüzde 45,45'e düşürdü.
- Satır kapsamının yüzde 100 olması yolların çalıştığı anlamına gelmez; çalışmayan yollar
  dal ve işlev kapsamında görünür.
- Gerçekten gelen gereksinim gönderi başına değil dönem başınaydı; eklenti noktasının
  imzası bunu ifade edemedi ve tek testli koşum 44.938 kuruşluk farkla kırmızı bitti.
- Genelleştirmenin atılması 48 satır sildi; doğrudan çözüm tek karar noktası, yüzde 100
  dal kapsamı ve altı testle aynı gereksinimi karşıladı.
- İlkenin ölçütü "ileride gerekir mi" değil, gerektiğinde eklemenin bugün eklemeye göre
  ne kadar pahalı olduğudur.

#### Sonraki Adım

Genelleştirme atıldıktan sonra kalan dosya çalışıyor ama düzenlenmiş değil: bölge
katsayısı tablosu tarifeye ait bilgiyi ücret dosyasında tutuyor, dönem hesabındaki eşik
karşılaştırması adsız duruyor ve `g` adı bir gönderiyi temsil ettiğini söylemiyor. Sonraki
ders bu üç kusuru üç mekanik teknikle giderir — ayıklama, taşıma ve yeniden adlandırma —
her tekniği ara adımlarıyla uygular ve her adımdan sonra test koşumunu gösterir.

### Yeniden Düzenleme Teknikleri

Source: https://academia.sh/tr/kurslar/temiz-kod/yeniden-duzenleme-teknikleri

Ayıklama, taşıma ve yeniden adlandırma tekniklerinin ücret kitaplığına ara adımlarıyla uygulanması; her adımdan sonra test koşumunun alınması ve adımların kapsam, dosya ve ad geçiş sayısıyla ölçülmesi.

Önceki ders genelleştirmeyi atıp gelen gereksinimi doğrudan karşıladı. Kalan dosya
çalışıyor ama üç kusuru var: bölge katsayısı tablosu tarifeye ait bilgiyi ücret dosyasında
tutuyor, dönem hesabındaki eşik karşılaştırması adsız duruyor ve `g` adı bir gönderiyi
temsil ettiğini söylemiyor. Bu ders üç kusuru üç mekanik teknikle giderir — **ayıklama**
(extract), **taşıma** (move) ve **yeniden adlandırma** (rename) — her tekniği ara
adımlarıyla uygular ve her adımdan sonra koşum çıktısını gösterir.

Test odaklı geliştirme konusunda kırmızı–yeşil–yeniden düzenle döngüsü kurulmuştu: orada
testler tasarımı sürüklüyor, üçüncü aşama düzenlemeye ayrılıyordu. Burada konu döngü değil,
o üçüncü aşamada uygulanan **tekniklerin kendisi**. Bu derste tek bir test yazılmayacak,
tek bir test beklentisi değişmeyecek ve hiçbir koşum kırmızıya dönmeyecek; testler yalnız
davranışın korunduğunu bildiren ölçü aracıdır.

Başlangıç noktası önceki dersin bitiş durumudur.

```js
// tarife.mjs — kademe bilgisinin tek kaynagi
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

export function kademe(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}
```

```js
// ucret.mjs — onceki dersin bitis hali
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  if (gonderiler.length <= INDIRIM_ESIGI) return toplam;
  return Math.round(toplam * INDIRIM_ORANI);
}
```

```js
// ucret.test.mjs — onceki dersten devralinan alti test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti, donemUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };
const donem = (adet) => Array.from({ length: adet }, () => ({ ...kutu, gram: 3000, bolge: 'B1' }));

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});

test('esigi asan donemde butun gonderiler indirimli toplanir', () => {
  assert.equal(donemUcreti(donem(60)), Math.round(60 * 7490 * 0.88));
});

test('esigin altindaki donemde indirim uygulanmaz', () => {
  assert.equal(donemUcreti(donem(10)), 10 * 7490);
});
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))'
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
ok 5 - esigi asan donemde butun gonderiler indirimli toplanir
ok 6 - esigin altindaki donemde indirim uygulanmaz
# tests 6
# pass 6
# fail 0
```

#### Üç Tekniğin Ortak Biçimi

Üç teknik de aynı üç parçadan oluşur. **Önkoşul:** düzenlemenin davranışı değiştirmediğini
gösterecek bir koşum vardır ve yeşildir. **Ara durum:** eski ile yeni bir süre birlikte
durur; bu aralıkta kod çalışır, testler yeşildir, ama yapı iki yerlidir. **Kaldırma:**
eskisi silinir ve ara durum kapanır.

Ara durumun varlığı tekniklerin can alıcı yanıdır. Bir düzenleme tek hamlede yapıldığında,
koşum kırmızıya döndüğünde hatanın yeni yapıda mı yoksa çağrı yerlerinin geçirilmesinde mi
olduğu ayırt edilemez. Adımlara bölündüğünde her koşum tek bir soruyu yanıtlar.

#### Ayıklama

Dönem hesabındaki eşik karşılaştırması adsız duruyor: dönem uzunluğunu `INDIRIM_ESIGI`
ile karşılaştıran satır bir kuralı uyguluyor ama kuralın adını söylemiyor. Ayıklama, bir
ifadeyi ya da deyim öbeğini kendi adı olan bir birime çıkarmaktır.

Birinci adım yeni fonksiyonu yazar ve **hiçbir çağrı yerine dokunmaz**.

```js
// ucret.mjs — ayiklama adim 1: yeni fonksiyon yazildi, cagri yeri degismedi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function indirimOrani(adet) {
  return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  if (gonderiler.length <= INDIRIM_ESIGI) return toplam;
  return Math.round(toplam * INDIRIM_ORANI);
}
```

```sh
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | grep -E '^# ((tests|pass|fail) |file |ucret\.mjs)'
```

```
# tests 6
# pass 6
# fail 0
# file       | line % | branch % | funcs % | uncovered lines
# ucret.mjs  |  92.31 |   100.00 |   80.00 | 14-15
```

Koşum yeşil, ama ara durumun bedeli kapsamda görünüyor: satır kapsamı yüzde 100'den 92,31'e,
işlev kapsamı yüzde 80'e düştü ve 14–15. satırlar kapsanmayan olarak listelendi. Yeni
fonksiyon henüz çağrılmıyor. Bu, ayıklamanın yarıda bırakıldığında bıraktığı izdir: kimsenin
kullanmadığı bir isim.

İkinci adım çağrı yerini geçirir ve eski ifadeyi siler.

```js
// ucret.mjs — ayiklama adim 2: cagri yeri yeni fonksiyona gecirildi
import { kademe } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function indirimOrani(adet) {
  return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * BOLGE_KATSAYI[g.bolge]);
  return Math.max(bolgeli, ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```sh
node --test --experimental-test-coverage --test-reporter=tap ucret.test.mjs 2>&1 \
  | grep -E '^# ((tests|pass|fail) |file |ucret\.mjs)'
```

```
# tests 6
# pass 6
# fail 0
# file       | line % | branch % | funcs % | uncovered lines
# ucret.mjs  | 100.00 |   100.00 |  100.00 | 
```

Kapsam geri geldi. Dönem fonksiyonunun gövdesi dört satırdan üç satıra indi ve indirim
kuralı artık kendi adıyla ayrıca sınanabilir durumda. Ayıklamanın ölçülebilir kazancı budur:
sınanabilir birim sayısı arttı, davranış aynı kaldı.

#### Taşıma

Bölge katsayısı tablosu ücret dosyasında duruyor ama tarifeye ait bir bilgi: ağırlık
kademeleri gibi ticari bir karar ve aynı merci tarafından değiştiriliyor. Taşıma üç adımda
yapılır, çünkü ithal ilişkisi iki dosyayı birden ilgilendirir.

Birinci adım hedef modülde yeni işlevi oluşturur; kaynak dosyaya dokunulmaz.

```js
// tarife.mjs — tasima adim 1: islev hedef modulde olusturuldu
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

export function kademe(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}

export function bolgeKatsayisi(bolge) {
  return BOLGE_KATSAYI[bolge];
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
printf 'katsayi tablosu tarife.mjs: %s ucret.mjs: %s\n' \
  "$(grep -c 'B2: 1.25' tarife.mjs)" "$(grep -c 'B2: 1.25' ucret.mjs)"
```

```
# tests 6
# pass 6
# fail 0
katsayi tablosu tarife.mjs: 1 ucret.mjs: 1
```

Tablo şu anda iki dosyada. Bu, birinci dersteki bilgi yinelemesinin ta kendisi ve bilerek
üretilen geçici bir durumdur; taşımanın sonraki iki adımı bu yinelemeyi kapatmak içindir.

İkinci adım çağrı yerini yeni işleve geçirir. Eski sabit dosyada kalır, artık kullanılmaz.

```js
// ucret.mjs — tasima adim 2: cagri yeri tarifedeki isleve gecirildi
import { kademe, bolgeKatsayisi } from './tarife.mjs';

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };
const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(g) {
  return Math.max(g.gram, Math.ceil((g.en * g.boy * g.yukseklik) / 3));
}

export function indirimOrani(adet) {
  return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(g) {
  const bolgeli = Math.round(kademe(etkinAgirlik(g)).kurus * bolgeKatsayisi(g.bolge));
  return Math.max(bolgeli, ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
printf 'ucret.mjs icinde BOLGE_KATSAYI gecisi: %s\n' "$(grep -c BOLGE_KATSAYI ucret.mjs)"
```

```
# tests 6
# pass 6
# fail 0
ucret.mjs icinde BOLGE_KATSAYI gecisi: 1
```

Geçiş sayısının bire inmesi kaldırma adımının önkoşuludur: ad yalnız kendi tanımında
geçiyorsa hiçbir yerde okunmuyor demektir. Üçüncü adım tanımı siler.

```sh
grep -v 'BOLGE_KATSAYI' ucret.mjs > gecici.mjs && mv gecici.mjs ucret.mjs
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
printf 'katsayi tablosu tarife.mjs: %s ucret.mjs: %s\n' \
  "$(grep -c 'B2: 1.25' tarife.mjs)" "$(grep -c 'B2: 1.25' ucret.mjs)"
```

```
# tests 6
# pass 6
# fail 0
katsayi tablosu tarife.mjs: 1 ucret.mjs: 0
```

Katsayı tablosu artık tek dosyada. Taşımanın ölçüsü satır sayısı değil, bir bilginin
bulunduğu dosya sayısıdır: ikiden bire indi ve tarife kararlarını değiştiren kişi bundan
sonra tek dosyaya bakacak.

#### Yeniden Adlandırma

Taşıma yeni bir kusur üretti. Tarife modülünde şimdi iki arama işlevi var: biri ağırlığa
göre, diğeri bölgeye göre. `kademe` adı, modülde tek arama varken yeterliydi; ikincisi
gelince hangi boyutta arama yaptığını söylemez oldu. Yeniden adlandırmanın gerekçesi çoğu
zaman böyle doğar — ad kötüleşmez, adın içinde durduğu bağlam değişir.

Dışa verilen bir ad, onu ithal eden her dosyayı ilgilendirir. Bu yüzden adlandırma da üç
adımdır ve birinci adımda eski ad yaşatılır.

```js
// tarife.mjs — ad adim 1: yeni ad eskisinin yanina eklendi
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

export function agirlikKademesi(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}

export const kademe = agirlikKademesi;

export function bolgeKatsayisi(bolge) {
  return BOLGE_KATSAYI[bolge];
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
```

```
# tests 6
# pass 6
# fail 0
eski ad gecisi: 3
```

İkinci adım çağrı yerlerini yeni ada geçirir. Aynı adımda ikinci bir yeniden adlandırma
yapılıyor: `g` parametresi `gonderi` oluyor. İki adlandırmanın aynı adıma sığması bir
gevşeklik değil, kapsam farkının sonucudur; `g` adının kapsamı tek fonksiyondur, dışarıdan
kimse onu görmez, dolayısıyla ara adıma ihtiyaç duymaz.

```js
// ucret.mjs — ad adim 2: cagri yeri yeni ada gecirildi
import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs';

const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(gonderi) {
  const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3);
  return Math.max(gonderi.gram, hacim);
}

export function indirimOrani(adet) {
  return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(gonderi) {
  const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus;
  return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
```

```
# tests 6
# pass 6
# fail 0
eski ad gecisi: 1
```

Üçte birden bire indi; kalan tek geçiş, eski adı yaşatan bağdır. Üçüncü adım onu siler.

```sh
grep -v 'export const kademe = agirlikKademesi;' tarife.mjs > gecici.mjs
mv gecici.mjs tarife.mjs
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))'
printf 'eski ad gecisi: %s\n' "$(cat tarife.mjs ucret.mjs | grep -o 'kademe' | wc -l | tr -d ' ')"
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
ok 5 - esigi asan donemde butun gonderiler indirimli toplanir
ok 6 - esigin altindaki donemde indirim uygulanmaz
# tests 6
# pass 6
# fail 0
eski ad gecisi: 0
```

Test adlarında "kademe" sözcüğü duruyor ve durması gerekiyor: orada geçen şey bir kod adı
değil, alanın sözcüğü. Yeniden adlandırma sembolü değiştirir, alanın sözlüğünü değiştirmez.

#### Adımların Bilançosu

| Adım | Teknik | Koşum | Ölçülen |
|---|---|---|---|
| 1 | ayıklama | yeşil | işlev kapsamı %100 → %80 |
| 2 | ayıklama | yeşil | işlev kapsamı %80 → %100 |
| 3 | taşıma | yeşil | katsayı tablosu 1 dosya → 2 dosya |
| 4 | taşıma | yeşil | eski sabitin geçiş sayısı 2 → 1 |
| 5 | taşıma | yeşil | katsayı tablosu 2 dosya → 1 dosya |
| 6 | adlandırma | yeşil | eski adın geçişi 3 |
| 7 | adlandırma | yeşil | eski adın geçişi 3 → 1 |
| 8 | adlandırma | yeşil | eski adın geçişi 1 → 0 |

Sekiz adımın sekizinde koşum yeşil bitti ve test dosyası hiç değişmedi: altı test, altı
beklenti, sıfır düzenleme. Ölçülerin ikisi bir adımda geriye gitti — üçüncü adımda tablo
iki dosyaya çıktı, birinci adımda kapsam düştü. İkisi de ara durumun izidir ve bir sonraki
adımda kapandı. Yeniden düzenlemenin adım adım yapılmasının anlamı burada görünür: ara
durumlar kaçınılmazdır, tek çare onları kısa tutmaktır.

#### Özet

- Ayıklama, taşıma ve yeniden adlandırma aynı üç parçadan oluşur: yeşil bir önkoşul koşumu,
  eski ile yeninin birlikte durduğu ara durum ve kaldırma adımı.
- Ayıklamanın ara durumu kapsamda görünür; çağrı yeri geçirilmeden önce işlev kapsamı yüzde
  80'e, satır kapsamı yüzde 92,31'e düştü ve iki satır kapsanmayan olarak listelendi.
- Taşımanın ara durumu bilerek üretilmiş bir bilgi yinelemesidir; katsayı tablosu bir
  dosyadan ikiye çıktı, çağrı yeri geçirildikten sonra bire indi.
- Kaldırma adımının önkoşulu ölçülebilir: eski adın geçiş sayısı bire indiyse ad yalnız
  kendi tanımında duruyor demektir.
- Dışa verilen adlar üç adımda, kapsamı tek fonksiyon olan adlar tek adımda değiştirilir;
  fark adın kapsamından gelir.
- Sekiz adımın sekizi yeşil bitti ve altı testin hiçbiri değişmedi; teknikler davranışı
  değil yalnız yapıyı değiştirdi.

#### Sonraki Adım

Bu derste her adımın ardından alınan yeşil koşum, davranışın korunduğunun kanıtı sayıldı.
Bu sayışın kendisi sınanmadı: altı test, ücret hesabının bütün davranışını gerçekten
kapsıyor mu? Sonraki ders testsiz yapılan bir ayıklama adımında sessizce değişen bir
davranışı gösterir, aynı adımın mevcut altı testle de yakalanmadığını ölçer, testlerin
yakalama gücünü sayıya döker ve güçlendirilmiş kümenin aynı adımı kırmızıya çevirdiği anı
gösterir.

### Test Altında Yeniden Düzenleme

Source: https://academia.sh/tr/kurslar/temiz-kod/test-altinda-yeniden-duzenleme

Testsiz yapılan bir ayıklama adımında sessizce değişen davranışın tarama ile bulunması, altı testlik kümenin yakalama gücünün bozma denemeleriyle ölçülmesi ve güçlendirilmiş kümenin aynı adımı kırmızıya çevirmesi.

Önceki derste her adımın ardından alınan yeşil koşum, davranışın korunduğunun kanıtı
sayıldı. Bu sayışın kendisi sınanmadı: altı test, ücret hesabının bütün davranışını
gerçekten kapsıyor mu? Bu ders önce testsiz yapılan bir ayıklama adımında sessizce değişen
bir davranışı gösterir, aynı adımın mevcut altı testle de yakalanmadığını ölçer, test
kümesinin yakalama gücünü sayıya döker ve güçlendirilmiş kümenin aynı adımı kırmızıya
çevirdiği anı gösterir.

Testlerin buradaki işi tektir: **davranış koruması**. Test yazımı, sahte bağımlılık kurma
ve testi tasarımın sürükleyicisi yapma, Yazılım Kalitesi ve Test müfredatının konularıdır.
Burada testlere yalnız tek bir soru sorulur — yeniden düzenlemeden önceki ve sonraki kod
aynı girdilere aynı yanıtı veriyor mu?

Başlangıç noktası önceki dersin bitiş durumudur.

```js
// tarife.mjs — onceki dersin bitis hali
export const KADEME = [
  { ustGram: 1000, kurus: 4990, ad: 'kucuk' },
  { ustGram: 5000, kurus: 7490, ad: 'orta' },
  { ustGram: 10000, kurus: 9900, ad: 'buyuk' },
  { ustGram: 20000, kurus: 12900, ad: 'cok-buyuk' },
  { ustGram: Infinity, kurus: 24900, ad: 'agir' },
];

const BOLGE_KATSAYI = { B1: 1.0, B2: 1.25, B3: 1.6 };

export function agirlikKademesi(gram) {
  return KADEME.find((k) => gram <= k.ustGram);
}

export function bolgeKatsayisi(bolge) {
  return BOLGE_KATSAYI[bolge];
}
```

```js
// ucret.mjs — onceki dersin bitis hali
import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs';

const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(gonderi) {
  const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3);
  return Math.max(gonderi.gram, hacim);
}

export function indirimOrani(adet) {
  return adet > INDIRIM_ESIGI ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(gonderi) {
  const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus;
  return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```js
// ucret.test.mjs — onceki dersten devralinan alti test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { gonderiUcreti, donemUcreti } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };
const donem = (adet) => Array.from({ length: adet }, () => ({ ...kutu, gram: 3000, bolge: 'B1' }));

test('kademe ucreti agirliktan okunur', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B1' }), 7490);
});

test('bolge katsayisi taban ucreti carpar', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 3000, bolge: 'B3' }), 11984);
});

test('asgari ucret bolge carpimindan sonra uygulanir', () => {
  assert.equal(gonderiUcreti({ ...kutu, gram: 500, bolge: 'B2' }), 6990);
});

test('hacim agirligi gercek agirliktan buyukse o kullanilir', () => {
  assert.equal(gonderiUcreti({ en: 40, boy: 30, yukseklik: 30, gram: 500, bolge: 'B2' }), 16125);
});

test('esigi asan donemde butun gonderiler indirimli toplanir', () => {
  assert.equal(donemUcreti(donem(60)), Math.round(60 * 7490 * 0.88));
});

test('esigin altindaki donemde indirim uygulanmaz', () => {
  assert.equal(donemUcreti(donem(10)), 10 * 7490);
});
```

Karşılaştırma yapılabilmesi için düzenlemeden önceki sürümün bir kopyası saklanıyor.

```sh
cp ucret.mjs ucret-onceki.mjs
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
```

```
# tests 6
# pass 6
# fail 0
```

#### Sessizce Değişen Davranış

Düzenleme, önceki dersteki ayıklamanın aynısıdır: dönem indiriminin eşik kararı kendi adını
alıyor. Adı verilirken karşılaştırmanın yönü kayıyor.

```js
// ucret.mjs — ayiklama: esik karari indirimliMi adiyla cikarildi
import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs';

const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(gonderi) {
  const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3);
  return Math.max(gonderi.gram, hacim);
}

export function indirimliMi(adet) {
  return adet >= INDIRIM_ESIGI;
}

export function indirimOrani(adet) {
  return indirimliMi(adet) ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(gonderi) {
  const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus;
  return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^ *(ok|# (tests|pass|fail))'
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
ok 5 - esigi asan donemde butun gonderiler indirimli toplanir
ok 6 - esigin altindaki donemde indirim uygulanmaz
# tests 6
# pass 6
# fail 0
```

Altı testin altısı yeşil. Testsiz bir kod tabanında bu adım fark edilmeden geçerdi; testli
bir kod tabanında da fark edilmeden geçti. Değişikliğin gerçekten davranışı koruyup
korumadığını görmenin yolu, iki sürümü aynı girdiler üzerinde karşılaştırmaktır.

```js
// fark.mjs — ayni girdilerde iki surumun sonucunu karsilastirir
import { donemUcreti as onceki } from './ucret-onceki.mjs';
import { donemUcreti as simdiki } from './ucret.mjs';

const kutu = { en: 20, boy: 15, yukseklik: 10 };
let denenen = 0;
const ayrisan = [];

for (const gram of [500, 3000, 12000]) {
  for (let adet = 1; adet <= 120; adet++) {
    const donem = Array.from({ length: adet }, () => ({ ...kutu, gram, bolge: 'B1' }));
    denenen += 1;
    const a = onceki(donem);
    const b = simdiki(donem);
    if (a !== b) ayrisan.push(`adet=${adet} gram=${gram} onceki=${a} simdiki=${b}`);
  }
}

console.log('denenen donem:', denenen);
console.log('sonucu degisen:', ayrisan.length);
console.log('ilk ornek:', ayrisan[0] ?? 'yok');
```

```
denenen donem: 360
sonucu degisen: 3
ilk ornek: adet=50 gram=500 onceki=349500 simdiki=307560
```

Üç yüz altmış dönemden üçünün sonucu değişti ve üçü de aynı noktada: tam elli gönderilik
dönemler. Alan karşılığı, tam eşikte duran bir sözleşmeli müşterinin faturasının 3.495,00
TL yerine 3.075,60 TL çıkmasıdır. Yeşil koşum yanlış bir şey söylemedi; **kendisine
sorulmayan** soruyu yanıtlamadı. Testler girdi uzayının tamamını değil, kendi girdilerini
sabitler.

#### Korumanın Ölçülmesi

O hâlde ölçülmesi gereken büyüklük, test kümesinin **yakalama gücüdür**: kaç tür davranış
değişikliği koşumu kırmızıya çevirir? Bu, koda kasıtlı ve küçük bozmalar uygulayıp koşumun
tepkisini sayarak ölçülür. Her bozma tek bir karakter ya da tek bir sayı düzeyindedir ve
ölçümden sonra geri alınır; bunlara **bozma denemesi** (mutation) denir.

```sh
cp ucret.mjs ucret-ayiklanmis.mjs
cp ucret-onceki.mjs ucret.mjs
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
```

```
# tests 6
# pass 6
# fail 0
```

```js
// koruma.mjs — test kumesinin kac davranis degisikligini yakaladigini olcer
import { readFileSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';

const MUTASYON = [
  ['esik karsilastirmasi', 'ucret.mjs', 'adet > INDIRIM_ESIGI', 'adet >= INDIRIM_ESIGI'],
  ['asgari ucret', 'ucret.mjs', 'ASGARI_KURUS = 6990', 'ASGARI_KURUS = 6890'],
  ['indirim orani', 'ucret.mjs', 'INDIRIM_ORANI = 0.88', 'INDIRIM_ORANI = 0.89'],
  ['hacim yuvarlamasi', 'ucret.mjs', 'Math.ceil(', 'Math.floor('],
  ['bolge katsayisi', 'tarife.mjs', 'B2: 1.25', 'B2: 1.3'],
  ['kademe siniri', 'tarife.mjs', 'gram <= k.ustGram', 'gram < k.ustGram'],
];

function kirmiziMi() {
  try {
    execFileSync('node', ['--test', 'ucret.test.mjs'], { stdio: 'ignore' });
    return false;
  } catch {
    return true;
  }
}

let yakalanan = 0;
for (const [ad, dosya, eski, yeni] of MUTASYON) {
  const asil = readFileSync(dosya, 'utf8');
  writeFileSync(dosya, asil.replace(eski, yeni));
  const kirmizi = kirmiziMi();
  writeFileSync(dosya, asil);
  if (kirmizi) yakalanan += 1;
  console.log(`${ad}: ${kirmizi ? 'yakalandi' : 'kacti'}`);
}
console.log(`yakalanan: ${yakalanan}/${MUTASYON.length}`);
```

```
esik karsilastirmasi: kacti
asgari ucret: yakalandi
indirim orani: yakalandi
hacim yuvarlamasi: kacti
bolge katsayisi: yakalandi
kademe siniri: yakalandi
yakalanan: 4/6
```

Altı bozmadan dördü yakalandı. Kaçan ikisi tesadüfen kaçmıyor. Eşik karşılaştırması kaçtı,
çünkü dönem testleri 60 ve 10 gönderi deniyor; ikisi de eşiğin kendisi değil. Hacim
yuvarlaması kaçtı, çünkü testlerdeki iki kutunun hacim ağırlığı da tam sayı çıkıyor —
3.000 ve 12.000 gram — ve yukarı yuvarlama ile aşağı yuvarlama aynı sonucu veriyor. İki
kaçış da aynı desende: sınır denenmemiş.

Kapatma yolu, önceki dersin ayıklamalarının açtığı kapıdan geçer. Ayıklama sınanabilir
birim sayısını artırmıştı; sınır davranışı artık dış uçtan değil, o birimlerin kendisinden
sabitlenebilir.

```sh
cat >> ucret.test.mjs <<'SON'

import { etkinAgirlik, indirimOrani } from './ucret.mjs';

test('hacim agirligi yukari yuvarlanir', () => {
  assert.equal(etkinAgirlik({ en: 11, boy: 11, yukseklik: 11, gram: 100 }), 444);
});

test('esikteki gonderi sayisi indirim uretmez', () => {
  assert.equal(indirimOrani(50), 1);
});
SON
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
node koruma.mjs
```

```
# tests 8
# pass 8
# fail 0
esik karsilastirmasi: yakalandi
asgari ucret: yakalandi
indirim orani: yakalandi
hacim yuvarlamasi: yakalandi
bolge katsayisi: yakalandi
kademe siniri: yakalandi
yakalanan: 6/6
```

İki test, kaçan iki bozmayı kapattı. Kazanılan büyüklük test sayısı değil, oran: yakalama
gücü altıda dörtten altıda altıya çıktı. Bu oran bir güvence belgesi değil, bir ölçüdür;
listede olmayan bir bozma türü hâlâ kaçabilir. Ölçünün değeri, "testlerim yeterli mi"
sorusunu sayılabilir bir soruya çevirmesindedir.

#### Aynı Adım, Güçlendirilmiş Küme

Kaymış ayıklama şimdi geri getiriliyor.

```sh
cp ucret-ayiklanmis.mjs ucret.mjs
node --test --test-reporter=tap ucret.test.mjs \
  | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
```

```
ok 1 - kademe ucreti agirliktan okunur
ok 2 - bolge katsayisi taban ucreti carpar
ok 3 - asgari ucret bolge carpimindan sonra uygulanir
ok 4 - hacim agirligi gercek agirliktan buyukse o kullanilir
ok 5 - esigi asan donemde butun gonderiler indirimli toplanir
ok 6 - esigin altindaki donemde indirim uygulanmaz
ok 7 - hacim agirligi yukari yuvarlanir
not ok 8 - esikteki gonderi sayisi indirim uretmez
  expected: 1
  actual: 0.88
# tests 8
# pass 7
# fail 1
```

Aynı düzenleme, aynı kod, farklı sonuç: sekizinci test kırmızı ve beklenen ile gerçekleşen
değerler koşumdan okunuyor. Eşik testi, üç yüz altmış dönemlik taramanın bulduğu sapmayı
tek bir çağrıyla gösteriyor. Düzeltme, kaymış karşılaştırmayı geri çevirmektir.

```js
// ucret.mjs — ayni ayiklama, karsilastirma yonu korunarak
import { agirlikKademesi, bolgeKatsayisi } from './tarife.mjs';

const ASGARI_KURUS = 6990;
const INDIRIM_ESIGI = 50;
const INDIRIM_ORANI = 0.88;

export function etkinAgirlik(gonderi) {
  const hacim = Math.ceil((gonderi.en * gonderi.boy * gonderi.yukseklik) / 3);
  return Math.max(gonderi.gram, hacim);
}

export function indirimliMi(adet) {
  return adet > INDIRIM_ESIGI;
}

export function indirimOrani(adet) {
  return indirimliMi(adet) ? INDIRIM_ORANI : 1;
}

export function gonderiUcreti(gonderi) {
  const taban = agirlikKademesi(etkinAgirlik(gonderi)).kurus;
  return Math.max(Math.round(taban * bolgeKatsayisi(gonderi.bolge)), ASGARI_KURUS);
}

export function donemUcreti(gonderiler) {
  const toplam = gonderiler.reduce((t, g) => t + gonderiUcreti(g), 0);
  return Math.round(toplam * indirimOrani(gonderiler.length));
}
```

```sh
node --test --test-reporter=tap ucret.test.mjs | grep -E '^# (tests|pass|fail)'
node fark.mjs
```

```
# tests 8
# pass 8
# fail 0
denenen donem: 360
sonucu degisen: 0
ilk ornek: yok
```

Koşum yeşil ve tarama sıfır sapma buluyor. İki ölçü birlikte okunduğunda anlam kazanır:
tarama bu düzenlemenin denenen girdilerde davranışı koruduğunu, bozma denemesi ölçüsü de
kümenin altı bozma türünün altısını yakaladığını söylüyor. Davranış koruması bir niyet
değil, iki sayıyla ifade edilen bir güvencedir.

#### Özet

- Yeniden düzenleme adımının davranışı koruduğu iddiası, yeşil koşumdan değil, iki sürümün
  aynı girdilerde aynı sonucu vermesinden okunur.
- Kaymış tek bir karşılaştırma yönü altı testin altısını yeşil bıraktı; tarama 360 dönemin
  üçünde sapma buldu ve üçü de tam eşikte toplandı.
- Bir test kümesinin yakalama gücü ölçülebilir: koda uygulanan altı bozma denemesinin
  dördü yakalandı, ikisi kaçtı ve iki kaçış da sınanmamış sınırdan geldi.
- Sınır davranışı, ayıklama ile ortaya çıkan birimler üzerinden iki testle sabitlendi ve
  yakalama gücü altıda altıya çıktı.
- Güçlendirilmiş kümede aynı düzenleme sekizinci testi kırmızıya çevirdi; beklenen ile
  gerçekleşen değer koşumdan doğrudan okundu.
- Bozma denemesi ölçüsü yeterlilik belgesi değil, "testlerim yeterli mi" sorusunu sayıya
  çeviren bir araçtır.

#### Kurs Kapanışı

Temiz Kod kursu on altı derste kodun iç yapısını üç açıdan ele aldı.

**Adlandırma ve Yapı**, okunabilirliği ölçülebilir bir büyüklüğe çevirdi: niyeti açıklayan
adlar, yorumun yerini alan adlandırma, tek soyutlama düzeyinde duran küçük fonksiyonlar,
girinti ve biçim tutarlılığının fark maliyeti, çevrimsel karmaşıklığın dallanma
sayısıyla ölçülmesi ve koşul, arama tablosu ile çok biçimlilik arasındaki seçim.

**Arayüz ve Veri Kararları**, kararı imzaya taşıdı: boş değer geçirmemek, mantıksal bayrak
parametrelerini iki fonksiyona bölmek, en dar görünürlüğü seçmek, kodu değişim nedenine —
yani aktöre — göre gruplamak ve aynı işi projede tek biçimde yapmak.

**Sadelik İlkeleri**, kararların zaman içindeki maliyetini saydı: aynı bilginin tek yerde
durması ile tesadüfi benzerliğin yanlış soyutlamaya dönüşmesi, tasarımın küçük adımlarla
evrilmesi, gelmemiş gereksinim için yazılan genelleştirmenin bedeli, ayıklama–taşıma–yeniden
adlandırma teknikleri ve davranış korumasının ölçülmesi.

On altı dersin ortak yanı, her iddianın bir sayıya bağlanmasıydı: dokunulan dosya sayısı,
dallanma sayısı, kapsanmayan yol sayısı, ayrışan sınır sayısı, yakalanan bozma sayısı. Ortak
yanları olan başka bir şey daha var ve bu ders sonuna kadar adlandırılmadı: bütün kararlar
**tek bir programlama biçimi** içinde verildi. Fonksiyonlar, sabitler, modüller ve nesne
sözlükleri hiç tartışılmadan kullanıldı. Kodun nasıl yazıldığı sorgulandı; hangi modelle
düşünüldüğü sorgulanmadı.

Ücret kitaplığı bir dizi fonksiyon ve veri tablosu olarak modellendi. Aynı alan, davranışı
ve durumu bir arada tutan nesneler olarak da modellenebilirdi; ya da dönüşümlerin
zincirlendiği, durumun hiç değişmediği bir hesap olarak. Bu üç modelin hangisinin seçileceği
temiz kod sorusu değil, **paradigma** sorusudur. Sonraki kurs olan Programlama
Paradigmaları bunu ele alır: nesneye dayalı ve fonksiyonel yaklaşımların modelleme farkı,
kalıtım yerine bileşimin seçildiği durumlar ve aynı problemin birden çok paradigmayla
modellenip karşılaştırılması.

