---
title: 'Tasarım İlkeleri'
source: 'https://academia.sh/tr/kurslar/tasarim-ilkeleri'
language: tr
updated: '2026-08-17T04:42:38+00:00'
license: 'CC BY-SA 4.0'
lessons: 19
outcomes:
    - 'SOLID ilkelerini örnek ve karşı örnekle açıklar'
    - 'Bağlaşım ve uyum ölçütleriyle modül sınırlarını değerlendirir'
    - 'Bağımlılık yönünü soyutlamalara doğru çevirir'
---

# Tasarım İlkeleri

Bir tasarımın iyi olup olmadığının birimin içine değil birimler arasındaki bağa bakılarak ölçülmesi: SOLID ilkelerinin örnek ve karşı örnekle kurulması, bağlaşım ve uyum ölçütleriyle modül sınırlarının değerlendirilmesi, bağımlılık yönünün soyutlamalara çevrilmesi ve sınırların politika–ayrıntı ayrımına göre çizilmesi.

## SOLID

Beş tasarım ilkesinin ölçülebilir karşılıkları: bir modülün değişim nedeni sayısı, yeni bir tarife türü eklendiğinde değişen ve eklenen satırlar, alt tip sözleşmesini bozan bir sınıfın sözleşme sınamasında kırılması, istemcinin çağırdığı yöntem sayısı ile arayüzün dayattığı yöntem sayısı ve soyutlamanın hangi tarafta tanımlandığı.

### Tek Sorumluluk İlkesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/tek-sorumluluk-ilkesi

Bir modülün değişim nedeni sayısının aktör tablosuyla sayılması: üç farklı aktörün kararını taşıyan tek modül ile kararları ayrılmış tasarımın karşılaştırılması ve muhasebe kaynaklı bir değişikliğin operasyon raporuna sızıp sızmadığının çıktı satırı sayarak ölçülmesi.

Programlama Paradigmaları kursu, iki paradigmanın da tek tek birimlerin nasıl kurulacağını
söylediğini, ama birimler arasındaki ilişkinin ne zaman sağlıklı olduğunu ölçmediğini
adlandırarak kapandı. Bir tasarımın iyi olup olmadığı birimin içine değil, birimler
arasındaki bağa bakılarak anlaşılır. Bu kurs o bağı ölçer.

İlk ölçü en küçük soruya ait: bir modül ne zaman fazla şey yapıyor sayılır? Yaygın yanıt
"çok iş yapıyorsa" biçimindedir ve işe yaramaz, çünkü iş miktarı için bir eşik yoktur. **Tek
sorumluluk ilkesi** (single responsibility principle) soruyu satır sayısından çıkarıp
değişikliği isteyen tarafa taşır: bir modülün yalnız tek bir **aktörün** kararıyla değişmesi
beklenir. Buradaki ölçü, modülün büyüklüğü değil, ona değiştirme isteği gönderebilen ayrı
tarafların sayısıdır.

#### Üç Aktör, Tek Modül

Kurs boyunca sürecek örnek bir gönderi ücretlendirme ve yönlendirme kitaplığıdır. Ücret
hesabı üç ayrı kararın çarpımıdır: ağırlık kademesinden gelen taban ücret, adresten türetilen
bölge katsayısı ve sözleşmeli müşteriye uygulanan hacim indirimi. Bu kararların sahipleri
farklı birimlerdir: bölge katsayılarını operasyon, indirim eşiğini sözleşme yönetimi, fatura
tutarının yuvarlanmasını muhasebe belirler.

```sh
mkdir -p birlesik ayrilmis
```

```js
// birlesik/ucret.mjs — uc aktorun karari tek modulde
export const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
export const BOLGE = { "34": 100, "06": 115, "65": 140 };
export const HACIM_ESIGI = 3;

export const yuvarla = (kurus) => Math.round(kurus / 100) * 100;
export const bolgeKatsayisi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;
export const indirimOrani = (adet) => (adet >= HACIM_ESIGI ? 10 : 0);

export function kademeUcreti(agirlik) {
  for (const [ust, ucret] of KADEME) if (agirlik <= ust) return ucret;
  return KADEME.at(-1)[1] + Math.ceil(agirlik - KADEME.at(-1)[0]) * 900;
}

export function ucret(gonderi, adet) {
  const ham = (kademeUcreti(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100;
  return yuvarla((ham * (100 - indirimOrani(adet))) / 100);
}

export function bolgeRaporu(gonderiler) {
  const toplam = new Map();
  for (const g of gonderiler) {
    const b = g.adres.slice(0, 2);
    toplam.set(b, (toplam.get(b) ?? 0) + ucret(g, gonderiler.length));
  }
  return [...toplam].sort().map(([b, k]) => `bolge ${b} toplam ${k} kurus`);
}
```

```js
// birlesik/rapor.mjs — operasyonun bolge raporu ile muhasebenin fatura tutari
import { bolgeRaporu, ucret } from "./ucret.mjs";

const GONDERILER = [
  { agirlik: 0.8, adres: "34100" }, { agirlik: 4.2, adres: "06500" },
  { agirlik: 12.0, adres: "65200" }, { agirlik: 2.5, adres: "34700" },
];

for (const satir of bolgeRaporu(GONDERILER)) console.log(satir);
console.log("fatura toplami =", GONDERILER.reduce((t, g) => t + ucret(g, GONDERILER.length), 0), "kurus");
```

```sh
node birlesik/rapor.mjs
```

```
bolge 06 toplam 6600 kurus
bolge 34 toplam 9300 kurus
bolge 65 toplam 14900 kurus
fatura toplami = 30800 kurus
```

Modül otuz satır ve doğru çalışıyor. Satır sayısına bakan bir ölçüt burada bir sorun
göremez.

#### Değişim Nedeninin Sayılması

Ölçü, aktörlerin hangi simgeleri değiştireceğini bir tabloya yazıp bu simgelerin hangi
dosyalarda tanımlandığını saymakla elde edilir. Aşağıdaki betik bir dizindeki modülleri
tarar, her modül için kaç ayrı aktörün karar taşıdığını ve her aktörün kaç dosyaya
dokunacağını bildirir.

```js
// neden-say.mjs — her modulun kac aktorun karariyla degistigini sayar
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

// Her aktorun degistirmek isteyecegi simgeler. Gereksinimler bu tablodan okunur.
const AKTOR = {
  operasyon: ["BOLGE", "bolgeKatsayisi", "bolgeRaporu"],
  muhasebe: ["yuvarla", "faturaTutari"],
  sozlesme: ["HACIM_ESIGI", "indirimOrani"],
};

const tanim = (metin, simge) =>
  new RegExp(`\\b(?:const|function|let|class)\\s+${simge}\\b`).test(metin);

const kok = process.argv[2];
const dosyalar = readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort();
const aktorunDosyalari = new Map(Object.keys(AKTOR).map((a) => [a, []]));
let enYuksek = 0;

for (const d of dosyalar) {
  const metin = readFileSync(join(kok, d), "utf8");
  const bulunan = Object.entries(AKTOR)
    .filter(([, simgeler]) => simgeler.some((s) => tanim(metin, s)))
    .map(([a]) => a);
  for (const a of bulunan) aktorunDosyalari.get(a).push(d);
  enYuksek = Math.max(enYuksek, bulunan.length);
  console.log(`${d.padEnd(18)} degisim nedeni = ${bulunan.length}  ${bulunan.join(" ")}`);
}
for (const [a, ds] of aktorunDosyalari) {
  console.log(`${a.padEnd(10)} -> ${ds.length} dosya: ${ds.join(" ")}`);
}
console.log(`en yuksek degisim nedeni = ${enYuksek}`);
```

```sh
node neden-say.mjs birlesik
```

```
rapor.mjs          degisim nedeni = 0
ucret.mjs          degisim nedeni = 3  operasyon muhasebe sozlesme
operasyon  -> 1 dosya: ucret.mjs
muhasebe   -> 1 dosya: ucret.mjs
sozlesme   -> 1 dosya: ucret.mjs
en yuksek degisim nedeni = 3
```

Üç aktör de tek dosyaya dokunuyor ve o dosyanın değişim nedeni sayısı üç. İlkeyi ihlal eden
durum budur: dosyanın büyüklüğü değil, üç ayrı tarafın aynı metni düzenleyecek olması.

#### Kararların Ayrılması

Ayrılmış tasarımda her aktörün kararı kendi modülünde durur. Ortada kalan tarife çekirdeği
hiçbir aktörün doğrudan kararını taşımaz; kuruş cinsinden **kesin** değeri üretir ve
yuvarlamayı yapmaz.

```js
// ayrilmis/bolge.mjs — operasyonun karari: bolge katsayilari
export const BOLGE = { "34": 100, "06": 115, "65": 140 };
export const bolgeKatsayisi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;
```

```js
// ayrilmis/sozlesme.mjs — sozlesme yonetiminin karari: hacim indirimi
export const HACIM_ESIGI = 3;
export const indirimOrani = (adet) => (adet >= HACIM_ESIGI ? 10 : 0);
```

```js
// ayrilmis/tarife.mjs — tarife cekirdegi: kurus cinsinden kesin ucret, yuvarlama yok
import { bolgeKatsayisi } from "./bolge.mjs";
import { indirimOrani } from "./sozlesme.mjs";

export const KADEME = [[1, 3900], [5, 6400], [20, 11800]];

export function kademeUcreti(agirlik) {
  for (const [ust, ucret] of KADEME) if (agirlik <= ust) return ucret;
  return KADEME.at(-1)[1] + Math.ceil(agirlik - KADEME.at(-1)[0]) * 900;
}

export function ucret(gonderi, adet) {
  const ham = (kademeUcreti(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100;
  return (ham * (100 - indirimOrani(adet))) / 100;
}
```

```js
// ayrilmis/fatura.mjs — muhasebenin karari: fatura tutari tam liraya yuvarlanir
import { ucret } from "./tarife.mjs";

export const yuvarla = (kurus) => Math.round(kurus / 100) * 100;
export const faturaTutari = (gonderiler) =>
  gonderiler.reduce((t, g) => t + yuvarla(ucret(g, gonderiler.length)), 0);
```

```js
// ayrilmis/bolge-raporu.mjs — operasyonun karari: rapor kesin ucreti toplar
import { ucret } from "./tarife.mjs";

export function bolgeRaporu(gonderiler) {
  const toplam = new Map();
  for (const g of gonderiler) {
    const b = g.adres.slice(0, 2);
    toplam.set(b, (toplam.get(b) ?? 0) + ucret(g, gonderiler.length));
  }
  return [...toplam].sort().map(([b, k]) => `bolge ${b} toplam ${k} kurus`);
}
```

```js
// ayrilmis/rapor.mjs — ayni iki cikti, ayrilmis tasarimla
import { bolgeRaporu } from "./bolge-raporu.mjs";
import { faturaTutari } from "./fatura.mjs";

const GONDERILER = [
  { agirlik: 0.8, adres: "34100" }, { agirlik: 4.2, adres: "06500" },
  { agirlik: 12.0, adres: "65200" }, { agirlik: 2.5, adres: "34700" },
];

for (const satir of bolgeRaporu(GONDERILER)) console.log(satir);
console.log("fatura toplami =", faturaTutari(GONDERILER), "kurus");
```

```sh
node ayrilmis/rapor.mjs
node neden-say.mjs ayrilmis
```

```
bolge 06 toplam 6624 kurus
bolge 34 toplam 9270 kurus
bolge 65 toplam 14868 kurus
fatura toplami = 30800 kurus
bolge-raporu.mjs   degisim nedeni = 1  operasyon
bolge.mjs          degisim nedeni = 1  operasyon
fatura.mjs         degisim nedeni = 1  muhasebe
rapor.mjs          degisim nedeni = 0
sozlesme.mjs       degisim nedeni = 1  sozlesme
tarife.mjs         degisim nedeni = 0
operasyon  -> 2 dosya: bolge-raporu.mjs bolge.mjs
muhasebe   -> 1 dosya: fatura.mjs
sozlesme   -> 1 dosya: sozlesme.mjs
en yuksek degisim nedeni = 1
```

Fatura toplamı iki tasarımda da aynı. Bölge raporu değişti: birleşik sürüm yuvarlanmış
tutarları topluyordu, ayrılmış sürüm kesin kuruşları topluyor. İkinci ölçü tabloyu tersten
okur: aktör başına dosya sayısı operasyon için ikiye çıktı. Dosya sayısındaki bu artış
ilkenin bedelidir; karşılığı bir sonraki ölçüde görünür.

#### Sızan Değişiklik

Muhasebe yeni bir kural getiriyor: fatura tutarı en yakın liraya değil, yukarı yuvarlanacak.
Değişiklik `Math.round` yerine `Math.ceil` yazmaktır. Aşağıdaki betik değişikliği iki
tasarımda da uygular ve çıktının kaç satırının değiştiğini sayar. Yerinde düzenleme
seçeneğinin uyumlu biçimi için yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde
aynı davranır.

```sh
node birlesik/rapor.mjs > birlesik-once.txt
node ayrilmis/rapor.mjs > ayrilmis-once.txt
echo "dokunulan dosya: $(grep -rl 'Math.round' birlesik ayrilmis | sort | tr '\n' ' ')"
sed -i.yedek 's/Math.round/Math.ceil/' $(grep -rl 'Math.round' birlesik ayrilmis)
node birlesik/rapor.mjs > birlesik-sonra.txt
node ayrilmis/rapor.mjs > ayrilmis-sonra.txt
echo "birlesik: degisen cikti satiri = $(diff birlesik-once.txt birlesik-sonra.txt | grep -c '^<')"
echo "ayrilmis: degisen cikti satiri = $(diff ayrilmis-once.txt ayrilmis-sonra.txt | grep -c '^<')"
diff birlesik-once.txt birlesik-sonra.txt || true
```

```
dokunulan dosya: ayrilmis/fatura.mjs birlesik/ucret.mjs
birlesik: degisen cikti satiri = 3
ayrilmis: degisen cikti satiri = 1
1,2c1,2
< bolge 06 toplam 6600 kurus
< bolge 34 toplam 9300 kurus
---
> bolge 06 toplam 6700 kurus
> bolge 34 toplam 9400 kurus
4c4
< fatura toplami = 30800 kurus
---
> fatura toplami = 31000 kurus
```

İki tasarımda da tek dosyaya dokunuldu; fark dokunma sayısında değil, sonuçta. Muhasebenin
istediği tek satır fatura toplamıydı: ayrılmış tasarımda yalnız o satır değişti. Birleşik
tasarımda bölge raporunun iki satırı da kaydı, çünkü rapor yuvarlama kararının ürününü
topluyordu. Operasyon bu değişikliği talep etmedi, haberi de olmadı.

İlkenin ölçülebilir vaadi budur: değişim nedeni sayısı bire indiğinde bir aktörün kararı,
kararı paylaşmayan bir aktörün çıktısını bozamaz. Bedel de ölçüldü — operasyonun dosya
sayısı ikiye çıktı. İlke, sürprizi dosya sayısıyla takas eder.

#### Sorumluluğun Sınırı Nerede

Tabloyu aktör yerine "işlev" üzerinden kurmak yaygın bir hatadır. Bölge katsayısı ile bölge
raporu farklı işlerdir, ama ikisi de operasyonun kararıdır; ayrı dosyalarda durmaları
zorunlu değildir. Buna karşılık yuvarlama ile bölge katsayısı aynı satırda hesaplansa bile
farklı aktörlere aittir; birlikte durmaları ölçülebilir bir sürprize yol açar.

Sınırı bulmanın pratik yolu, geçmiş değişiklik isteklerinin kimden geldiğine bakmaktır. Aynı
dosyaya iki farklı birimin talebiyle dokunulduysa değişim nedeni sayısı birden büyüktür.
Hiçbir talep gelmemiş bir ayrımı önden kurmak ise ters yönde bir maliyettir: dosya sayısı
artar, karşılığında ölçülen bir sürpriz azalmaz.

#### Özet

- Tek sorumluluk ilkesi modülün büyüklüğünü değil, ona değişiklik talebi gönderebilen ayrı
  aktörlerin sayısını sınırlar.
- Değişim nedeni sayısı ölçülebilir: aktör–simge tablosu taranarak birleşik modülde 3,
  ayrılmış tasarımda en fazla 1 bulundu.
- Ölçünün karşılığı koşumla görüldü: aynı muhasebe değişikliği birleşik tasarımda 3 çıktı
  satırını, ayrılmış tasarımda 1 çıktı satırını değiştirdi.
- Ayrımın bedeli de sayıldı: operasyonun dokunduğu dosya sayısı birden ikiye çıktı. İlke
  sürprizi dosya sayısıyla takas eder.
- Sınır işlev benzerliğine göre değil, değişiklik talebinin kimden geldiğine göre çizilir.

#### Sonraki Adım

Ayrılmış tasarımda her aktör kendi dosyasını değiştirdi, ama her aktör yine de **var olan
bir dosyayı düzenledi**. Bölge katsayısı tablosuna yeni bir bölge eklemek `bolge.mjs`
dosyasının içindeki nesneyi düzenlemek demektir. Kademe tablosuna değil de tarifenin
**türüne** dokunan bir istek geldiğinde — örneğin ağırlık yerine hacme göre ücretlendiren
ikinci bir tarife — düzenlenecek dosya sayısı yeniden artar. Sonraki ders, yeni bir tarife
türü eklendiğinde değişen satır sayısı ile eklenen satır sayısını iki tasarımda ölçer ve
genişlemenin değişiklik gerektirmediği bir yapının ne demek olduğunu gösterir.

### Açık–Kapalı İlkesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/acik-kapali-ilkesi

Genişlemeye açık, değişikliğe kapalı tasarımın ölçülmesi: aynı tarife türü iki ayrı switch üzerinden seçilen sürüm ile tarifelerin kendini bir kayda yazdığı sürüme yeni bir tarife eklenir, düzenlenen var olan dosya sayısı, eklenen satır sayısı ve hesap modülünün değişip değişmediği karşılaştırılır.

Tek sorumluluk ilkesi kararları aktörlerine göre ayırdı, ama her aktör yine var olan bir
dosyayı düzenledi. Bölge tablosuna yeni bir bölge eklemek `bolge.mjs` içindeki nesneyi
düzenlemektir. Bu, tablodaki bir satırı çoğaltmak olduğu sürece ucuzdur. Tarifenin
**türüne** dokunan bir istek geldiğinde — ağırlık yerine hacme göre ücretlendiren ikinci bir
tarife — maliyet başka türlü çıkar.

**Açık–kapalı ilkesi** (open–closed principle) bu iki işlemi ayırır: bir modül yeni
davranışla **genişlemeye açık**, var olan metninin **değiştirilmesine kapalı** olmalıdır.
İddia ölçülebilir: aynı gereksinim iki tasarıma uygulanır, düzenlenen var olan dosya sayısı,
eklenen satır sayısı ve hesap modülünün değişip değişmediği sayılır.

#### Türün Switch ile Seçildiği Sürüm

İlk sürümde tarife türü bir alan olarak gönderinin üstünde durur ve iki ayrı yerde `switch`
ile çözülür: ücretin tabanı ve fatura satırının açıklaması.

```js
// v1/ucret.mjs — tarife turu iki ayri switch ile secilir
export const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function taban(gonderi) {
  switch (gonderi.tarife) {
    case "agirlik":
      return gonderi.agirlik <= 1 ? 3900 : gonderi.agirlik <= 5 ? 6400 : 11800;
    case "sabit":
      return 4500;
    default:
      throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`);
  }
}

export function aciklama(gonderi) {
  switch (gonderi.tarife) {
    case "agirlik":
      return `agirlik kademesi (${gonderi.agirlik} kg)`;
    case "sabit":
      return "sabit ucret";
    default:
      throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`);
  }
}

export const ucret = (g) => Math.round((taban(g) * katsayi(g.adres)) / 100);
```

```js
// v1/kur.mjs — ornek gonderiler icin ucret satirlari
import { ucret, aciklama } from "./ucret.mjs";

export const ORNEK = [
  { tarife: "agirlik", agirlik: 0.8, adres: "34100", en: 20, boy: 20, yukseklik: 15 },
  { tarife: "sabit", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 },
];

for (const g of ORNEK) console.log(`${ucret(g)} kurus  ${aciklama(g)}`);
```

```sh
node v1/kur.mjs
```

```
3900 kurus  agirlik kademesi (0.8 kg)
5175 kurus  sabit ucret
```

Bu tasarımda tarife türü bir **veri**, davranış ise türü çözen kod. İki `switch` aynı
listeyi ayrı ayrı sayıyor; üçüncü bir kullanım yeri çıksa üçüncü bir liste olacak.

#### Türün Kayda Taşındığı Sürüm

İkinci sürümde tarife bir tür etiketi değil, davranışı taşıyan bir nesnedir. Nesneler ortak
bir kayda kendilerini yazar; hesap modülü kayda sorar ve tür adlarını hiç bilmez.

```js
// v2/kayit.mjs — tarife kaydi: yeni tarifeler bu tabloya kendini yazar
const TARIFELER = new Map();

export function kaydet(tarife) {
  TARIFELER.set(tarife.ad, tarife);
}

export function tarife(ad) {
  const t = TARIFELER.get(ad);
  if (t === undefined) throw new RangeError(`bilinmeyen tarife: ${ad}`);
  return t;
}
```

```js
// v2/tarifeler/agirlik.mjs — agirlik kademesi tarifesi
import { kaydet } from "../kayit.mjs";

kaydet({
  ad: "agirlik",
  taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800),
  aciklama: (g) => `agirlik kademesi (${g.agirlik} kg)`,
});
```

```js
// v2/tarifeler/sabit.mjs — sabit ucret tarifesi
import { kaydet } from "../kayit.mjs";

kaydet({ ad: "sabit", taban: () => 4500, aciklama: () => "sabit ucret" });
```

```js
// v2/ucret.mjs — tarife turune bakmayan hesap; secim kayittan gelir
import { tarife } from "./kayit.mjs";

export const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export const ucret = (g) => Math.round((tarife(g.tarife).taban(g) * katsayi(g.adres)) / 100);
export const aciklama = (g) => tarife(g.tarife).aciklama(g);
```

```js
// v2/kur.mjs — bilesim koku: kullanilacak tarifeleri yalniz burasi bilir
import "./tarifeler/agirlik.mjs";
import "./tarifeler/sabit.mjs";
import { ucret, aciklama } from "./ucret.mjs";

export const ORNEK = [
  { tarife: "agirlik", agirlik: 0.8, adres: "34100", en: 20, boy: 20, yukseklik: 15 },
  { tarife: "sabit", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 },
];

for (const g of ORNEK) console.log(`${ucret(g)} kurus  ${aciklama(g)}`);
```

```sh
node v2/kur.mjs
```

```
3900 kurus  agirlik kademesi (0.8 kg)
5175 kurus  sabit ucret
```

Aynı çıktı. Fark davranışta değil, yeni bir türün nereye yazılacağında. Tarife adlarının
listesi tek bir yerde durur ve orası bileşim köküdür — Katman Sorumlulukları dersinde
tanıtılan, parçaları yalnızca kendisinin birbirine bağladığı modül.

#### Genişlemenin Ölçülmesi

Yeni gereksinim hacim tarifesidir: desi hesabı en, boy ve yükseklikten türetilir, taban ücret
desi başına artar. İki ağaç kopyalanır ve değişiklik ikisine de uygulanır.

```sh
cp -r v1 v1-yeni
cp -r v2 v2-yeni
```

```js
// v1-yeni/ucret.mjs — hacim tarifesi eklendi: iki switch de duzenlendi
export const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;
const desi = (g) => Math.ceil((g.en * g.boy * g.yukseklik) / 5000);

export function taban(gonderi) {
  switch (gonderi.tarife) {
    case "agirlik":
      return gonderi.agirlik <= 1 ? 3900 : gonderi.agirlik <= 5 ? 6400 : 11800;
    case "sabit":
      return 4500;
    case "hacim":
      return 2600 + desi(gonderi) * 700;
    default:
      throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`);
  }
}

export function aciklama(gonderi) {
  switch (gonderi.tarife) {
    case "agirlik":
      return `agirlik kademesi (${gonderi.agirlik} kg)`;
    case "sabit":
      return "sabit ucret";
    case "hacim":
      return `hacim tarifesi (${desi(gonderi)} desi)`;
    default:
      throw new RangeError(`bilinmeyen tarife: ${gonderi.tarife}`);
  }
}

export const ucret = (g) => Math.round((taban(g) * katsayi(g.adres)) / 100);
```

```js
// v2-yeni/tarifeler/hacim.mjs — hacim tarifesi: hesap modulu duzenlenmedi
import { kaydet } from "../kayit.mjs";

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

kaydet({
  ad: "hacim",
  taban: (g) => 2600 + desi(g) * 700,
  aciklama: (g) => `hacim tarifesi (${desi(g)} desi)`,
});
```

Kalan iki düzenleme her iki ağaçta da örnek veriye ve kayıt listesine aittir; aşağıdaki
betik bunları uygular, iki sürümü çalıştırır ve farkı sayar. Yerinde düzenlemede yedek
uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde aynı davranır.

```sh
ORNEK='  { tarife: "hacim", agirlik: 3.0, adres: "06500", en: 30, boy: 40, yukseklik: 50 },'
sed -i.y "s#^];#$ORNEK\n];#" v1-yeni/kur.mjs v2-yeni/kur.mjs
sed -i.y 's#^import "./tarifeler/sabit.mjs";#&\nimport "./tarifeler/hacim.mjs";#' v2-yeni/kur.mjs
rm -f v1-yeni/*.y v2-yeni/*.y
node v1-yeni/kur.mjs
node v2-yeni/kur.mjs

olc() {
  echo "$1 -> $2"
  echo "  duzenlenen var olan dosya = $(diff -rq $1 $2 | grep -c '^Files ')"
  echo "  yeni dosya                = $(diff -rq $1 $2 | grep -c '^Only in ')"
  echo "  eklenen satir             = $(diff -rN $1 $2 | grep '^>' | grep -cvE '^> *(//|$)')"
  echo "  hesap modulu degisti mi   = $(diff -qI '^//' $1/ucret.mjs $2/ucret.mjs > /dev/null && echo hayir || echo evet)"
}
olc v1 v1-yeni
olc v2 v2-yeni
```

```
3900 kurus  agirlik kademesi (0.8 kg)
5175 kurus  sabit ucret
12650 kurus  hacim tarifesi (12 desi)
3900 kurus  agirlik kademesi (0.8 kg)
5175 kurus  sabit ucret
12650 kurus  hacim tarifesi (12 desi)
v1 -> v1-yeni
  duzenlenen var olan dosya = 2
  yeni dosya                = 0
  eklenen satir             = 6
  hesap modulu degisti mi   = evet
v2 -> v2-yeni
  duzenlenen var olan dosya = 1
  yeni dosya                = 1
  eklenen satir             = 9
  hesap modulu degisti mi   = hayir
```

Sonuçlar aynı; maliyetin dağılımı farklı. İkinci sürüm daha çok satır ekledi (9'a 6), çünkü
yeni tarife kendi dosyasında tam bir nesne olarak duruyor. Buna karşılık düzenlenen var olan
dosya sayısı ikiden bire indi ve bu tek dosya bileşim köküdür. Son satır belirleyici olan:
ücret hesabını yapan modül birinci sürümde değişti, ikincisinde değişmedi.

Karşılaştırma iki tür maliyeti ayırıyor. Eklenen satır ucuzdur: yeni metin var olan bir
davranışı bozamaz. Düzenlenen satır pahalıdır: `taban` ve `aciklama` işlevlerinin daha önce
doğru çalışan iki tarifesi, üçüncü tarife eklenirken yeniden gözden geçirilmek zorundadır.
İlkenin ölçüsü "kaç satır yazıldı" değil, "kaç satır yeniden doğrulanmak zorunda kaldı"
sorusudur.

#### Kapalılığın Sınırı

Kapalılık evrensel olamaz. İkinci sürüm yeni **tarife türüne** kapalıdır, ama tarife
sözleşmesinin kendisine kapalı değildir. Tarife nesnesine üçüncü bir yöntem eklemek — örneğin
azami desi sınırı — kayıttaki bütün tarife dosyalarını düzenlemeyi gerektirir; maliyet
tarife sayısıyla doğru orantılı büyür.

Seçim bu yüzden hangi eksenin değişeceğinin tahminine dayanır. Tarife sayısı artıyorsa
ikinci sürüm kazanır; tarife sözleşmesi oynuyor ve tür sayısı iki üçte kalıyorsa `switch`
daha ucuzdur, çünkü kayıt düzeneğinin kendisi de bir maliyettir. İlke, her yerde soyutlama
kurmayı değil, hangi eksende değişiklik geldiğini ölçüp o eksene kapanmayı söyler. Ölçünün
kaynağı da tahmin değil, geçmiş genişleme istekleridir.

#### Özet

- Açık–kapalı ilkesi genişleme (yeni metin eklemek) ile değişiklik (var olan metni
  düzenlemek) arasında ayrım yapar ve ikincisini sınırlar.
- Aynı gereksinim iki tasarıma uygulandığında `switch` sürümü 2 var olan dosyayı düzenledi ve
  hesap modülünü değiştirdi; kayıt sürümü 1 dosya düzenledi, 1 yeni dosya ekledi ve hesap
  modülüne dokunmadı.
- Kayıt sürümü daha çok satır ekledi (9'a 6); ilkenin ölçüsü yazılan satır sayısı değil,
  yeniden doğrulanmak zorunda kalan satır sayısıdır.
- Kapalılık tek bir eksende geçerlidir: kayıt sürümü yeni tarife türüne kapalıdır, tarife
  sözleşmesinin değişmesine değil.
- Hangi eksene kapanılacağı tahminle değil, geçmişte gelen genişleme istekleriyle belirlenir.

#### Sonraki Adım

Kayıt sürümünde bütün tarifeler aynı sözleşmeyi karşıladığı için hesap modülü hangi tarifeyi
aldığını sormadan çalıştı. Bu, sözleşmenin gerçekten karşılandığı varsayımına dayanıyor.
Yeni bir tarife `taban` yöntemini tanımlar ama negatif değer döndürürse, ya da yalnız belli
gönderilerde çalışıp kalanında hata atarsa, hesap modülü değişmeden bozulur. Sonraki ders
alt tip sözleşmesinin ne anlama geldiğini tanımlar, sözleşmeyi bozan bir tarife yazar ve
üst tip üzerinden çalışan bir sözleşme sınamasını iki tarifeyle koşturarak bozan sürümde
kırıldığını gösterir.

### Liskov Yerine Koyma İlkesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/liskov-yerine-koyma-ilkesi

Alt tip sözleşmesinin ön koşul ve son koşul üzerinden tanımlanması: üst tip üzerinden çalışan bir sözleşme sınamasının kayıttaki bütün tarifelerle koşturulması, ön koşulu güçlendiren ve son koşulu zayıflatan iki tarifenin sınamayı kırması ve sözleşmeyi bozmadan aynı gereksinimin karşılanması.

Açık–kapalı ilkesi tarife türünü bir kayda taşıdı; hesap modülü hangi tarifeyi aldığını
sormadan çalıştı. Bu, kaydın içindeki her nesnenin aynı sözleşmeyi gerçekten karşıladığı
varsayımına dayanıyor. Varsayım yazılı değildi: kayıt yalnız `taban` ve `aciklama` adlarının
var olduğunu görüyordu, bu işlevlerin neyi kabul edip ne döndürdüğünü görmüyordu.

**Liskov yerine koyma ilkesi** (Liskov substitution principle) o varsayımı bir kurala
çevirir: üst tipin beklendiği her yerde alt tiplerinden biri, programın doğruluğunu bozmadan
konabilmelidir. TypeScript kursunda tanımlanan **alt tip** ilişkisi tip denetleyicisinin
gördüğü bir yapı uyumudur; bu ilke aynı ilişkiyi davranış düzeyinde ister. Kural iki
maddeye iner: alt tip, üst tipin kabul ettiğinden **daha azını isteyemez** (ön koşulu
güçlendiremez) ve üst tipin vaat ettiğinden **daha azını veremez** (son koşulu
zayıflatamaz).

#### Sözleşmenin Yazılması

Sözleşme üst tipin yanında, yorum olarak değil, sınanabilir bir biçimde durmalıdır. İlk adım
üst tipi tanımlamak ve beklentiyi açıkça yazmaktır.

```js
// tarife.mjs — ust tip ve karsilanmasi gereken sozlesme
export class Tarife {
  constructor(ad) {
    this.ad = ad;
  }

  // On kosul: gonderi agirlik, en, boy, yukseklik ve adres alanlarini tasir.
  // Son kosul: pozitif tam sayi (kurus) doner, ayni gonderi icin ayni deger.
  taban(gonderi) {
    throw new Error(`${this.ad}: taban gerceklestirilmedi (${typeof gonderi})`);
  }

  // Son kosul: bos olmayan bir dize doner.
  aciklama(gonderi) {
    throw new Error(`${this.ad}: aciklama gerceklestirilmedi (${typeof gonderi})`);
  }
}
```

```js
// tarifeler.mjs — sozlesmeyi karsilayan uc alt tip
import { Tarife } from "./tarife.mjs";

export class AgirlikTarifesi extends Tarife {
  constructor() { super("agirlik"); }
  taban(g) { return g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800; }
  aciklama(g) { return `agirlik kademesi (${g.agirlik} kg)`; }
}

export class SabitTarife extends Tarife {
  constructor() { super("sabit"); }
  taban() { return 4500; }
  aciklama() { return "sabit ucret"; }
}

export class HacimTarifesi extends Tarife {
  constructor() { super("hacim"); }
  desi(g) { return Math.ceil((g.en * g.boy * g.yukseklik) / 5000); }
  taban(g) { return 2600 + this.desi(g) * 700; }
  aciklama(g) { return `hacim tarifesi (${this.desi(g)} desi)`; }
}
```

Kayıt bir önceki dersteki işi görür; tek farkı nesne yerine örnek tutmasıdır.

```js
// kayit.mjs — tarife kaydi: ad ile erisim ve kayitli tarifelerin listesi
const TABLO = new Map();

export function kaydet(t) {
  TABLO.set(t.ad, t);
}

export function tarife(ad) {
  const t = TABLO.get(ad);
  if (t === undefined) throw new RangeError(`bilinmeyen tarife: ${ad}`);
  return t;
}

export const hepsi = () => [...TABLO.values()];
```

```js
// kurulum.mjs — sozlesmeyi karsilayan uc tarifeyi kayda yazar
import { kaydet } from "./kayit.mjs";
import { AgirlikTarifesi, SabitTarife, HacimTarifesi } from "./tarifeler.mjs";

kaydet(new AgirlikTarifesi());
kaydet(new SabitTarife());
kaydet(new HacimTarifesi());
```

```js
// ucret.mjs — hesap: tarifenin turunu bilmez, yalniz sozlesmeye guvenir
import { tarife } from "./kayit.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export const ucret = (g) => Math.round((tarife(g.tarife).taban(g) * katsayi(g.adres)) / 100);
export const aciklama = (g) => tarife(g.tarife).aciklama(g);
```

```js
// kur.mjs — bilesim koku: kurulumu yukler, ucret satirlarini yazar
import "./kurulum.mjs";
import { ucret, aciklama } from "./ucret.mjs";

const ORNEK = [
  { tarife: "agirlik", agirlik: 0.8, en: 20, boy: 20, yukseklik: 15, adres: "34100" },
  { tarife: "sabit", agirlik: 3.0, en: 30, boy: 40, yukseklik: 50, adres: "06500" },
  { tarife: "hacim", agirlik: 12.0, en: 60, boy: 40, yukseklik: 40, adres: "65200" },
];

for (const g of ORNEK) console.log(`${ucret(g)} kurus  ${aciklama(g)}`);
```

```sh
node kur.mjs
```

```
3900 kurus  agirlik kademesi (0.8 kg)
5175 kurus  sabit ucret
23240 kurus  hacim tarifesi (20 desi)
```

#### Üst Tip Üzerinden Çalışan Sınama

Sözleşme sınaması alt tiplerin adlarını bilmez. Kayıttaki her tarifeyi sırayla alır ve üst
tipin iki maddesini denetler. Ölçüsü tek bir sayıdır: kırılan sınama sayısı.

```js
// sozlesme-sinamasi.mjs — ust tip uzerinden calisir, alt tiplerin adini bilmez
import test from "node:test";
import assert from "node:assert/strict";
import { Tarife } from "./tarife.mjs";
import { hepsi } from "./kayit.mjs";

// Ust tipin kabul ettigi gonderiler. Hicbir alt tip bunlari reddedemez.
const GONDERILER = [
  { agirlik: 0.8, en: 20, boy: 20, yukseklik: 15, adres: "34100" },
  { agirlik: 3.0, en: 30, boy: 40, yukseklik: 50, adres: "06500" },
  { agirlik: 12.0, en: 60, boy: 40, yukseklik: 40, adres: "65200" },
];

const dene = (f) => {
  try { return { deger: f(), hata: null }; } catch (e) { return { deger: null, hata: e.message }; }
};

await import(process.env.KURULUM ?? "./kurulum.mjs");

for (const t of hepsi()) {
  test(`${t.ad}: ust tipin alt tipi`, () => {
    assert.ok(t instanceof Tarife, `${t.ad} ust tipten turemiyor`);
  });
  test(`${t.ad}: on kosul guclendirilmedi`, () => {
    for (const g of GONDERILER) {
      const s = dene(() => t.taban(g));
      assert.ok(s.hata === null, `${t.ad} ${g.adres} gonderisini reddetti: ${s.hata}`);
    }
  });
  test(`${t.ad}: son kosul zayiflatilmadi`, () => {
    for (const g of GONDERILER) {
      const s = dene(() => t.taban(g));
      assert.ok(Number.isInteger(s.deger) && s.deger > 0,
        `${t.ad} ${g.adres} icin taban = ${s.hata ?? s.deger}`);
      assert.ok(dene(() => t.aciklama(g)).deger?.length > 0, `${t.ad} ${g.adres} aciklamasi bos`);
    }
  });
}
```

Kurulum modülü bir ortam değişkeninden okunuyor; aynı sınama böylece farklı kayıtlarla
koşturulabilir. Rapor biçimi zaman ve dosya yolu içerdiği için süre ve konum satırları
ayıklanmıştır.

```sh
node --test --test-reporter=tap sozlesme-sinamasi.mjs 2>&1 |
  grep -E '^(ok|not ok|# (tests|pass|fail))'
```

```
ok 1 - agirlik: ust tipin alt tipi
ok 2 - agirlik: on kosul guclendirilmedi
ok 3 - agirlik: son kosul zayiflatilmadi
ok 4 - sabit: ust tipin alt tipi
ok 5 - sabit: on kosul guclendirilmedi
ok 6 - sabit: son kosul zayiflatilmadi
ok 7 - hacim: ust tipin alt tipi
ok 8 - hacim: on kosul guclendirilmedi
ok 9 - hacim: son kosul zayiflatilmadi
# tests 9
# pass 9
# fail 0
```

#### Sözleşmeyi Bozan İki Alt Tip

İki yeni gereksinim geliyor: yurt dışı gönderileri için ayrı bir tarife ve hafif gönderilere
ücretsiz kampanya. İkisi de üst tipten türetilerek, akla ilk gelen biçimde yazılmıştır.

```js
// bozan-tarifeler.mjs — sozlesmeyi iki ayri yerden bozan alt tipler
import { Tarife } from "./tarife.mjs";

// On kosulu guclendirir: ust tip her gonderiyi kabul ederken bu alt tip `ulke` ister.
export class YurtdisiTarifesi extends Tarife {
  constructor() { super("yurtdisi"); }
  taban(g) {
    if (g.ulke === undefined) throw new RangeError("yurtdisi: ulke alani zorunlu");
    return 9800 + Math.ceil(g.agirlik) * 1200;
  }
  aciklama(g) { return `yurtdisi (${g.ulke})`; }
}

// Son kosulu zayiflatir: ust tip pozitif tam sayi vaat ederken bu alt tip 0 dondurur.
export class KampanyaTarifesi extends Tarife {
  constructor() { super("kampanya"); }
  taban(g) { return g.agirlik <= 1 ? 0 : 3900; }
  aciklama() { return "kampanya"; }
}
```

```js
// kurulum-bozuk.mjs — ayni uc tarife, ustune sozlesmeyi bozan iki tarife
import "./kurulum.mjs";
import { kaydet } from "./kayit.mjs";
import { YurtdisiTarifesi, KampanyaTarifesi } from "./bozan-tarifeler.mjs";

kaydet(new YurtdisiTarifesi());
kaydet(new KampanyaTarifesi());
```

Aynı sınama, değiştirilmeden, ikinci kurulumla koşturulur.

```sh
KURULUM=./kurulum-bozuk.mjs node --test --test-reporter=tap sozlesme-sinamasi.mjs 2>&1 |
  grep -E '^(not ok|  error:|# (tests|pass|fail))'
```

```
not ok 11 - yurtdisi: on kosul guclendirilmedi
  error: 'yurtdisi 34100 gonderisini reddetti: yurtdisi: ulke alani zorunlu'
not ok 12 - yurtdisi: son kosul zayiflatilmadi
  error: 'yurtdisi 34100 icin taban = yurtdisi: ulke alani zorunlu'
not ok 15 - kampanya: son kosul zayiflatilmadi
  error: 'kampanya 34100 icin taban = 0'
# tests 15
# pass 12
# fail 3
```

Sınama sayısı dokuzdan on beşe çıktı, üçü kırıldı. Her iki tarife de `extends Tarife` yazıyor
ve iki yöntemi de tanımlıyor; yapı uyumu tamam. Kırılan, davranış sözleşmesidir.

#### İhlalin Koşumdaki Karşılığı

Kırılan sınama soyut bir uyarı değil. Hesap modülü hiç değişmeden, aynı kayıt üzerinden
çalıştırıldığında sonuç görünür.

```js
// kur-bozuk.mjs — hesap modulu degismeden, bozan tarifelerle calisir
import "./kurulum-bozuk.mjs";
import { ucret, aciklama } from "./ucret.mjs";

const ORNEK = [
  { tarife: "kampanya", agirlik: 0.8, en: 20, boy: 20, yukseklik: 15, adres: "34100" },
  { tarife: "yurtdisi", agirlik: 3.0, en: 30, boy: 40, yukseklik: 50, adres: "06500" },
];

for (const g of ORNEK) {
  try {
    console.log(`${g.tarife.padEnd(9)} -> ${ucret(g)} kurus  ${aciklama(g)}`);
  } catch (e) {
    console.log(`${g.tarife.padEnd(9)} -> ${e.constructor.name}: ${e.message}`);
  }
}
```

```sh
node kur-bozuk.mjs
```

```
kampanya  -> 0 kurus  kampanya
yurtdisi  -> RangeError: yurtdisi: ulke alani zorunlu
```

İki sonuç iki ayrı ihlal biçimini gösteriyor. Güçlendirilmiş ön koşul bir hataya dönüştü:
hesap modülü kendi gönderdiği veriyle reddedildi. Zayıflatılmış son koşul daha sessiz —
sıfır kuruşluk bir ücret üretildi ve hiçbir yerde hata görünmedi. İkinci biçim tehlikelidir,
çünkü ancak faturaya bakan biri fark eder.

#### Sözleşmeyi Bozmadan Aynı Gereksinim

İki gereksinim de karşılanabilir; sözleşmeyi bozan, gereksinimlerin kendisi değil,
yazılışlarıdır. Ön koşul, eksik alanı hata yerine tanımlı bir varsayılana çevirerek
genişletilir. Son koşul, indirimi asgari ücretle sınırlayarak korunur.

```js
// uyumlu-tarifeler.mjs — ayni iki gereksinim, sozlesme bozulmadan
import { Tarife } from "./tarife.mjs";

// On kosul genisletilmedi: ulke alani yoksa adresten turetilen yurtici degeri kullanilir.
export class YurtdisiTarifesi extends Tarife {
  constructor() { super("yurtdisi"); }
  taban(g) { return 9800 + Math.ceil(g.agirlik) * 1200; }
  aciklama(g) { return `yurtdisi (${g.ulke ?? "yurtici"})`; }
}

// Son kosul korundu: indirim asgari ucretin altina inemez.
export class KampanyaTarifesi extends Tarife {
  constructor() { super("kampanya"); }
  taban(g) { return Math.max(1500, g.agirlik <= 1 ? 0 : 3900); }
  aciklama() { return "kampanya"; }
}
```

```js
// kurulum-uyumlu.mjs — uc tarife, ustune sozlesmeyi karsilayan iki tarife
import "./kurulum.mjs";
import { kaydet } from "./kayit.mjs";
import { YurtdisiTarifesi, KampanyaTarifesi } from "./uyumlu-tarifeler.mjs";

kaydet(new YurtdisiTarifesi());
kaydet(new KampanyaTarifesi());
```

```sh
KURULUM=./kurulum-uyumlu.mjs node --test --test-reporter=tap sozlesme-sinamasi.mjs 2>&1 |
  grep -E '^(not ok|  error:|# (tests|pass|fail))'
```

```
# tests 15
# pass 15
# fail 0
```

On beş sınamanın tamamı geçiyor. Üçüncü bir seçenek de vardır ve bazen doğrusu odur: bir
gereksinim üst tipin kabul ettiğinden gerçekten fazlasını istiyorsa, o gereksinim bir alt tip
değildir. Yurt dışı ücretlendirmesi ülke bilgisi olmadan anlamsızsa, `Tarife` kaydına
yazılmamalı, kendi sözleşmesiyle ayrı bir tip olmalıdır. İlkenin söylediği "her şey aynı üst
tipten türesin" değil, "türeyen her şey üst tipin sözünü tutsun" biçimindedir.

#### Sözleşmenin Kapsamı

Ön koşul ve son koşul sözleşmenin tamamı değildir. Üst tipin koruduğu değişmezler de alt
tipte geçerli kalmalıdır: `taban` aynı gönderi için hep aynı değeri döndürüyorsa, alt tip
rastgele bir katsayı ekleyemez. Sınamanın üç maddesi bu yüzden aynı gönderi listesini
kullanır; bir alt tip ikinci çağrıda farklı sonuç verirse üçüncü madde kırılır.

Sözleşmenin yazılı olmadığı durumda ne olduğu da ölçüldü: kayıt beş tarifeyi de kabul etti,
hesap modülü ikisini de çalıştırdı, biri sessizce yanlış ücret üretti. Sözleşme yazılınca
aynı kayıt üç kırık sınama verdi. Fark, kodun kendisinde değil, hatanın nerede görüldüğünde.

#### Özet

- Liskov yerine koyma ilkesi alt tipin yapı uyumunu değil davranış uyumunu ister: üst tipin
  beklendiği yerde alt tip programın doğruluğunu bozmadan kullanılabilmelidir.
- Kural iki maddedir: alt tip ön koşulu güçlendiremez ve son koşulu zayıflatamaz.
- Sözleşme sınaması üst tip üzerinden yazılır ve alt tiplerin adını bilmez; kayıttaki her tipi
  aynı ölçütle geçirir.
- Üç uyumlu tarifeyle 9 sınamanın tamamı geçti; ön koşulu güçlendiren ve son koşulu
  zayıflatan iki tarife eklendiğinde 15 sınamanın 3'ü kırıldı.
- Güçlendirilmiş ön koşul koşumda hataya dönüştü, zayıflatılmış son koşul sessizce sıfır
  kuruşluk ücret üretti; ikinci biçim sözleşme sınaması olmadan görünmez.

#### Sonraki Adım

Sözleşme sınaması iki yöntem üzerinden yürüdü ve her tarife ikisini de gerçekten
kullanıyordu. Tarife arayüzü büyüdüğünde bu kolaylık kalkar. Taşıyıcı seçimi, rota planlama
ve teslimat durumu tek bir taşıyıcı arayüzünde toplandığında, yalnız ücret hesabıyla
ilgilenen bir istemci de kullanmadığı yöntemlere bağlanır; sahte bir taşıyıcı yazmak için o
yöntemlerin hepsini doldurmak gerekir. Sonraki ders istemcilerin gerçekten çağırdığı yöntem
sayısı ile arayüzün dayattığı yöntem sayısını ölçer ve şişkin arayüzün sahte bağımlılık
düzeneğine maliyetini sayar.

### Arayüz Ayrımı İlkesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/arayuz-ayrimi-ilkesi

Şişkin bir taşıyıcı arayüzünün istemcilere maliyetinin sayılması: her istemcinin gerçekten çağırdığı yöntem sayısı ile arayüzün dayattığı yöntem sayısının taranarak karşılaştırılması, sahte bağımlılıklardaki toplam yöntem sayısının ölçülmesi ve arayüze dokuzuncu bir yöntem eklendiğinde kaç sahtenin kırıldığının koşumla bulunması.

Sözleşme sınaması iki yöntem üzerinden yürüdü ve her tarife ikisini de gerçekten
kullanıyordu. Taşıyıcı tarafına geçildiğinde bu denge bozulur: taşıyıcı seçimi, rota planlama
ve teslimat takibi aynı nesnenin üstünde toplanır. Tek bir taşıyıcı arayüzü kurulduğunda
yalnız ücret hesaplayan bir istemci de rota ve takip yöntemlerine bağlanır.

**Arayüz ayrımı ilkesi** (interface segregation principle) bunu yasaklar: hiçbir istemci
kullanmadığı yöntemlere bağımlı olmaya zorlanmamalıdır. İlkenin ölçüsü iki ayrı sayının
karşılaştırılmasıdır — istemcinin **çağırdığı** yöntem sayısı ile arayüzün **dayattığı**
yöntem sayısı.

#### Tek ve Geniş Sözleşme

İlk sürümde sekiz yöntemin tamamı tek bir sözleşmede toplanmıştır. Sözleşme bir liste ve onu
denetleyen bir doğrulayıcı olarak yazılır; istemciler doğrulayıcıyı çağırarak aldıkları
nesnenin sözleşmeyi karşıladığından emin olur.

```js
// v1/tasiyici.mjs — tek ve genis tasiyici sozlesmesi
export const TASIYICI_SOZLESMESI = [
  "bolgeKatsayisi", "tabanUcret", "aktarmaNoktalari", "tahminiSure",
  "durum", "teslimAlindi", "etiketBas", "iptalEt",
];

export function sozlesmeyiDogrula(t) {
  const eksik = TASIYICI_SOZLESMESI.filter((ad) => typeof t?.[ad] !== "function");
  if (eksik.length > 0) throw new TypeError(`tasiyici sozlesmesi eksik: ${eksik.join(", ")}`);
  return t;
}
```

Üç istemci bu sözleşmeye bağlanır. Her biri `GEREKEN` adıyla, bağlandığı sözleşmeyi dışa
verir; ölçüm betiği bu adı okuyacak.

```js
// v1/ucret-hesabi.mjs — ucret istemcisi: iki yontem cagirir, sekizini ister
import { sozlesmeyiDogrula, TASIYICI_SOZLESMESI } from "./tasiyici.mjs";

export const GEREKEN = TASIYICI_SOZLESMESI;

export function ucret(tasiyici, gonderi) {
  sozlesmeyiDogrula(tasiyici);
  return Math.round((tasiyici.tabanUcret(gonderi) * tasiyici.bolgeKatsayisi(gonderi.adres)) / 100);
}
```

```js
// v1/rota-planlayici.mjs — rota istemcisi
import { sozlesmeyiDogrula, TASIYICI_SOZLESMESI } from "./tasiyici.mjs";

export const GEREKEN = TASIYICI_SOZLESMESI;

export function rota(tasiyici, kaynak, hedef) {
  sozlesmeyiDogrula(tasiyici);
  const noktalar = tasiyici.aktarmaNoktalari(kaynak, hedef);
  return { noktalar, saat: tasiyici.tahminiSure(noktalar) };
}
```

```js
// v1/takip.mjs — takip istemcisi
import { sozlesmeyiDogrula, TASIYICI_SOZLESMESI } from "./tasiyici.mjs";

export const GEREKEN = TASIYICI_SOZLESMESI;

export function teslimEt(tasiyici, takipNo, imza) {
  sozlesmeyiDogrula(tasiyici);
  tasiyici.teslimAlindi(takipNo, imza);
  return tasiyici.durum(takipNo);
}
```

#### İki Sayının Ölçülmesi

Ölçüm betiği her istemcinin metninde `tasiyici.` ile başlayan çağrıları tarar ve bulduğu ayrı
yöntem adlarını sayar. Dayatılan sayı ise istemcinin bağlandığı sözleşmenin uzunluğudur.

```js
// arayuz-olc.mjs — istemcinin cagirdigi yontem sayisi ile arayuzun dayattigi sayi
import { readFileSync } from "node:fs";

const CAGRI = /\btasiyici\.(\w+)\(/g;
const kok = process.argv[2];
const istemciler = process.argv.slice(3);
let kullanilanToplam = 0;
let dayatilanToplam = 0;

for (const ad of istemciler) {
  const metin = readFileSync(`${kok}/${ad}`, "utf8");
  const kullanilan = new Set([...metin.matchAll(CAGRI)].map((m) => m[1]));
  const { GEREKEN } = await import(`./${kok}/${ad}`);
  kullanilanToplam += kullanilan.size;
  dayatilanToplam += GEREKEN.length;
  console.log(`${ad.padEnd(20)} kullanilan = ${kullanilan.size}  dayatilan = ${GEREKEN.length}` +
    `  fazlalik = ${GEREKEN.length - kullanilan.size}`);
}
console.log(`${kok}: kullanilan toplam = ${kullanilanToplam}, dayatilan toplam = ${dayatilanToplam}`);
```

```sh
node arayuz-olc.mjs v1 ucret-hesabi.mjs rota-planlayici.mjs takip.mjs
```

```
ucret-hesabi.mjs     kullanilan = 2  dayatilan = 8  fazlalik = 6
rota-planlayici.mjs  kullanilan = 2  dayatilan = 8  fazlalik = 6
takip.mjs            kullanilan = 2  dayatilan = 8  fazlalik = 6
v1: kullanilan toplam = 6, dayatilan toplam = 24
```

Altı çağrının karşılığı yirmi dört bağımlılık. Fazlalık kavramsal değil: her istemci,
kullanmadığı altı yöntemin adına ve varlığına bağlıdır.

#### Sahte Bağımlılığın Maliyeti

Fazlalığın ilk somut faturası sahte bağımlılıklarda çıkar. Her istemci için yalnız o
istemcinin çağırdığı yöntemleri taşıyan sahteler yazılır.

```js
// sahte-az.mjs — her sahte yalniz kendi istemcisinin cagirdigi yontemleri tasir
export const SAHTE = {
  ucret: { bolgeKatsayisi: () => 115, tabanUcret: () => 6400 },
  rota: { aktarmaNoktalari: (k, h) => [k, "41", h], tahminiSure: (n) => n.length * 6 },
  takip: { durum: () => "teslim edildi", teslimAlindi: () => undefined },
};
```

```js
// sahte-dene.mjs — verilen sahtelerle uc istemciyi calistirir, kirilani ve maliyeti sayar
const kok = process.argv[2];
const { SAHTE } = await import(process.argv[3]);
const { ucret } = await import(`./${kok}/ucret-hesabi.mjs`);
const { rota } = await import(`./${kok}/rota-planlayici.mjs`);
const { teslimEt } = await import(`./${kok}/takip.mjs`);

const isler = [
  ["ucret", () => ucret(SAHTE.ucret, { agirlik: 3.0, adres: "06500" })],
  ["rota", () => rota(SAHTE.rota, "34", "06")],
  ["takip", () => teslimEt(SAHTE.takip, "TK-1", "A.Y.")],
];

let kirilan = 0;
for (const [ad, f] of isler) {
  try {
    console.log(`${ad.padEnd(6)} -> ${JSON.stringify(f())}`);
  } catch (e) {
    kirilan += 1;
    console.log(`${ad.padEnd(6)} -> ${e.message}`);
  }
}
const yontem = Object.values(SAHTE).reduce((t, s) => t + Object.keys(s).length, 0);
console.log(`${kok} + ${process.argv[3]}: sahte yontem = ${yontem}, kirilan istemci = ${kirilan}`);
```

```sh
node sahte-dene.mjs v1 ./sahte-az.mjs
```

```
ucret  -> tasiyici sozlesmesi eksik: aktarmaNoktalari, tahminiSure, durum, teslimAlindi, etiketBas, iptalEt
rota   -> tasiyici sozlesmesi eksik: bolgeKatsayisi, tabanUcret, durum, teslimAlindi, etiketBas, iptalEt
takip  -> tasiyici sozlesmesi eksik: bolgeKatsayisi, tabanUcret, aktarmaNoktalari, tahminiSure, etiketBas, iptalEt
v1 + ./sahte-az.mjs: sahte yontem = 6, kirilan istemci = 3
```

Üç istemcinin üçü de kırıldı. Çalışması için her sahtenin sekiz yöntemi de taşıması gerekir;
altısı hiç çağrılmayacak dolgudur.

```js
// sahte-dolu.mjs — genis sozlesmeyi karsilamak icin doldurulmus sahteler
const BOS = () => undefined;

export const SAHTE = {
  ucret: {
    bolgeKatsayisi: () => 115, tabanUcret: () => 6400,
    aktarmaNoktalari: BOS, tahminiSure: BOS, durum: BOS, teslimAlindi: BOS,
    etiketBas: BOS, iptalEt: BOS,
  },
  rota: {
    aktarmaNoktalari: (k, h) => [k, "41", h], tahminiSure: (n) => n.length * 6,
    bolgeKatsayisi: BOS, tabanUcret: BOS, durum: BOS, teslimAlindi: BOS,
    etiketBas: BOS, iptalEt: BOS,
  },
  takip: {
    durum: () => "teslim edildi", teslimAlindi: BOS,
    bolgeKatsayisi: BOS, tabanUcret: BOS, aktarmaNoktalari: BOS, tahminiSure: BOS,
    etiketBas: BOS, iptalEt: BOS,
  },
};
```

```sh
node sahte-dene.mjs v1 ./sahte-dolu.mjs
```

```
ucret  -> 7360
rota   -> {"noktalar":["34","41","06"],"saat":18}
takip  -> "teslim edildi"
v1 + ./sahte-dolu.mjs: sahte yontem = 24, kirilan istemci = 0
```

Altı yöntemlik sahte düzeneği yirmi dörde çıktı. Aynı yük gerçek gerçekleştirimlerde de
görülür: yalnız takip hizmeti veren bir taşıyıcı da sekiz yöntemi doldurmak zorundadır.

#### Sözleşmenin İstemciye Göre Bölünmesi

İkinci sürümde sekiz yöntem, istemci sayısı kadar sözleşmeye ayrılır. Bölmenin ölçütü
yöntemlerin konusu değil, onları çağıran istemcidir.

```js
// v2/sozlesmeler.mjs — istemciye ozgu uc kucuk sozlesme
export const UCRET_SOZLESMESI = ["bolgeKatsayisi", "tabanUcret"];
export const ROTA_SOZLESMESI = ["aktarmaNoktalari", "tahminiSure"];
export const TAKIP_SOZLESMESI = ["durum", "teslimAlindi"];

export const dogrula = (ad, liste) => (t) => {
  const eksik = liste.filter((y) => typeof t?.[y] !== "function");
  if (eksik.length > 0) throw new TypeError(`${ad} sozlesmesi eksik: ${eksik.join(", ")}`);
  return t;
};
```

```js
// v2/ucret-hesabi.mjs — yalniz ucret sozlesmesine baglanir
import { dogrula, UCRET_SOZLESMESI } from "./sozlesmeler.mjs";

export const GEREKEN = UCRET_SOZLESMESI;
const dogrulaUcret = dogrula("ucret", UCRET_SOZLESMESI);

export function ucret(tasiyici, gonderi) {
  dogrulaUcret(tasiyici);
  return Math.round((tasiyici.tabanUcret(gonderi) * tasiyici.bolgeKatsayisi(gonderi.adres)) / 100);
}
```

```js
// v2/rota-planlayici.mjs — yalniz rota sozlesmesine baglanir
import { dogrula, ROTA_SOZLESMESI } from "./sozlesmeler.mjs";

export const GEREKEN = ROTA_SOZLESMESI;
const dogrulaRota = dogrula("rota", ROTA_SOZLESMESI);

export function rota(tasiyici, kaynak, hedef) {
  dogrulaRota(tasiyici);
  const noktalar = tasiyici.aktarmaNoktalari(kaynak, hedef);
  return { noktalar, saat: tasiyici.tahminiSure(noktalar) };
}
```

```js
// v2/takip.mjs — yalniz takip sozlesmesine baglanir
import { dogrula, TAKIP_SOZLESMESI } from "./sozlesmeler.mjs";

export const GEREKEN = TAKIP_SOZLESMESI;
const dogrulaTakip = dogrula("takip", TAKIP_SOZLESMESI);

export function teslimEt(tasiyici, takipNo, imza) {
  dogrulaTakip(tasiyici);
  tasiyici.teslimAlindi(takipNo, imza);
  return tasiyici.durum(takipNo);
}
```

Sekiz yöntemin tamamını sunan bir taşıyıcı üç sözleşmeyi de karşılar; bölünen arayüz,
gerçekleştirimin bölünmesini gerektirmez. Değişen yalnız istemcinin ne bildiğidir.

```sh
node arayuz-olc.mjs v2 ucret-hesabi.mjs rota-planlayici.mjs takip.mjs
node sahte-dene.mjs v2 ./sahte-az.mjs
```

```
ucret-hesabi.mjs     kullanilan = 2  dayatilan = 2  fazlalik = 0
rota-planlayici.mjs  kullanilan = 2  dayatilan = 2  fazlalik = 0
takip.mjs            kullanilan = 2  dayatilan = 2  fazlalik = 0
v2: kullanilan toplam = 6, dayatilan toplam = 6
ucret  -> 7360
rota   -> {"noktalar":["34","41","06"],"saat":18}
takip  -> "teslim edildi"
v2 + ./sahte-az.mjs: sahte yontem = 6, kirilan istemci = 0
```

Dayatılan toplam yirmi dörtten altıya, sahte düzeneği yirmi dört yöntemden altıya indi.

#### Dokuzuncu Yöntem

Asıl fark arayüz büyüdüğünde ortaya çıkar. Sigorta primi hesabı gerekiyor ve yeni yöntem
ücret tarafına aittir. İki ağaç kopyalanır, değişiklik ikisine de uygulanır. Yerinde
düzenlemede yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde aynı davranır.

```sh
cp -r v1 v1-yeni
cp -r v2 v2-yeni
sed -i.y 's/"etiketBas", "iptalEt",/"etiketBas", "iptalEt", "sigortaPrimi",/' v1-yeni/tasiyici.mjs
sed -i.y 's/UCRET_SOZLESMESI = \["bolgeKatsayisi", "tabanUcret"\]/UCRET_SOZLESMESI = ["bolgeKatsayisi", "tabanUcret", "sigortaPrimi"]/' v2-yeni/sozlesmeler.mjs
rm -f v1-yeni/*.y v2-yeni/*.y
node sahte-dene.mjs v1-yeni ./sahte-dolu.mjs
node sahte-dene.mjs v2-yeni ./sahte-az.mjs
```

```
ucret  -> tasiyici sozlesmesi eksik: sigortaPrimi
rota   -> tasiyici sozlesmesi eksik: sigortaPrimi
takip  -> tasiyici sozlesmesi eksik: sigortaPrimi
v1-yeni + ./sahte-dolu.mjs: sahte yontem = 24, kirilan istemci = 3
ucret  -> ucret sozlesmesi eksik: sigortaPrimi
rota   -> {"noktalar":["34","41","06"],"saat":18}
takip  -> "teslim edildi"
v2-yeni + ./sahte-az.mjs: sahte yontem = 6, kirilan istemci = 1
```

Ücret tarafına eklenen tek yöntem, geniş arayüzde üç istemcinin üçünü birden kırdı; bölünmüş
arayüzde yalnız ilgili olanı kırdı. Sigorta priminden hiç haberi olmayan rota planlayıcının
sınaması ilk sürümde bozuldu, ikincisinde bozulmadı.

#### Bölmenin Ölçütü ve Sınırı

Arayüzü bölmenin doğru ekseni istemcidir. Yöntemleri konularına göre gruplamak — "hesapla
ilgili olanlar", "kayıtla ilgili olanlar" — aynı sonucu vermez; iki farklı istemci aynı
konudan farklı alt kümeler kullanabilir. Ölçüt tarama sonucudur: hangi istemci hangi yöntem
adlarını çağırıyor.

Bölmenin de bir bedeli vardır. Üç sözleşme üç doğrulayıcı ve üç ad demektir; ayrıca bir
istemci gerçekten dört yöntemin dördünü de çağırıyorsa onu ikiye bölmek fazlalığı
azaltmaz, yalnız dosya sayısını artırır. İlkenin eşiği ölçümdedir: fazlalık sıfırsa bölmenin
kazancı da sıfırdır.

#### Özet

- Arayüz ayrımı ilkesi, istemcinin kullanmadığı yöntemlere bağımlı kalmasını yasaklar;
  ölçüsü çağrılan yöntem sayısı ile dayatılan yöntem sayısının farkıdır.
- Geniş sözleşmede üç istemcinin toplam çağrısı 6, dayatılan bağımlılık 24 bulundu; bölünmüş
  sözleşmelerde ikisi de 6 oldu.
- Fazlalığın ilk faturası sahte bağımlılıklardadır: geniş sözleşmeyi karşılamak için sahte
  düzeneği 6 yöntemden 24 yönteme çıktı, 18'i hiç çağrılmayan dolgudur.
- Sözleşmeye dokuzuncu bir yöntem eklendiğinde geniş sürümde 3 istemci, bölünmüş sürümde 1
  istemci kırıldı.
- Bölme ekseni yöntemlerin konusu değil, onları çağıran istemcidir; fazlalık sıfır ölçülen
  yerde bölme yalnız dosya sayısını artırır.

#### Sonraki Adım

Bölünmüş sözleşmeler `v2/sozlesmeler.mjs` dosyasında, istemcilerin yanında durdu. Bu yerleşim
tesadüf değil: sözleşmeyi kimin tanımladığı, arayüzün kaç parçaya bölündüğünden ayrı bir
karardır. Sözleşme taşıyıcı gerçekleştiriminin yanında tanımlansaydı, istemciler bölünmüş bir
arayüze bağlanmaya devam ederdi ama yine gerçekleştirimin bulunduğu tarafı ithal ederdi.
Sonraki ders bu iki yerleşimi ithal yönünü çıkararak karşılaştırır ve soyutlamanın hangi
tarafta tanımlandığını bir sayıya çevirir.

### Bağımlılığın Ters Çevrilmesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/bagimliligin-ters-cevrilmesi

Soyutlamanın hangi tarafta tanımlandığının sayıya çevrilmesi: kural ve taşıyıcı paketleri arasındaki ithal kenarlarının üç sürümde yönüyle sayılması, sözleşmenin sahibi ile giren kenar sayısının ölçülmesi ve taşıyıcı paketi olmadan kural modülünün yüklenip yüklenemediğinin koşumla sınanması.

Bölünmüş sözleşmeler istemcilerin yanında durdu, ama bu yerleşim ayrı bir karardı. Arayüzün
kaç parçaya bölündüğü ile o parçaların hangi pakette tanımlandığı birbirinden bağımsızdır:
küçük bir sözleşme de gerçekleştirimin yanında yaşayabilir. O durumda istemci küçük bir
arayüze bağlanır ama yine gerçekleştirimin bulunduğu paketi ithal eder.

**Bağımlılığın tersine çevrilmesi** (dependency inversion) SOLID'in beşinci maddesidir ve tam
bu soruyu ele alır: üst düzey kural, alt düzey gerçekleştirime değil, ikisinin de bağlandığı
bir soyutlamaya bağlanmalıdır; soyutlama da gerçekleştirimin değil kuralın tanımladığı bir şey
olmalıdır. Veri Erişim Katmanı ve İş Mantığı kursunda bu kural katmanlar arasındaki ithal
grafiği üzerinden denetlenmiş, ihlal sayısı ve ithal kapanışının boyutu ölçülmüştü. Buradaki
soru daha dar ve ölçüsü de farklı: iki paket arasındaki kenarlar hangi yöne bakıyor ve
sözleşme dosyası hangi pakette duruyor?

#### Üç Yerleşim

Kod tabanı iki pakete ayrılmıştır. `kural/` ücret kuralını, `tasiyici/` taşıyıcıya özgü taban
ücret hesaplarını tutar. Aynı davranış üç ayrı yerleşimle yazılır.

```sh
mkdir -p dogrudan/kural dogrudan/tasiyici arada/kural arada/tasiyici
mkdir -p tersine/kural tersine/tasiyici
```

Birinci yerleşimde soyutlama hiç yoktur; kural iki somut modülü doğrudan ithal eder ve
aralarında kendisi seçim yapar.

```js
// dogrudan/tasiyici/yurtici.mjs — yurtici tasiyicinin taban ucreti
export const yurticiTaban = (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800);
```

```js
// dogrudan/tasiyici/ekspres.mjs — ekspres tasiyicinin taban ucreti
export const ekspresTaban = (g) => 5200 + Math.ceil(g.agirlik) * 900;
```

```js
// dogrudan/kural/ucret.mjs — ust duzey kural iki somut tasiyiciyi kendisi ithal eder
import { yurticiTaban } from "../tasiyici/yurtici.mjs";
import { ekspresTaban } from "../tasiyici/ekspres.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(gonderi) {
  const taban = gonderi.tasiyici === "ekspres" ? ekspresTaban(gonderi) : yurticiTaban(gonderi);
  return Math.round((taban * katsayi(gonderi.adres)) / 100);
}
```

İkinci yerleşimde bir sözleşme vardır, ama taşıyıcı paketinde tanımlıdır.

```js
// arada/tasiyici/sozlesme.mjs — sozlesme var, ama tasiyici paketinde tanimli
export function tasiyiciyiDogrula(t) {
  if (typeof t?.taban !== "function") throw new TypeError("tasiyici sozlesmesi eksik: taban");
  return t;
}
```

```js
// arada/tasiyici/yurtici.mjs — sozlesmeyi karsilayan yurtici tasiyici
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

export const yurtici = tasiyiciyiDogrula({
  ad: "yurtici",
  taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800),
});
```

```js
// arada/tasiyici/ekspres.mjs — sozlesmeyi karsilayan ekspres tasiyici
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

export const ekspres = tasiyiciyiDogrula({
  ad: "ekspres",
  taban: (g) => 5200 + Math.ceil(g.agirlik) * 900,
});
```

```js
// arada/kural/ucret.mjs — kural sozlesmeye baglanir, ama sozlesme tasiyici paketinde
import { tasiyiciyiDogrula } from "../tasiyici/sozlesme.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(tasiyici, gonderi) {
  tasiyiciyiDogrula(tasiyici);
  return Math.round((tasiyici.taban(gonderi) * katsayi(gonderi.adres)) / 100);
}
```

Üçüncü yerleşimde sözleşme dosyası olduğu gibi kural paketine taşınır. Dosyanın içeriği
değişmez, yalnız yeri değişir.

```js
// tersine/kural/sozlesme.mjs — sozlesme kural paketinde tanimli
export function tasiyiciyiDogrula(t) {
  if (typeof t?.taban !== "function") throw new TypeError("tasiyici sozlesmesi eksik: taban");
  return t;
}
```

```js
// tersine/kural/ucret.mjs — kural yalniz kendi sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "./sozlesme.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };
const katsayi = (adres) => BOLGE[adres.slice(0, 2)] ?? 160;

export function ucret(tasiyici, gonderi) {
  tasiyiciyiDogrula(tasiyici);
  return Math.round((tasiyici.taban(gonderi) * katsayi(gonderi.adres)) / 100);
}
```

```js
// tersine/tasiyici/yurtici.mjs — gerceklestirim, kuralin sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "../kural/sozlesme.mjs";

export const yurtici = tasiyiciyiDogrula({
  ad: "yurtici",
  taban: (g) => (g.agirlik <= 1 ? 3900 : g.agirlik <= 5 ? 6400 : 11800),
});
```

```js
// tersine/tasiyici/ekspres.mjs — gerceklestirim, kuralin sozlesmesini ithal eder
import { tasiyiciyiDogrula } from "../kural/sozlesme.mjs";

export const ekspres = tasiyiciyiDogrula({
  ad: "ekspres",
  taban: (g) => 5200 + Math.ceil(g.agirlik) * 900,
});
```

Üç sürüm de aynı ücreti üretir.

```js
// ayni-sonuc.mjs — uc surum ayni ucreti uretir; fark davranista degil
const GONDERI = { tasiyici: "ekspres", agirlik: 3.0, adres: "06500" };

const { ucret: dogrudanUcret } = await import("./dogrudan/kural/ucret.mjs");
const { ucret: aradaUcret } = await import("./arada/kural/ucret.mjs");
const { ekspres: aradaEkspres } = await import("./arada/tasiyici/ekspres.mjs");
const { ucret: tersineUcret } = await import("./tersine/kural/ucret.mjs");
const { ekspres: tersineEkspres } = await import("./tersine/tasiyici/ekspres.mjs");

console.log("dogrudan =", dogrudanUcret(GONDERI), "kurus");
console.log("arada    =", aradaUcret(aradaEkspres, GONDERI), "kurus");
console.log("tersine  =", tersineUcret(tersineEkspres, GONDERI), "kurus");
```

```sh
node ayni-sonuc.mjs
```

```
dogrudan = 9085 kurus
arada    = 9085 kurus
tersine  = 9085 kurus
```

#### Kenarların Yönle Sayılması

Fark yalnız ithal grafiğinde görünür. Aşağıdaki betik iki paketteki bütün modülleri okur,
göreli ithal yollarını çözer ve paket sınırını geçen kenarları yönüne göre sayar. İkinci
ölçüsü soyutlamanın kendisiyle ilgilidir: sözleşme dosyası hangi pakette duruyor ve kaç modül
ona bağlanıyor?

```js
// yon-olc.mjs — iki paket arasindaki ithal kenarlarini ve soyutlamanin sahibini sayar
import { readdirSync, readFileSync } from "node:fs";
import { join, relative, resolve, dirname } from "node:path";

const ITHAL = /^\s*(?:import|export)[^;'"]*from\s+["'](\.[^"']+)["']/gm;
const paket = (yol) => yol.split("/")[0];
const kok = process.argv[2];
const kenarlar = [];

for (const p of ["kural", "tasiyici"]) {
  for (const ad of readdirSync(join(kok, p)).filter((a) => a.endsWith(".mjs")).sort()) {
    const kaynak = `${p}/${ad}`;
    for (const m of readFileSync(join(kok, kaynak), "utf8").matchAll(ITHAL)) {
      kenarlar.push([kaynak, relative(kok, resolve(kok, dirname(kaynak), m[1]))]);
    }
  }
}

const disa = kenarlar.filter(([a, b]) => paket(a) !== paket(b));
for (const [a, b] of disa) console.log(`  ${a} -> ${b}`);
const say = (p) => disa.filter(([a]) => paket(a) === p).length;
console.log(`${kok.padEnd(9)} kural -> tasiyici = ${say("kural")}` +
  `   tasiyici -> kural = ${say("tasiyici")}`);

const soyutlamalar = [...new Set(kenarlar.map(([, b]) => b))].filter((b) => b.endsWith("sozlesme.mjs"));
if (soyutlamalar.length === 0) {
  console.log(`${kok.padEnd(9)} soyutlama tanimli degil`);
}
for (const s of soyutlamalar) {
  const giren = kenarlar.filter(([, b]) => b === s).length;
  console.log(`${kok.padEnd(9)} soyutlamanin sahibi = ${paket(s)}, giren kenar = ${giren}`);
}
```

```sh
node yon-olc.mjs dogrudan
node yon-olc.mjs arada
node yon-olc.mjs tersine
```

```
  kural/ucret.mjs -> tasiyici/yurtici.mjs
  kural/ucret.mjs -> tasiyici/ekspres.mjs
dogrudan  kural -> tasiyici = 2   tasiyici -> kural = 0
dogrudan  soyutlama tanimli degil
  kural/ucret.mjs -> tasiyici/sozlesme.mjs
arada     kural -> tasiyici = 1   tasiyici -> kural = 0
arada     soyutlamanin sahibi = tasiyici, giren kenar = 3
  tasiyici/ekspres.mjs -> kural/sozlesme.mjs
  tasiyici/yurtici.mjs -> kural/sozlesme.mjs
tersine   kural -> tasiyici = 0   tasiyici -> kural = 2
tersine   soyutlamanin sahibi = kural, giren kenar = 3
```

Üç satır üç ayrı durumu ayırıyor. Birinci sürümde kuraldan gerçekleştirime iki kenar var ve
soyutlama yok. İkinci sürümde soyutlama var, giren kenar sayısı üç — yani sözleşme gerçekten
işini görüyor — ama sınırı geçen kenar sayısı ikiden bire indi, sıfıra inmedi: kural hâlâ
taşıyıcı paketini ithal ediyor. Üçüncü sürümde aynı sözleşme, aynı üç giren kenarla, kural
paketinde duruyor ve sınırı geçen kenarların tamamı ters yöne bakıyor.

İkinci sürüm ilkenin en sık yapılan yarım uygulamasıdır. Soyutlamanın var olması yetmez;
**kimin tanımladığı** ölçünün asıl belirleyicisidir.

#### Gerçekleştirim Olmadan Kural

Kenar yönü soyut bir ölçü gibi görünür; karşılığı somuttur. Her sürümden yalnız kural
paketini içeren bir kopya çıkarılır ve kural modülü tek başına yüklenmeye çalışılır.

```js
// kurali-dene.mjs — tasiyici paketi olmadan kural modulu yuklenebiliyor mu
const kok = process.argv[2];
try {
  const m = await import(`./${kok}/kural/ucret.mjs`);
  console.log(`${kok.padEnd(16)} -> yuklendi, disa verilen: ${Object.keys(m).join(", ")}`);
} catch (e) {
  console.log(`${kok.padEnd(16)} -> yuklenemedi, ${e.code}`);
}
```

```sh
for k in dogrudan arada tersine; do
  mkdir -p "$k-yalniz"
  cp -r "$k/kural" "$k-yalniz/kural"
  node kurali-dene.mjs "$k-yalniz"
done
```

```
dogrudan-yalniz  -> yuklenemedi, ERR_MODULE_NOT_FOUND
arada-yalniz     -> yuklenemedi, ERR_MODULE_NOT_FOUND
tersine-yalniz   -> yuklendi, disa verilen: ucret
```

Sözleşmesi olan ikinci sürüm de yüklenemedi. Kuralın gerçekleştirimden bağımsız
çözümlenebilmesi, sözleşmenin varlığına değil, sözleşmenin kural tarafında tanımlanmış
olmasına bağlı. Ölçünün sayı karşılığı budur: kuraldan gerçekleştirime giden kenar sayısı
sıfır olmadıkça kural tek başına ayakta duramaz.

#### İlkenin Sınırı ve Bedeli

Üçüncü sürümde ücret modülü hangi taşıyıcıyla çalışacağını bilmiyor; taşıyıcı ona dışarıdan
veriliyor. Bu, Frontend Mimarisi ve Birim Testi kurslarında kurulan **bağımlılık
enjeksiyonu**dur ve burada ilkenin bir sonucu olarak beliriyor: bağımlılığını kendisi
kurmayan bir modülün onu bir yerden alması gerekir. O yer bileşim köküdür.

Ters çevirmenin bedeli de sayılabilir: üçüncü sürümde iki paket arasında bir sözleşme dosyası
fazladan duruyor ve taşıyıcı seçimi artık ücret modülünün içinde değil, dışında. Gerçekleştirimin
tek olduğu ve değişmeyeceği bilinen bir yerde bu fazlalık karşılıksızdır. İlke her bağımlılığın
tersine çevrilmesini istemez; değişme hızı yüksek olan tarafın, değişme hızı düşük olan tarafa
bağlanmasını ister. Taşıyıcı listesi ücret kuralından daha sık değişiyorsa yön tersine
çevrilir; tersi doğruysa ölçüm de tersini söyler.

#### Özet

- Bağımlılığın tersine çevrilmesi, üst düzey kuralın gerçekleştirime değil soyutlamaya
  bağlanmasını ve soyutlamanın kural tarafından tanımlanmasını ister.
- Ölçü, paket sınırını geçen ithal kenarlarının yönüdür: kuraldan taşıyıcıya 2, 1 ve 0 kenar
  sayıldı; taşıyıcıdan kurala 0, 0 ve 2 kenar sayıldı.
- Soyutlamanın varlığı yetmez: sözleşmesi taşıyıcı paketinde duran sürümde giren kenar sayısı
  3 olmasına rağmen kural hâlâ taşıyıcı paketini ithal ediyordu.
- Kenar yönünün karşılığı koşumla görüldü: taşıyıcı paketi olmadan yalnız üçüncü sürümün kural
  modülü yüklendi, ilk ikisi `ERR_MODULE_NOT_FOUND` verdi.
- İlke her bağımlılığı değil, değişme hızı yüksek tarafa bakan bağımlılıkları çevirir;
  sözleşme dosyası ve dışarıdan verme düzeneği bunun bedelidir.

#### Sonraki Adım

Beş ilke birimin **kime** bağlanacağını söyledi: kaç aktöre, hangi genişleme eksenine, hangi
sözleşmeye, hangi büyüklükteki arayüze, hangi taraftaki soyutlamaya. Hiçbiri bağın kendisini
bir ölçüye bağlamadı. İki modül arasındaki bağın gücü nasıl sayılır — paylaşılan ad sayısı
mı, geçilen parametre sayısı mı, ortak değiştirilen durum mu? Bir modülün içindeki parçaların
birbirine gerçekten ait olup olmadığı nasıl anlaşılır? Sonraki konu bu iki ölçüyü kurar ve
ilkelerin buraya kadar niteliksel bıraktığı yargıları sayıya çevirir.

## Bağlaşım ve Uyum

İki modül arasındaki bağın gücünün ve bir modül içindeki parçaların birbirine ait olup olmadığının sayılması: bağlaşım türleri, uyum ölçütü, Demeter yasası, sorma, söyle ilkesi, komut–sorgu ayrımı, değişim eksenlerinin yalıtımı, somut tipe bağlanmanın maliyeti ve denetimin ters çevrilmesi.

### Bağlaşım Türleri

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/baglasim-turleri

İki modül arasındaki bağın gücünün sayılması: paylaşılan ad sayısı, geçilen parametre sayısı ve ortak değiştirilen durum ölçen bir betiğin yazılması, aynı fatura satırını üreten üç tasarımda ölçümlerin karşılaştırılması ve paylaşılan duruma yapılan tek satırlık bir dokunuşun yalnız bir tasarımı bozduğunun gösterilmesi.

SOLID ilkeleri bir birimin kime bağlanacağını söyledi ama bağın kendisini ölçmedi. "Gevşek
bağlı" ile "sıkı bağlı" arasındaki fark, bir tasarım yorumundan çıkarılmak yerine sayılabilir
olmalıdır. Bu konu iki ölçü kurar; ilki modüller **arasındaki** bağa ait.

**Bağlaşım** (coupling) iki modül arasındaki bağın gücüdür. Gücün kaynağı üç yerdedir: bir
modülün diğerinden aldığı **ad sayısı**, çağrılarda **geçirilen parametre sayısı** ve iki
modülün **ortak değiştirdiği durum**. Bağlaşım türleri bu üç ölçünün birleşiminden çıkar ve
klasik sıralaması en güçlüden en zayıfa doğrudur: içerik, ortak, denetim, damga, veri.

| Tür | İngilizce | Belirtisi |
|---|---|---|
| İçerik bağlaşımı | content coupling | Bir modül diğerinin iç adına erişir |
| Ortak bağlaşım | common coupling | İki modül aynı değiştirilebilir durumu paylaşır |
| Denetim bağlaşımı | control coupling | Çağrıya, çağrılanın hangi dalı seçeceğini söyleyen bayrak geçilir |
| Damga bağlaşımı | stamp coupling | Kullanılmayan alanları da olan bütün bir kayıt geçilir |
| Veri bağlaşımı | data coupling | Yalnız gereken sayı ya da dize geçilir |

Güçlü bağlaşım kötü olduğu için değil, **değişikliği yayan yüzeyi büyüttüğü** için sınırlanır.
Aşağıdaki üç tasarım aynı fatura satırını üretir; fark yalnız bu yüzeyin genişliğindedir.

#### Aynı Çıktı, Üç Tasarım

Birinci tasarım paylaşılan bir ayar nesnesi üzerinden çalışır. Hesap modülü sonucu hem
döndürür hem paylaşılan duruma yazar; fatura modülü sonucu oradan ve hesabın iç adından
okur.

```sh
mkdir -p ortak damga veri
```

```js
// ortak/ayar.mjs — butun modullerin okuyup yazdigi paylasilan durum
export const AYAR = { bolge: 100, kdv: 20, _son: 0 };
```

```js
// ortak/hesap.mjs — sonucu doner, ama once paylasilan duruma yazar
import { AYAR } from "./ayar.mjs";

export const _KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function hesapla(gonderi) {
  AYAR.bolge = BOLGE[gonderi.adres.slice(0, 2)] ?? 160;
  const taban = _KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
  AYAR._son = Math.round((taban * AYAR.bolge * (100 + AYAR.kdv)) / 10000);
  return AYAR._son;
}
```

```js
// ortak/fatura.mjs — sonucu paylasilan durumdan ve hesabin ic adindan okur
import { AYAR } from "./ayar.mjs";
import { hesapla, _KADEME } from "./hesap.mjs";

export function satir(gonderi) {
  hesapla(gonderi);
  return `${AYAR._son} kurus (${_KADEME.length} kademe)`;
}
```

İkinci tasarımda paylaşılan durum yoktur. Bunun yerine bütün gönderi kaydı ve hesabın hangi
dalı seçeceğini söyleyen bir bayrak geçirilir.

```js
// damga/hesap.mjs — paylasilan durum yok; butun kayit ve bir bayrak gecirilir
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export const kademeSayisi = () => KADEME.length;

export function hesapla(gonderi, kdvEkle) {
  const bolge = BOLGE[gonderi.adres.slice(0, 2)] ?? 160;
  const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
  const ham = (taban * bolge) / 100;
  return Math.round(kdvEkle ? (ham * 120) / 100 : ham);
}
```

```js
// damga/fatura.mjs — hesaba butun gonderiyi ve davranisi secen bayragi verir
import { hesapla, kademeSayisi } from "./hesap.mjs";

export const satir = (gonderi) => `${hesapla(gonderi, true)} kurus (${kademeSayisi()} kademe)`;
```

Üçüncü tasarımda hesap adımları ayrı işlevlere bölünür ve her biri yalnız ihtiyaç duyduğu
sayıyı alır. Bayrak, iki ayrı işlev adına dönüşür.

```js
// veri/hesap.mjs — her adim yalniz ihtiyac duydugu sayilari alir
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];

export const kademeSayisi = () => KADEME.length;
export const taban = (agirlik) => KADEME.find(([ust]) => agirlik <= ust)?.[1] ?? 15000;
export const bolgeli = (kurus, katsayi) => (kurus * katsayi) / 100;
export const kdvli = (kurus) => Math.round((kurus * 120) / 100);
```

```js
// veri/fatura.mjs — hesaba yalniz sayi gecer, bayrak yok
import { taban, bolgeli, kdvli, kademeSayisi } from "./hesap.mjs";

const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function satir(gonderi) {
  const katsayi = BOLGE[gonderi.adres.slice(0, 2)] ?? 160;
  return `${kdvli(bolgeli(taban(gonderi.agirlik), katsayi))} kurus (${kademeSayisi()} kademe)`;
}
```

#### Ölçer

Aşağıdaki betik bir dizindeki modülleri okur, her ithal deyimi için bir kenar üretir ve o
kenarın üç ölçüsünü hesaplar: alınan ad sayısı, çağrılarda geçirilen en yüksek parametre
sayısı ve ithal edilmiş bir ada yapılan yazma sayısı. Türü bu ölçülerden türetir; bir kenarda
birden çok tür bulunursa en güçlüsü etiket olur.

```js
// baglasim-olc.mjs — iki modul arasindaki bagin uc olcusu ve turu
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const ITHAL = /import\s*\{([^}]*)\}\s*from\s*["']\.\/([^"']+)["']/g;
// Butun kayit olarak gecirilen alan nesneleri. Bir cagrida bunlardan biri argumansa damga.
const KAYITLAR = ["gonderi", "tarife", "musteri"];

function cagriArgumanlari(metin, ad) {
  const cagrilar = [];
  for (const m of metin.matchAll(new RegExp(`\\b${ad}\\s*\\(`, "g"))) {
    let i = m.index + m[0].length;
    let derinlik = 1;
    let parca = "";
    const parcalar = [];
    while (i < metin.length && derinlik > 0) {
      const c = metin[i];
      if ("([{".includes(c)) derinlik += 1;
      else if (")]}".includes(c)) derinlik -= 1;
      if (derinlik === 0) break;
      if (c === "," && derinlik === 1) { parcalar.push(parca.trim()); parca = ""; } else parca += c;
      i += 1;
    }
    if (parca.trim() !== "") parcalar.push(parca.trim());
    cagrilar.push(parcalar);
  }
  return cagrilar;
}

const kok = process.argv[2];
const dosyalar = readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort();
const metinler = new Map(dosyalar.map((d) => [d, readFileSync(join(kok, d), "utf8")]));

// Once tum agacta hangi ithal edilmis adlara yazildigini topla: yazilan ad paylasilan durumdur.
const yazilan = new Set();
for (const [, metin] of metinler) {
  for (const m of metin.matchAll(/\b([A-Za-z_]\w*)\.\w+\s*=[^=]/g)) yazilan.add(m[1]);
}

let toplamAd = 0;
let toplamParam = 0;
let toplamYazma = 0;

for (const d of dosyalar) {
  const metin = metinler.get(d);
  for (const m of metin.matchAll(ITHAL)) {
    const adlar = m[1].split(",").map((s) => s.trim()).filter(Boolean);
    const hedef = m[2];
    let param = 0;
    let yazma = 0;
    const turler = new Set();
    for (const ad of adlar) {
      const cagrilar = cagriArgumanlari(metin, ad);
      const arg = cagrilar.flat();
      param += cagrilar.length === 0 ? 0 : Math.max(...cagrilar.map((c) => c.length));
      yazma += [...metin.matchAll(new RegExp(`\\b${ad}\\.\\w+\\s*=[^=]`, "g"))].length;
      if (ad.startsWith("_")) turler.add("icerik");
      if (yazilan.has(ad)) turler.add("ortak");
      if (arg.some((a) => a === "true" || a === "false")) turler.add("denetim");
      if (arg.some((a) => KAYITLAR.includes(a))) turler.add("damga");
    }
    if (turler.size === 0) turler.add("veri");
    const sira = ["icerik", "ortak", "denetim", "damga", "veri"];
    const enGuclu = sira.find((t) => turler.has(t));
    toplamAd += adlar.length;
    toplamParam += param;
    toplamYazma += yazma;
    console.log(`  ${d} -> ${hedef}`.padEnd(30) +
      `ad=${adlar.length} parametre=${param} yazma=${yazma}  tur=${enGuclu}` +
      (turler.size > 1 ? ` (${[...sira.filter((t) => turler.has(t))].join(", ")})` : ""));
  }
}
console.log(`${kok.padEnd(6)} toplam: ad=${toplamAd} parametre=${toplamParam} yazma=${toplamYazma}`);
```

```sh
node baglasim-olc.mjs ortak
node baglasim-olc.mjs damga
node baglasim-olc.mjs veri
```

```
  fatura.mjs -> ayar.mjs      ad=1 parametre=0 yazma=0  tur=ortak
  fatura.mjs -> hesap.mjs     ad=2 parametre=1 yazma=0  tur=icerik (icerik, damga)
  hesap.mjs -> ayar.mjs       ad=1 parametre=0 yazma=2  tur=ortak
ortak  toplam: ad=4 parametre=1 yazma=2
  fatura.mjs -> hesap.mjs     ad=2 parametre=2 yazma=0  tur=denetim (denetim, damga)
damga  toplam: ad=2 parametre=2 yazma=0
  fatura.mjs -> hesap.mjs     ad=4 parametre=4 yazma=0  tur=veri
veri   toplam: ad=4 parametre=4 yazma=0
```

Üç tasarım da aynı satırı üretiyor, ölçüleri farklı. Birinci tasarımda üç kenar var, ikisi
paylaşılan duruma bakıyor ve iki yazma sayıldı. İkinci tasarımda tek kenar kaldı; ad ve
parametre sayısı düşük ama tür denetim bağlaşımı. Üçüncü tasarımda yazma sıfır, tür veri
bağlaşımı, buna karşılık ad sayısı dörde ve parametre sayısı dörde çıktı.

Son satır ölçünün önemli bir yanını gösteriyor: **tür ile büyüklük ayrı şeylerdir**. Üçüncü
tasarım en zayıf bağlaşım türüne sahip, ama en çok adı paylaşan tasarım da o. Bağlaşımı
azaltmak "daha az ad geçir" demek değildir; geçen şeyin ne olduğunu değiştirmektir.

#### Değişikliğin Yayılması

Ölçünün karşılığı, ilgisiz bir yerden gelen bir dokunuşta görülür. Aşağıdaki betik üç
tasarımın satırını yazar, sonra yalnız paylaşılan ayar nesnesinin bir alanına dokunur ve
satırları yeniden yazar.

```js
// uzaktan-dokunus.mjs — ilgisiz bir modulun tek satiri uc tasarimi nasil etkiliyor
import { AYAR } from "./ortak/ayar.mjs";
import { satir as ortakSatir } from "./ortak/fatura.mjs";
import { satir as damgaSatir } from "./damga/fatura.mjs";
import { satir as veriSatir } from "./veri/fatura.mjs";

const G = { agirlik: 3.0, adres: "06500" };
const yaz = (etiket) =>
  console.log(`${etiket.padEnd(8)} ortak=${ortakSatir(G)}  damga=${damgaSatir(G)}  veri=${veriSatir(G)}`);

yaz("once");
AYAR.kdv = 0;
yaz("sonra");
```

```sh
node uzaktan-dokunus.mjs
```

```
once     ortak=8832 kurus (3 kademe)  damga=8832 kurus (3 kademe)  veri=8832 kurus (3 kademe)
sonra    ortak=7360 kurus (3 kademe)  damga=8832 kurus (3 kademe)  veri=8832 kurus (3 kademe)
```

Hesap modülüne, fatura modülüne, kademe tablosuna dokunulmadı. Değişen tek şey paylaşılan
nesnenin bir alanı oldu ve birinci tasarımın çıktısı 8832'den 7360'a düştü. Ortak bağlaşımın
ölçülebilir tanımı budur: bir modülün davranışı, onu hiç ithal etmemiş bir modül tarafından
değiştirilebiliyor.

#### Türlerin Somut Belirtileri

Denetim bağlaşımının belirtisi bir bayrak parametresidir. `hesapla(gonderi, true)` çağrısını
okuyan biri `true` ne demek diye sormak zorundadır; cevabı çağrılanın gövdesindedir. Bayrak
kaldırıldığında bilgi çağrının kendisine geçer: `kdvli(...)` ile `bolgeli(...)` ayrı adlardır
ve hangisinin çağrıldığı çağrı yerinde okunur.

Damga bağlaşımının belirtisi, çağrılanın aldığı kaydın alanlarından yalnız bir kısmını
kullanmasıdır. `hesapla(gonderi, true)` gönderinin ağırlık ve adres alanlarını kullanır;
kaydın geri kalanı — değer, teslim adresi, hacim — çağrıya girer ve kullanılmaz. Bu, gönderi
kaydına eklenen her yeni alanın hesap modülünün girdi yüzeyini büyütmesi demektir.

İçerik bağlaşımının belirtisi ad kuralıdır: `_KADEME` adı altçizgiyle başlayarak dışarı
verilmemesi gerektiğini söylüyor, ama fatura modülü onu yine de ithal ediyor. Ölçer bunu
yakaladı; kademe tablosunun biçimi değiştiğinde fatura modülü de değişecek.

#### Özet

- Bağlaşım iki modül arasındaki bağın gücüdür ve üç ölçüyle sayılır: paylaşılan ad sayısı,
  geçirilen parametre sayısı ve ortak değiştirilen durum.
- Türler en güçlüden en zayıfa içerik, ortak, denetim, damga ve veri bağlaşımıdır; bir kenarda
  birden çoğu bulunabilir, etiket en güçlüsüdür.
- Aynı fatura satırını üreten üç tasarımda ölçüm sırasıyla içerik, denetim ve veri bağlaşımı
  verdi; yazma sayısı 2, 0 ve 0 bulundu.
- Tür ile büyüklük ayrı ölçülerdir: en zayıf türe sahip tasarım aynı zamanda en çok adı (4)
  ve en çok parametreyi (4) paylaşan tasarımdı.
- Paylaşılan nesnenin tek bir alanına yapılan dokunuş yalnız ortak bağlaşımlı tasarımın
  çıktısını 8832'den 7360'a değiştirdi; diğer ikisi etkilenmedi.

#### Sonraki Adım

Bağlaşım ölçüsü modüller arasındaki kenarları saydı ve modülün içine hiç bakmadı. Üçüncü
tasarımın hesap modülü dört ad dışa veriyor; bu dört adın birbirine ait olup olmadığı sorusu
ölçünün dışında kaldı. Aynı dosyada duran işlevler ortak bir veriye mi dokunuyor, yoksa yalnız
aynı dosyada oldukları için mi yan yanalar? Sonraki ders modül içindeki işlevleri
dokundukları veri kümesine göre kümeler, düşük ilgililikli bir modülü bölerek ölçümü
tekrarlar ve bölmenin bağlaşım ölçüsünü nasıl etkilediğini sayar.

### Uyum

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/uyum

Modül içindeki parçaların birbirine ait olup olmadığının sayılması: işlevlerin dokundukları modül düzeyi adlara göre kümelenmesi, bileşen sayısı ile paylaşmayan işlev çifti sayısının hesaplanması, düşük uyumlu modülün bölünüp ölçümün tekrarlanması ve bölmenin yeni bağlaşım kenarı üretmediğinin gösterilmesi.

Bağlaşım ölçüsü modüller arasındaki kenarları saydı ve modülün içine hiç bakmadı. Bir modül
dört ad dışa veriyor olabilir; bu dört adın birbirine ait olup olmadığı sorusu ölçünün
dışındadır. Aynı dosyada duran işlevler ortak bir veriye mi dokunuyor, yoksa yalnız aynı
dosyada oldukları için mi yan yanalar?

**Uyum** (cohesion) bir modül içindeki parçaların birbirine ait olma derecesidir ve
bağlaşımın modül içi karşılığıdır. Ölçülebilir tanımı şudur: bir modülün işlevleri,
dokundukları modül düzeyi adlara göre kümelendiğinde kaç ayrı kümeye ayrılıyor? Tek kümeye
ayrılıyorsa modül tek bir şeyle ilgilidir; birden çok kümeye ayrılıyorsa modül birkaç ayrı
şeyi aynı dosyada tutuyordur.

#### Ölçülecek Modül

Aşağıdaki modül gönderiyle ilgili üç işi bir arada tutuyor: ücret hesabı, takip numarası
üretimi ve adres biçimleme. Üçü de "gönderi" sözcüğüyle anılabildiği için aynı dosyada
durmaları makul görünür.

```sh
mkdir -p birlesik bolunmus
```

```js
// birlesik/gonderi-servisi.mjs — gonderiyle ilgili her sey tek modulde
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };
const IL_ADLARI = { "34": "Istanbul", "06": "Ankara", "65": "Van" };
const ONEK = "GN";
let SAYAC = 0;

export function taban(agirlik) {
  return KADEME.find(([ust]) => agirlik <= ust)?.[1] ?? 15000;
}

export function bolgeKatsayisi(adres) {
  return BOLGE[adres.slice(0, 2)] ?? 160;
}

export function ucret(gonderi) {
  return Math.round((taban(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100);
}

export function takipNo() {
  SAYAC += 1;
  return `${ONEK}-${String(SAYAC).padStart(6, "0")}`;
}

export function takipGecerli(no) {
  return new RegExp(`^${ONEK}-\\d{6}$`).test(no);
}

export function adresYaz(adres) {
  return `${IL_ADLARI[adres.slice(0, 2)] ?? "bilinmeyen"} ${adres}`;
}
```

#### Kümeleme Ölçeri

Betik modülün üst düzey adlarını çıkarır, her işlevin gövdesinde hangi adların geçtiğini
bulur ve iki işlevi aynı kümeye koyar: ya ortak bir ada dokunuyorlarsa ya da biri diğerini
çağırıyorsa. Sonuçta bileşen sayısı ile ortak ada dokunan ve dokunmayan işlev çifti sayısı
raporlanır.

```js
// uyum-olc.mjs — modul icindeki islevleri dokunduklari adlara gore kumeler
import { readFileSync } from "node:fs";

function govde(metin, acilis) {
  let i = acilis;
  let derinlik = 0;
  do {
    if (metin[i] === "{") derinlik += 1;
    else if (metin[i] === "}") derinlik -= 1;
    i += 1;
  } while (i < metin.length && derinlik > 0);
  return metin.slice(acilis, i);
}

function coz(yol) {
  const metin = readFileSync(yol, "utf8");
  const veri = [...metin.matchAll(/^(?:export\s+)?(?:const|let)\s+(\w+)/gm)].map((m) => m[1]);
  const islevAdlari = [...metin.matchAll(/^(?:export\s+)?function\s+(\w+)/gm)].map((m) => m[1]);
  const tumu = [...veri, ...islevAdlari];
  const islevler = [];
  for (const m of metin.matchAll(/^(?:export\s+)?function\s+(\w+)\s*\(/gm)) {
    const g = govde(metin, metin.indexOf("{", m.index + m[0].length));
    const ref = tumu.filter((a) => a !== m[1] && new RegExp(`\\b${a}\\b`).test(g));
    islevler.push({ ad: m[1], ref: new Set(ref) });
  }
  return { veri, islevler };
}

const kesisiyor = (a, b) =>
  [...a.ref].some((x) => b.ref.has(x)) || a.ref.has(b.ad) || b.ref.has(a.ad);

for (const yol of process.argv.slice(2)) {
  const { veri, islevler } = coz(yol);
  const bilesen = new Map(islevler.map((f, i) => [f.ad, i]));
  let degisti = true;
  while (degisti) {
    degisti = false;
    for (const a of islevler) {
      for (const b of islevler) {
        if (a.ad !== b.ad && kesisiyor(a, b) && bilesen.get(a.ad) !== bilesen.get(b.ad)) {
          const enKucuk = Math.min(bilesen.get(a.ad), bilesen.get(b.ad));
          bilesen.set(a.ad, enKucuk);
          bilesen.set(b.ad, enKucuk);
          degisti = true;
        }
      }
    }
  }
  const kumeler = new Map();
  for (const [ad, k] of bilesen) kumeler.set(k, [...(kumeler.get(k) ?? []), ad]);

  let paylasan = 0;
  let paylasmayan = 0;
  for (let i = 0; i < islevler.length; i += 1) {
    for (let j = i + 1; j < islevler.length; j += 1) {
      if (kesisiyor(islevler[i], islevler[j])) paylasan += 1; else paylasmayan += 1;
    }
  }
  console.log(`${yol}  islev=${islevler.length} veri=${veri.length} bilesen=${kumeler.size}`);
  let n = 0;
  for (const [, adlar] of [...kumeler].sort((a, b) => a[0] - b[0])) {
    n += 1;
    console.log(`  bilesen ${n}: ${adlar.join(", ")}`);
  }
  console.log(`  paylasan cift=${paylasan} paylasmayan cift=${paylasmayan}` +
    ` uyumsuzluk=${Math.max(0, paylasmayan - paylasan)}`);
}
```

```sh
node uyum-olc.mjs birlesik/gonderi-servisi.mjs
```

```
birlesik/gonderi-servisi.mjs  islev=6 veri=5 bilesen=3
  bilesen 1: taban, bolgeKatsayisi, ucret
  bilesen 2: takipNo, takipGecerli
  bilesen 3: adresYaz
  paylasan cift=3 paylasmayan cift=12 uyumsuzluk=9
```

Altı işlev üç ayrı kümeye ayrıldı. On beş işlev çiftinden yalnız üçü ortak bir ada dokunuyor;
kalan on ikisinin hiçbir ortak noktası yok. Uyumsuzluk sayısı bu iki değerin farkıdır ve
dokuz çıktı. Modülün adı "gönderi servisi" olduğu için parçaların ilgili göründüğüne dikkat:
ölçüm ada değil, koda bakıyor.

#### Bölme ve Yeniden Ölçüm

Bileşenler bölmenin sınırını doğrudan veriyor. Üç bileşen üç modüle çıkarılır; işlev
gövdeleri değişmez, yalnız hangi dosyada durdukları değişir.

```js
// bolunmus/ucret.mjs — birinci bilesen: ucret hesabi
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function taban(agirlik) {
  return KADEME.find(([ust]) => agirlik <= ust)?.[1] ?? 15000;
}

export function bolgeKatsayisi(adres) {
  return BOLGE[adres.slice(0, 2)] ?? 160;
}

export function ucret(gonderi) {
  return Math.round((taban(gonderi.agirlik) * bolgeKatsayisi(gonderi.adres)) / 100);
}
```

```js
// bolunmus/takip.mjs — ikinci bilesen: takip numarasi
const ONEK = "GN";
let SAYAC = 0;

export function takipNo() {
  SAYAC += 1;
  return `${ONEK}-${String(SAYAC).padStart(6, "0")}`;
}

export function takipGecerli(no) {
  return new RegExp(`^${ONEK}-\\d{6}$`).test(no);
}
```

```js
// bolunmus/adres.mjs — ucuncu bilesen: adres bicimleme
const IL_ADLARI = { "34": "Istanbul", "06": "Ankara", "65": "Van" };

export function adresYaz(adres) {
  return `${IL_ADLARI[adres.slice(0, 2)] ?? "bilinmeyen"} ${adres}`;
}
```

```sh
node uyum-olc.mjs bolunmus/ucret.mjs bolunmus/takip.mjs bolunmus/adres.mjs
grep -c "^import" bolunmus/ucret.mjs bolunmus/takip.mjs bolunmus/adres.mjs || true
```

```
bolunmus/ucret.mjs  islev=3 veri=2 bilesen=1
  bilesen 1: taban, bolgeKatsayisi, ucret
  paylasan cift=2 paylasmayan cift=1 uyumsuzluk=0
bolunmus/takip.mjs  islev=2 veri=2 bilesen=1
  bilesen 1: takipNo, takipGecerli
  paylasan cift=1 paylasmayan cift=0 uyumsuzluk=0
bolunmus/adres.mjs  islev=1 veri=1 bilesen=1
  bilesen 1: adresYaz
  paylasan cift=0 paylasmayan cift=0 uyumsuzluk=0
bolunmus/ucret.mjs:0
bolunmus/takip.mjs:0
bolunmus/adres.mjs:0
```

Üç modülün üçünde de bileşen sayısı bir, uyumsuzluk sıfır. Son üç satır bölmenin bedelini
sayıyor: yeni modüller birbirini hiç ithal etmiyor. Uyumsuz bir modülü bölmek bağlaşım
üretmedi, çünkü bölünen parçalar zaten birbirine dokunmuyordu. Bölmenin bağlaşım maliyeti
ancak gerçekten ortak veriye dokunan işlevler ayrıldığında doğar.

Davranış da değişmedi.

```js
// ayni-davranis.mjs — iki yerlesim ayni sonuclari uretir
const G = { agirlik: 3.0, adres: "06500" };
const b = await import("./birlesik/gonderi-servisi.mjs");
const u = await import("./bolunmus/ucret.mjs");
const t = await import("./bolunmus/takip.mjs");
const a = await import("./bolunmus/adres.mjs");

console.log("birlesik ->", b.ucret(G), b.takipNo(), b.adresYaz(G.adres), b.takipGecerli("GN-000001"));
console.log("bolunmus ->", u.ucret(G), t.takipNo(), a.adresYaz(G.adres), t.takipGecerli("GN-000001"));
```

```sh
node ayni-davranis.mjs
```

```
birlesik -> 7360 GN-000001 Ankara 06500 true
bolunmus -> 7360 GN-000001 Ankara 06500 true
```

#### Uyumun Pratik Karşılığı

Bileşen sayısı soyut kalmasın diye ikinci bir ölçü alınır: bir verinin değişmesi hâlinde
okunması gereken dosyanın büyüklüğü. Takip numarası öneki ile bölge katsayıları farklı
kişilerin kararıdır; ikisinin de nerede durduğu ölçülebilir.

```js
// etki-alani.mjs — bir veri adi degistiginde okunmasi gereken dosyanin buyuklugu
import { readFileSync } from "node:fs";

const [ad, ...dosyalar] = process.argv.slice(2);

for (const yol of dosyalar) {
  const metin = readFileSync(yol, "utf8");
  if (!new RegExp(`^(?:const|let)\\s+${ad}\\b`, "m").test(metin)) continue;
  const islev = [...metin.matchAll(/^(?:export\s+)?function\s+\w+/gm)].length;
  const satir = metin.trim().split("\n").length;
  console.log(`${ad.padEnd(10)} ${yol.padEnd(30)} islev=${islev} satir=${satir}`);
}
```

```sh
for ad in ONEK BOLGE; do
  node etki-alani.mjs $ad birlesik/gonderi-servisi.mjs \
    bolunmus/ucret.mjs bolunmus/takip.mjs bolunmus/adres.mjs
done
```

```
ONEK       birlesik/gonderi-servisi.mjs   islev=6 satir=31
ONEK       bolunmus/takip.mjs             islev=2 satir=12
BOLGE      birlesik/gonderi-servisi.mjs   islev=6 satir=31
BOLGE      bolunmus/ucret.mjs             islev=3 satir=15
```

Takip önekini değiştirmek birleşik yerleşimde 6 işlevlik, 31 satırlık bir dosyayı açmayı
gerektiriyor; bölünmüş yerleşimde 2 işlevlik, 12 satırlık bir dosyayı. Uyumun karşılığı
budur: değişikliğin okuma yüzeyi, değişikliğin kendisiyle orantılı kalır.

#### Yanıltıcı Uyum Ölçütleri

Adın benzerliği uyum değildir. `gonderi-servisi` adı altındaki altı işlev de gönderiyle
ilgiliydi; ölçüm yine üç bileşen buldu. Aynı biçimde "hepsi yardımcı işlev" ya da "hepsi
doğrulama yapıyor" gibi gerekçeler de ölçülemez; bu gruplamalarda işlevler ortak veri
paylaşmaz, yalnız benzer iş yaparlar.

Ters yönde bir yanılgı da vardır: bileşen sayısını bire indirmek her zaman doğru değildir.
Bir modülün bütün işlevlerini tek bir paylaşılan nesneye dokundurmak bileşen sayısını bire
düşürür ama bu, bir önceki derste ölçülen ortak bağlaşımı üretir. İki ölçü birlikte
okunmalıdır: modül içi bileşen sayısı bir, modüller arası yazma sayısı sıfır.

#### Özet

- Uyum, bir modül içindeki işlevlerin dokundukları adlara göre kaç kümeye ayrıldığıdır;
  bağlaşımın modül içi karşılığıdır.
- Birleşik modülde 6 işlev 3 bileşene ayrıldı; 15 işlev çiftinden 12'si hiçbir ortak ada
  dokunmuyordu, uyumsuzluk 9 ölçüldü.
- Bileşenler bölmenin sınırını doğrudan verir: üç bileşen üç modüle çıkarıldığında her
  modülde bileşen sayısı 1, uyumsuzluk 0 oldu.
- Bölmenin bağlaşım maliyeti sıfır çıktı: yeni modüller birbirini hiç ithal etmiyor, çünkü
  ayrılan parçalar zaten ortak veriye dokunmuyordu.
- Bir verinin değişmesinde okunacak dosya 6 işlev ve 31 satırdan 2 işlev ve 12 satıra indi.

#### Sonraki Adım

Uyum ölçüsü bir modülün kendi adlarına dokunmasına baktı. Modüller birbirine nesne
geçirmeye başladığında yeni bir soru çıkar: bir işlev, aldığı nesnenin içinden aldığı ikinci
bir nesnenin üçüncü bir alanına eriştiğinde, o işlev kaç ayrı tipin yapısını bilmek zorunda
kalır? `gonderi.alici.adres.il` gibi bir zincir, aradaki her adımın biçimine bağımlılık
üretir ve bu bağımlılık ne ithal grafiğinde ne de uyum ölçüsünde görünür. Sonraki ders zincir
derinliğini sayan bir tarama yazar ve zincir kısaldığında bir alan değişikliğinin dokunduğu
dosya sayısının nasıl düştüğünü ölçer.

### Demeter Yasası

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/demeter-yasasi

Zincirleme erişimin sınırlanması: erişim zinciri derinliğini sayan bir taramanın yazılması, aynı beş çıktıyı üreten iki tasarımda bulgu sayısının karşılaştırılması ve adres kaydı yeniden yapılandırıldığında düzenlenen dosya sayısının ölçülmesi.

Uyum ölçüsü bir modülün kendi adlarına dokunmasına baktı. Modüller birbirine nesne
geçirmeye başladığında yeni bir soru çıkar: bir işlev, aldığı nesnenin içinden aldığı ikinci
bir nesnenin üçüncü bir alanına eriştiğinde kaç ayrı yapıyı bilmek zorunda kalır? Bu bağımlılık
ne ithal grafiğinde ne de uyum ölçüsünde görünür, çünkü ortada yeni bir ithal ya da yeni bir
modül düzeyi ad yoktur.

**Demeter yasası** (Law of Demeter) bu erişimi sınırlar. Bir işlev yalnız şunların
yöntemlerini çağırmalıdır: kendi nesnesinin, aldığı parametrelerin, kendi kurduğu nesnelerin
ve doğrudan parçalarının. "Komşunla konuş, komşunun komşusuyla değil" biçiminde özetlenir.
Ölçülebilir karşılığı erişim zincirinin derinliğidir: `gonderi.kod` bir adım, `gonderi.alici.adres.il`
üç adımdır ve aradaki iki yapının biçimine bağımlılık üretir.

#### Zincirli Tasarım

Kayıt üç düzey derinliğinde ve beş ayrı istemci onun içine giriyor.

```sh
mkdir -p zincirli kisa
```

```js
// zincirli/kayit.mjs — ic ice gecmis gonderi kaydi
export const KAYIT = {
  kod: "GN-000001",
  alici: { ad: "A. Yilmaz", adres: { il: "Ankara", ilce: "Cankaya", posta: "06500" } },
  tarife: { ad: "agirlik", kademe: { ust: 5, ucret: 6400 } },
};
```

```js
// zincirli/etiket.mjs — alici kaydinin icine iki adim giriyor
export const etiket = (gonderi) =>
  `${gonderi.alici.ad} / ${gonderi.alici.adres.il} ${gonderi.alici.adres.posta}`;
```

```js
// zincirli/ucret.mjs — tarifenin icindeki kademeye ve adresin icine uzaniyor
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export const ucret = (gonderi) =>
  Math.round((gonderi.tarife.kademe.ucret *
    (BOLGE[gonderi.alici.adres.posta.slice(0, 2)] ?? 160)) / 100);
```

```js
// zincirli/rapor.mjs — il ve ilceyi ayri ayri okuyor
export const rapor = (gonderi) =>
  `${gonderi.kod} ${gonderi.alici.adres.il}/${gonderi.alici.adres.ilce} ${gonderi.tarife.ad}`;
```

```js
// zincirli/bildirim.mjs — bildirim metni de ayni zinciri yuruyor
export const bildirim = (gonderi) =>
  `${gonderi.kod} ${gonderi.alici.adres.il} icin yola cikti`;
```

```js
// zincirli/sube.mjs — teslim subesi secimi
export const sube = (gonderi) =>
  `${gonderi.alici.adres.il}-${gonderi.alici.adres.ilce} subesi`;
```

```js
// zincirli/kur.mjs — bes istemcinin ciktisi
import { KAYIT } from "./kayit.mjs";
import { etiket } from "./etiket.mjs";
import { ucret } from "./ucret.mjs";
import { rapor } from "./rapor.mjs";
import { bildirim } from "./bildirim.mjs";
import { sube } from "./sube.mjs";

for (const f of [etiket, ucret, rapor, bildirim, sube]) console.log(f(KAYIT));
```

```sh
node zincirli/kur.mjs
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya agirlik
GN-000001 Ankara icin yola cikti
Ankara-Cankaya subesi
```

#### Zincir Taraması

Betik her dosyadaki erişim zincirlerini çıkarır, derinliklerini sayar ve iki adımdan uzun
olanları bulgu olarak bildirir. Dil ve çalışma zamanı nesneleri (`Math`, `JSON`, `console`
gibi) alan zinciri sayılmaz; bileşim kökü olan `kur.mjs` de taramanın dışındadır.

```js
// zincir-tara.mjs — erisim zinciri derinligini sayar; iki adimdan uzun her zincir bir bulgu
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const ZINCIR = /\b[a-zA-Z_]\w*(?:\.\w+)+/g;
// Dil ve calisma zamani nesneleri alan zinciri sayilmaz.
const DIS = ["Math", "Object", "String", "Number", "JSON", "console", "process", "Array"];

const kok = process.argv[2];
let toplamBulgu = 0;
let enDerin = 0;

for (const d of readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort()) {
  if (d === "kur.mjs") continue;
  const metin = readFileSync(join(kok, d), "utf8").replace(/^\/\/.*$/gm, "");
  const zincirler = [...metin.matchAll(ZINCIR)]
    .map((m) => m[0])
    .filter((z) => !DIS.includes(z.split(".")[0]));
  const derinlikler = zincirler.map((z) => z.split(".").length - 1);
  const bulgular = zincirler.filter((z) => z.split(".").length - 1 >= 2);
  const yerelEnDerin = derinlikler.length === 0 ? 0 : Math.max(...derinlikler);
  toplamBulgu += bulgular.length;
  enDerin = Math.max(enDerin, yerelEnDerin);
  console.log(`  ${d.padEnd(14)} zincir=${zincirler.length} en derin=${yerelEnDerin}` +
    ` bulgu=${bulgular.length}${bulgular.length > 0 ? "  " + bulgular.join(" ") : ""}`);
}
console.log(`${kok.padEnd(9)} toplam bulgu=${toplamBulgu} en derin zincir=${enDerin}`);
```

```sh
node zincir-tara.mjs zincirli
```

```
  bildirim.mjs   zincir=2 en derin=3 bulgu=1  gonderi.alici.adres.il
  etiket.mjs     zincir=3 en derin=3 bulgu=3  gonderi.alici.ad gonderi.alici.adres.il gonderi.alici.adres.posta
  kayit.mjs      zincir=0 en derin=0 bulgu=0
  rapor.mjs      zincir=4 en derin=3 bulgu=3  gonderi.alici.adres.il gonderi.alici.adres.ilce gonderi.tarife.ad
  sube.mjs       zincir=2 en derin=3 bulgu=2  gonderi.alici.adres.il gonderi.alici.adres.ilce
  ucret.mjs      zincir=2 en derin=4 bulgu=2  gonderi.tarife.kademe.ucret gonderi.alici.adres.posta.slice
zincirli  toplam bulgu=11 en derin zincir=4
```

On bir bulgu, en derin zincir dört adım. Beş istemcinin beşi de adres kaydının alan adlarını
biliyor.

#### Zincirin Kısaltılması

Zinciri kısaltmanın yolu ara nesnelere davranış vermektir. Adres kaydının biçimini yalnız bir
modül bilir; kalanlar ondan sonuç ister.

```js
// kisa/kayit.mjs — ayni ic ice gecmis kayit
export const KAYIT = {
  kod: "GN-000001",
  alici: { ad: "A. Yilmaz", adres: { il: "Ankara", ilce: "Cankaya", posta: "06500" } },
  tarife: { ad: "agirlik", kademe: { ust: 5, ucret: 6400 } },
};
```

```js
// kisa/adres.mjs — adres kaydinin bicimini yalniz burasi bilir
export const adres = (kayit) => ({
  yaz: () => `${kayit.il} ${kayit.posta}`,
  bolgeKodu: () => kayit.posta.slice(0, 2),
  ilIlce: () => `${kayit.il}/${kayit.ilce}`,
  il: () => kayit.il,
});
```

```js
// kisa/gonderi.mjs — gonderi, komsusuna sorup sonucu kendi adiyla verir
import { adres } from "./adres.mjs";

export const gonderi = (kayit) => {
  const a = adres(kayit.alici.adres);
  return {
    kod: kayit.kod,
    etiketSatiri: () => `${kayit.alici.ad} / ${a.yaz()}`,
    bolgeKodu: () => a.bolgeKodu(),
    ilIlce: () => a.ilIlce(),
    il: () => a.il(),
    tarifeAdi: () => kayit.tarife.ad,
    kademeUcreti: () => kayit.tarife.kademe.ucret,
  };
};
```

```js
// kisa/etiket.mjs — tek adim
export const etiket = (gonderi) => gonderi.etiketSatiri();
```

```js
// kisa/ucret.mjs — tek adim
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export const ucret = (gonderi) =>
  Math.round((gonderi.kademeUcreti() * (BOLGE[gonderi.bolgeKodu()] ?? 160)) / 100);
```

```js
// kisa/rapor.mjs — tek adim
export const rapor = (gonderi) => `${gonderi.kod} ${gonderi.ilIlce()} ${gonderi.tarifeAdi()}`;
```

```js
// kisa/bildirim.mjs — tek adim
export const bildirim = (gonderi) => `${gonderi.kod} ${gonderi.il()} icin yola cikti`;
```

```js
// kisa/sube.mjs — tek adim
export const sube = (gonderi) => `${gonderi.ilIlce().replace("/", "-")} subesi`;
```

```js
// kisa/kur.mjs — ayni bes cikti
import { KAYIT } from "./kayit.mjs";
import { gonderi } from "./gonderi.mjs";
import { etiket } from "./etiket.mjs";
import { ucret } from "./ucret.mjs";
import { rapor } from "./rapor.mjs";
import { bildirim } from "./bildirim.mjs";
import { sube } from "./sube.mjs";

const g = gonderi(KAYIT);
for (const f of [etiket, ucret, rapor, bildirim, sube]) console.log(f(g));
```

```sh
node kisa/kur.mjs
node zincir-tara.mjs kisa
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya agirlik
GN-000001 Ankara icin yola cikti
Ankara-Cankaya subesi
  adres.mjs      zincir=6 en derin=2 bulgu=1  kayit.posta.slice
  bildirim.mjs   zincir=2 en derin=1 bulgu=0
  etiket.mjs     zincir=1 en derin=1 bulgu=0
  gonderi.mjs    zincir=10 en derin=3 bulgu=4  kayit.alici.adres kayit.alici.ad kayit.tarife.ad kayit.tarife.kademe.ucret
  kayit.mjs      zincir=0 en derin=0 bulgu=0
  rapor.mjs      zincir=3 en derin=1 bulgu=0
  sube.mjs       zincir=1 en derin=1 bulgu=0
  ucret.mjs      zincir=2 en derin=1 bulgu=0
kisa      toplam bulgu=5 en derin zincir=3
```

Aynı beş çıktı, on bir bulgu yerine beş bulgu. Daha önemlisi bulguların dağılımı: beş istemci
modülün beşi de sıfır bulguya indi, kalan bulgular yapıyı çözen iki modülde toplandı. Demeter
yasası yapının bilinmesini ortadan kaldırmaz, **bilen modül sayısını** düşürür.

#### Değişikliğin Dokunduğu Dosyalar

Ölçünün karşılığı bir yapı değişikliğinde çıkar. Adres kaydı yeniden düzenleniyor: `il` ve
`ilce` alanları `konum` adında bir alt kayda taşınıyor. `posta` yerinde kalıyor. Yerinde
düzenlemede yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde aynı davranır.

```sh
cp -r zincirli zincirli-yeni
cp -r kisa kisa-yeni
YENI='{ konum: { il: "Ankara", ilce: "Cankaya" }, posta: "06500" }'
ESKI='{ il: "Ankara", ilce: "Cankaya", posta: "06500" }'
sed -i.y "s#$ESKI#$YENI#" zincirli-yeni/kayit.mjs kisa-yeni/kayit.mjs
sed -i.y 's/\.adres\.il/.adres.konum.il/g' zincirli-yeni/*.mjs
sed -i.y 's/kayit\.il/kayit.konum.il/g' kisa-yeni/adres.mjs
rm -f zincirli-yeni/*.y kisa-yeni/*.y
node zincirli-yeni/kur.mjs
node kisa-yeni/kur.mjs
for k in zincirli kisa; do
  echo "$k: duzenlenen dosya = $(diff -rq $k $k-yeni | grep -c '^Files')  ->" \
    "$(diff -rq $k $k-yeni | sed 's#.*/\([a-z]*\.mjs\) .*#\1#' | tr '\n' ' ')"
done
```

```
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya agirlik
GN-000001 Ankara icin yola cikti
Ankara-Cankaya subesi
A. Yilmaz / Ankara 06500
7360
GN-000001 Ankara/Cankaya agirlik
GN-000001 Ankara icin yola cikti
Ankara-Cankaya subesi
zincirli: duzenlenen dosya = 5  -> bildirim.mjs etiket.mjs kayit.mjs rapor.mjs sube.mjs
kisa: duzenlenen dosya = 2  -> adres.mjs kayit.mjs
```

İki sürüm de aynı beş satırı üretmeye devam ediyor. Fark maliyette: aynı yapı değişikliği
zincirli tasarımda beş dosyayı, kısa tasarımda iki dosyayı düzenletti. Zincirli sürümde ücret
modülü dokunulmayan tek modül oldu, çünkü yalnız `posta` alanını kullanıyordu — yani hangi
modülün etkileneceği, o modülün hangi alan adına eriştiğine bağlıydı.

#### Yasanın Yanlış Uygulanışı

Zinciri kısaltmanın yanlış yolu, her ara alan için bir aktarıcı yöntem yazmaktır.
`gonderi.aliciAdi()`, `gonderi.aliciIl()`, `gonderi.aliciIlce()`, `gonderi.aliciPosta()`
biçiminde büyüyen bir arayüz zinciri kısaltır ama aynı bilgiyi bu kez yöntem adlarına
kopyalar; alan eklendikçe arayüz büyür ve bir önceki derste ölçülen arayüz fazlalığı geri
gelir. Yukarıdaki kısa sürümde `adres` nesnesi ham alan döndürmüyor, sorulan soruya cevap
veriyor: `yaz`, `bolgeKodu`, `ilIlce`. Bunlar alan adlarının değil, istemcilerin
sorularının adlarıdır.

Yasanın istisnası da vardır: veri taşımak için var olan kayıtlarda — sıralı bir yapılandırma
nesnesinde ya da bir sınır noktasında ayrıştırılmış gövdede — zincir kaçınılmazdır ve
maliyeti düşüktür, çünkü o kayıt zaten dış bir sözleşmedir. Ölçü, zincirin varlığı değil,
kaç modülün aynı yapıyı ayrı ayrı bilmesidir.

#### Özet

- Demeter yasası bir işlevin erişebileceği nesneleri kendi parçaları, parametreleri ve kurduğu
  nesnelerle sınırlar; ölçüsü erişim zincirinin adım sayısıdır.
- Beş istemcinin kayda doğrudan girdiği tasarımda 11 bulgu ve en derin 4 adımlık zincir
  sayıldı.
- Ara nesnelere davranış verilince bulgu sayısı 5'e indi ve beş istemci modülün beşi de sıfır
  bulguya düştü; kalan bulgular yapıyı çözen iki modülde toplandı.
- Adres kaydı yeniden yapılandırıldığında zincirli tasarımda 5, kısa tasarımda 2 dosya
  düzenlendi; iki sürüm de aynı beş satırı üretmeye devam etti.
- Zinciri her alan için bir aktarıcı yöntemle kısaltmak ölçüyü düzeltmez; ara nesne alan
  adlarını değil, istemcilerin sorularını karşılamalıdır.

#### Sonraki Adım

Kısa sürümde `adres` nesnesi hâlâ değer döndürüyor: `ilIlce()` bir dize verir, kararı çağıran
verir. Şube seçimi bu dizeyi alıp biçimlendiriyor, bildirim başka bir parçasını kullanıyor.
Aynı kural birden çok çağrı yerinde tekrarlandığında soru değişir: veriyi dışarı verip kararı
istemciye bırakmak mı, yoksa kararı nesneye söylemek mi? Sonraki ders aynı kuralın kaç ayrı
yerde tekrarlandığını sayar ve kararı veriye taşıyan sürümle karşılaştırır.

### Sorma, Söyle

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/sorma-soyle

Davranışın veriye taşınması: aynı sevk kuralını dört istemcinin ayrı ayrı kurduğu tasarım ile kararı nesneye söyleyen tasarımın karşılaştırılması, kuralın alanlarını bilen dosya sayısının sayılması, istemcilerin ayrık sonuç kümesinin ölçülmesi ve kural değiştiğinde düzenlenen dosya sayısının bulunması.

Demeter dersinde zincirler kısaldı, ama ara nesneler hâlâ değer döndürüyordu: `ilIlce()` bir
dize verir, kararı çağıran verir. Bu, sorunun bir başka biçimini gizler. Bir nesneden alan
alıp o alanlara bakarak karar veren her istemci, kuralın bir kopyasını kendi içinde taşır.
Kopya sayısı arttıkça kuralın tek bir tanımı kalmaz.

**Sorma, söyle** (tell, don't ask) bu tekrarı hedefler: veriye sahip olan nesneye ne yapması
gerektiği söylenir, verisi alınıp karar dışarıda verilmez. Ölçüsü iki sayıdır — kuralın
dayandığı alanları kaç dosya biliyor ve aynı kural kaç dosyada tekrarlanıyor.

#### Kuralı Dört Kez Yazan Tasarım

Sevk kuralı üç koşuldan oluşuyor: gönderi depoda olmalı, ödemesi yapılmış olmalı ve ağırlığı
sınırı aşmamalı. Dört istemci bu kuralı ayrı ayrı kuruyor.

```sh
mkdir -p sorma soyle
```

```js
// sorma/kayit.mjs — sevk edilecek gonderiler
export const KAYITLAR = [
  { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 },
  { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 },
  { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 },
  { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 },
  { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 },
];
```

```js
// sorma/web.mjs — HTTP ucu kurali kendisi kuruyor
export function gonder(kayit) {
  if (kayit.durum === "depoda" && kayit.odendi && kayit.agirlik <= 30) {
    return { httpKodu: 200, kod: kayit.kod };
  }
  return { httpKodu: 409, kod: kayit.kod };
}
```

```js
// sorma/toplu.mjs — toplu isleme ayni kurali yeniden yaziyor
export const secilenler = (kayitlar) =>
  kayitlar
    .filter((k) => k.durum === "depoda" && k.odendi && k.agirlik <= 30)
    .map((k) => k.kod);
```

```js
// sorma/kurye.mjs — kurye uygulamasi agirlik kosulunu yazmamis
export const listele = (kayitlar) =>
  kayitlar.filter((k) => k.durum === "depoda" && k.odendi).map((k) => k.kod);
```

```js
// sorma/zamanli.mjs — zamanlanmis is kurali ucuncu kez yaziyor
export function tarama(kayitlar) {
  const cikacak = [];
  for (const k of kayitlar) {
    if (k.durum === "depoda" && k.odendi && k.agirlik <= 30) cikacak.push(k.kod);
  }
  return cikacak;
}
```

#### Kararı Nesneye Söyleyen Tasarım

İkinci tasarımda kural gönderi nesnesinin içindedir. İstemciler alan okumaz; nesneye yola
çıkmasını söyler ve sonucu alır.

```js
// soyle/kayit.mjs — sevk edilecek gonderiler
export const KAYITLAR = [
  { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 },
  { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 },
  { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 },
  { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 },
  { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 },
];
```

```js
// soyle/gonderi.mjs — kural nesnenin icinde, tek yerde
const AGIRLIK_SINIRI = 30;

export const gonderi = (kayit) => ({
  kod: kayit.kod,
  yolaCikar() {
    if (kayit.durum !== "depoda") return { ok: false, sebep: "depoda_degil" };
    if (kayit.odendi === false) return { ok: false, sebep: "odenmedi" };
    if (kayit.agirlik > AGIRLIK_SINIRI) return { ok: false, sebep: "agirlik_siniri" };
    return { ok: true, sebep: "yola_cikti" };
  },
});
```

```js
// soyle/web.mjs — karari nesneye soyler
export function gonder(g) {
  return { httpKodu: g.yolaCikar().ok ? 200 : 409, kod: g.kod };
}
```

```js
// soyle/toplu.mjs — ayni emri verir
export const secilenler = (gonderiler) =>
  gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod);
```

```js
// soyle/kurye.mjs — ayni emri verir; kosul yazma firsati yok
export const listele = (gonderiler) =>
  gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod);
```

```js
// soyle/zamanli.mjs — ayni emri verir
export function tarama(gonderiler) {
  const cikacak = [];
  for (const g of gonderiler) if (g.yolaCikar().ok) cikacak.push(g.kod);
  return cikacak;
}
```

#### Tekrarın Sayılması

İlk ölçü kuralın dayandığı alan adlarını kaç dosyanın bildiğidir. Bir dosya bu alanlardan
ikisini birden biliyorsa kuralın bir kopyasını taşıyor sayılır; veri dosyası bu sayımın
dışındadır.

```js
// kural-say.mjs — kuralin dayandigi alanlari kac dosyanin bildigini sayar
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const ALANLAR = ["durum", "odendi", "agirlik"];
const kok = process.argv[2];
let bilenDosya = 0;
let tekrar = 0;

for (const d of readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort()) {
  const metin = readFileSync(join(kok, d), "utf8").replace(/^\/\/.*$/gm, "");
  const bilinen = ALANLAR.filter((a) => new RegExp(`\\b${a}\\b`).test(metin));
  if (bilinen.length > 0) bilenDosya += 1;
  if (bilinen.length >= 2 && d !== "kayit.mjs") tekrar += 1;
  console.log(`  ${d.padEnd(12)} bilinen alan=${bilinen.length}  ${bilinen.join(" ")}`);
}
console.log(`${kok.padEnd(6)} alani bilen dosya=${bilenDosya}, kurali tekrarlayan dosya=${tekrar}`);
```

```sh
node kural-say.mjs sorma
node kural-say.mjs soyle
```

```
  kayit.mjs    bilinen alan=3  durum odendi agirlik
  kurye.mjs    bilinen alan=2  durum odendi
  toplu.mjs    bilinen alan=3  durum odendi agirlik
  web.mjs      bilinen alan=3  durum odendi agirlik
  zamanli.mjs  bilinen alan=3  durum odendi agirlik
sorma  alani bilen dosya=5, kurali tekrarlayan dosya=4
  gonderi.mjs  bilinen alan=3  durum odendi agirlik
  kayit.mjs    bilinen alan=3  durum odendi agirlik
  kurye.mjs    bilinen alan=0
  toplu.mjs    bilinen alan=0
  web.mjs      bilinen alan=0
  zamanli.mjs  bilinen alan=0
soyle  alani bilen dosya=2, kurali tekrarlayan dosya=1
```

Birinci tasarımda dört dosya kuralı tekrarlıyor; ikincisinde bir. Tarama aynı zamanda
kuralın çatladığı yeri de gösterdi: kurye modülü üç alandan yalnız ikisini biliyor.

#### Ayrık Sonuç Kümesi

Kopyalar arasındaki sapmanın koşumdaki karşılığı, aynı veriyle çalışan istemcilerin farklı
listeler üretmesidir. Sürücü betik dört istemciyi aynı beş gönderiyle çalıştırır ve kaç ayrı
sonuç çıktığını sayar.

```js
// kur.mjs — dort istemci ayni veriyle calisir, sonuclari karsilastirilir
const kok = process.argv[2];
const { KAYITLAR } = await import(`./${kok}/kayit.mjs`);
const { gonder } = await import(`./${kok}/web.mjs`);
const { secilenler } = await import(`./${kok}/toplu.mjs`);
const { listele } = await import(`./${kok}/kurye.mjs`);
const { tarama } = await import(`./${kok}/zamanli.mjs`);

const girdi = kok.startsWith("soyle")
  ? KAYITLAR.map((await import(`./${kok}/gonderi.mjs`)).gonderi)
  : KAYITLAR;

const sonuclar = {
  web: girdi.filter((g) => gonder(g).httpKodu === 200).map((g) => g.kod),
  toplu: secilenler(girdi),
  kurye: listele(girdi),
  zamanli: tarama(girdi),
};
for (const [ad, liste] of Object.entries(sonuclar)) console.log(`${ad.padEnd(9)} ${liste.join(" ")}`);
const ayrik = new Set(Object.values(sonuclar).map((l) => l.join(" ")));
console.log(`${kok}: farkli sonuc veren istemci kumesi = ${ayrik.size}`);
```

```sh
node kur.mjs sorma
node kur.mjs soyle
```

```
web       GN-1 GN-5
toplu     GN-1 GN-5
kurye     GN-1 GN-2 GN-5
zamanli   GN-1 GN-5
sorma: farkli sonuc veren istemci kumesi = 2
web       GN-1 GN-5
toplu     GN-1 GN-5
kurye     GN-1 GN-5
zamanli   GN-1 GN-5
soyle: farkli sonuc veren istemci kumesi = 1
```

Kırk beş kilogramlık GN-2 kurye listesinde görünüyor, diğer üç istemcide görünmüyor. İkinci
tasarımda böyle bir sapma için yer yok, çünkü koşulu yazacak dört ayrı gövde yok.

#### Kural Değiştiğinde

Ağırlık sınırı 30'dan 20'ye iniyor. İki ağaç kopyalanır ve değişiklik ikisine de uygulanır.
Yerinde düzenlemede yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde aynı davranır.

```sh
cp -r sorma sorma-yeni
cp -r soyle soyle-yeni
sed -i.y 's/agirlik <= 30/agirlik <= 20/' sorma-yeni/*.mjs
sed -i.y 's/AGIRLIK_SINIRI = 30/AGIRLIK_SINIRI = 20/' soyle-yeni/gonderi.mjs
rm -f sorma-yeni/*.y soyle-yeni/*.y
node kur.mjs sorma-yeni
node kur.mjs soyle-yeni
for k in sorma soyle; do echo "$k: duzenlenen dosya = $(diff -rq $k $k-yeni | grep -c '^Files')"; done
```

```
web       GN-1
toplu     GN-1
kurye     GN-1 GN-2 GN-5
zamanli   GN-1
sorma-yeni: farkli sonuc veren istemci kumesi = 2
web       GN-1
toplu     GN-1
kurye     GN-1
zamanli   GN-1
soyle-yeni: farkli sonuc veren istemci kumesi = 1
sorma: duzenlenen dosya = 3
soyle: duzenlenen dosya = 1
```

Üç dosyaya karşı bir dosya. Sapmanın büyüdüğüne de dikkat: kurye listesi eskiden yalnız GN-2
fazlasını içeriyordu, sınır düşünce GN-5 de eklendi. Kural kopyalarının bir kez ayrışması
kalıcıdır; ayrışma her kural değişikliğinde büyür.

#### İlkenin Sınırı

"Sorma, söyle" her alan okumasını yasaklamaz. Bir alanın dışarıya verilmesi, o alanla ilgili
bir **karar** dışarıda veriliyorsa sorun olur. Gönderi kodunu raporda yazdırmak için okumak
karar değildir; kodun biçimine bakıp gönderinin yurt içi mi olduğuna karar vermek karardır.

İlkenin aşırı uygulanışı da ölçülebilir bir maliyet üretir. Nesneye her soru için ayrı bir
emir yöntemi eklenirse arayüz büyür ve arayüz ayrımı dersinde ölçülen fazlalık geri gelir.
Ölçüt, yöntem sayısı değil, kuralı tekrarlayan dosya sayısıdır: bu sayı birden büyükse
davranış yanlış yerdedir.

#### Özet

- Sorma–söyle ilkesi, veriye sahip nesneden alan alıp dışarıda karar vermek yerine karara
  konu olan işi nesneye söylemeyi ister.
- Kuralın alanlarını bilen dosya sayısı 5'ten 2'ye, kuralı tekrarlayan dosya sayısı 4'ten
  1'e indi.
- Tekrar sapmayı üretti: dört istemci aynı veriyle 2 ayrı sonuç kümesi verdi, tek tanımlı
  sürümde 1 küme çıktı.
- Ağırlık sınırı değiştiğinde tekrarlı tasarımda 3, tek tanımlı tasarımda 1 dosya düzenlendi;
  sapma kural değişikliğiyle büyüdü.
- İlke alan okumasını değil, alan üzerinde dışarıda karar vermeyi sınırlar; ölçüt kuralı
  tekrarlayan dosya sayısıdır.

#### Sonraki Adım

`yolaCikar` bir emir gibi adlandırıldı ama aslında yalnız bir sonuç döndürüyor: gönderinin
durumu değişmiyor. Gerçek bir sevk işlemi durumu da değiştirmeli. Aynı yöntem hem durumu
değiştirip hem bir değer döndürdüğünde ne olur? Yöntemi iki kez çağırmak aynı sonucu verir
mi, bir günlük satırına yazmak için yapılan fazladan bir çağrı sistemin durumunu değiştirir
mi? Sonraki ders hem durum değiştiren hem değer döndüren bir işlemin ürettiği sürprizi bir
koşumla gösterir ve iki sorumluluğu ayırmanın ölçüsünü kurar.

### Komut–Sorgu Ayrımı

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/komut-sorgu-ayrimi

Durum değiştiren ve okuyan işlemlerin ayrılması: hem yazan hem döndüren yöntemleri bulan bir taramanın yazılması, yalnız rapor üreten bir yolun kaç kaydı değiştirdiğinin sayılması ve günlük satırının gerçekte olanın tersini yazdığının koşumla gösterilmesi.

Önceki dersin `yolaCikar` yöntemi bir emir gibi adlandırılmıştı ama yalnız bir sonuç
döndürüyordu; gönderinin durumu değişmiyordu. Gerçek bir sevk işlemi durumu da değiştirmeli.
Yöntem bu iki işi birlikte üstlendiğinde yeni bir soru doğar: aynı yöntemi ikinci kez
çağırmak neyi değiştirir?

**Komut–sorgu ayrımı** (command–query separation) Programlama Temelleri kursunda tanıtılmıştı:
bir işlem ya durumu değiştirir ve değer döndürmez (komut), ya da değer döndürür ve durumu
değiştirmez (sorgu). Bu ders ilkeyi nesne tasarımına taşır ve ihlalinin bedelini iki sayıyla
ölçer: hem yazan hem döndüren yöntem sayısı ve yalnız okumak isteyen bir yolun değiştirdiği
kayıt sayısı.

#### Karışık Yöntem

Birinci tasarımda `yolaCikar` hem `durum` alanını yazar hem sonucu döndürür.

```sh
mkdir -p karisik ayrik
```

```js
// karisik/kayit.mjs — her deney taze kayitlarla baslasin diye uretici
export const yeniKayitlar = () => [
  { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 },
  { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 },
  { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 },
  { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 },
  { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 },
];
```

```js
// karisik/gonderi.mjs — tek yontem hem durumu degistiriyor hem deger donduruyor
const AGIRLIK_SINIRI = 30;

export const gonderi = (kayit) => ({
  kod: kayit.kod,
  yolaCikar() {
    if (kayit.durum !== "depoda") return { ok: false, sebep: "depoda_degil" };
    if (kayit.odendi === false) return { ok: false, sebep: "odenmedi" };
    if (kayit.agirlik > AGIRLIK_SINIRI) return { ok: false, sebep: "agirlik_siniri" };
    kayit.durum = "yolda";
    return { ok: true, sebep: "yola_cikti" };
  },
});
```

```js
// karisik/sevk.mjs — once sevk eder, sonra gunluge yazmak icin sebebi yeniden sorar
export function sevkEt(gonderiler) {
  const cikan = gonderiler.filter((g) => g.yolaCikar().ok).map((g) => g.kod);
  const gunluk = gonderiler.map((g) => `${g.kod}=${g.yolaCikar().sebep}`);
  return { cikan, gunluk };
}
```

```js
// karisik/rapor.mjs — yalniz sebepleri yazmak isteyen rapor
export const rapor = (gonderiler) => gonderiler.map((g) => `${g.kod}=${g.yolaCikar().sebep}`);
```

Rapor modülü hiçbir şeyi sevk etmek istemiyor; yalnız her gönderinin durumunu yazacak. Elinde
soracak tek yöntem olduğu için onu çağırıyor.

#### Ayrık Yöntemler

İkinci tasarımda aynı bilgi iki yönteme bölünür. `sevkEngeli` bir sorgudur: durumu okur,
değiştirmez. `yolaCikar` bir komuttur: durumu değiştirir, değer döndürmez; sevk edilemeyen
bir gönderi için hata verir.

```js
// ayrik/kayit.mjs — her deney taze kayitlarla baslasin diye uretici
export const yeniKayitlar = () => [
  { kod: "GN-1", durum: "depoda", odendi: true, agirlik: 12 },
  { kod: "GN-2", durum: "depoda", odendi: true, agirlik: 45 },
  { kod: "GN-3", durum: "yolda", odendi: true, agirlik: 8 },
  { kod: "GN-4", durum: "depoda", odendi: false, agirlik: 5 },
  { kod: "GN-5", durum: "depoda", odendi: true, agirlik: 25 },
];
```

```js
// ayrik/gonderi.mjs — sorgu durum degistirmez, komut deger dondurmez
const AGIRLIK_SINIRI = 30;

export const gonderi = (kayit) => ({
  kod: kayit.kod,
  sevkEngeli() {
    if (kayit.durum !== "depoda") return "depoda_degil";
    if (kayit.odendi === false) return "odenmedi";
    if (kayit.agirlik > AGIRLIK_SINIRI) return "agirlik_siniri";
    return null;
  },
  yolaCikar() {
    const engel = this.sevkEngeli();
    if (engel !== null) throw new RangeError(`${kayit.kod} sevk edilemez: ${engel}`);
    kayit.durum = "yolda";
  },
});
```

```js
// ayrik/sevk.mjs — once sorar, gunluge yazar, sonra komutu verir
export function sevkEt(gonderiler) {
  const cikan = [];
  const gunluk = [];
  for (const g of gonderiler) {
    const engel = g.sevkEngeli();
    gunluk.push(`${g.kod}=${engel ?? "yola_cikti"}`);
    if (engel === null) {
      g.yolaCikar();
      cikan.push(g.kod);
    }
  }
  return { cikan, gunluk };
}
```

```js
// ayrik/rapor.mjs — yalniz sebepleri yazmak isteyen rapor
export const rapor = (gonderiler) =>
  gonderiler.map((g) => `${g.kod}=${g.sevkEngeli() ?? "yola_cikti"}`);
```

#### Sürprizin Koşumla Gösterilmesi

Sürücü betik iki deney yapar. Birincisinde yalnız rapor çağrılır ve raporun kaç kaydın
durumunu değiştirdiği sayılır. İkincisinde sevk işlemi çalıştırılır, günlük satırları yazılır
ve aynı işlem ikinci kez çağrılır.

```js
// kur.mjs — uc deney: rapor, sevk gunlugu ve ayni islemin ikinci kez cagrilmasi
const kok = process.argv[2];
const { yeniKayitlar } = await import(`./${kok}/kayit.mjs`);
const { gonderi } = await import(`./${kok}/gonderi.mjs`);
const { rapor } = await import(`./${kok}/rapor.mjs`);
const { sevkEt } = await import(`./${kok}/sevk.mjs`);

const kayitlar = yeniKayitlar();
const once = kayitlar.map((k) => k.durum);
console.log("rapor      ", rapor(kayitlar.map(gonderi)).join(" "));
const sonra = kayitlar.map((k) => k.durum);
console.log(`raporun degistirdigi kayit = ${once.filter((d, i) => d !== sonra[i]).length}`);

const ikinciKayitlar = yeniKayitlar();
const gonderiler = ikinciKayitlar.map(gonderi);
const birinci = sevkEt(gonderiler);
console.log("sevk 1 cikan", birinci.cikan.join(" ") || "(bos)");
console.log("sevk 1 gunluk", birinci.gunluk.join(" "));
const ikinci = sevkEt(gonderiler);
console.log("sevk 2 cikan", ikinci.cikan.join(" ") || "(bos)");
```

```sh
node kur.mjs karisik
```

```
rapor       GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti
raporun degistirdigi kayit = 2
sevk 1 cikan GN-1 GN-5
sevk 1 gunluk GN-1=depoda_degil GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=depoda_degil
sevk 2 cikan (bos)
```

İki sürpriz birden. Rapor iki kaydın durumunu "yolda" yaptı: hiçbir şey sevk etmek istemeyen
bir kod yolu iki gönderiyi yola çıkardı. Günlük satırı ise gerçekte olanın tersini yazdı —
sevk edilen GN-1 ve GN-5 için `depoda_degil` görünüyor, çünkü sebep sorulduğunda durum artık
değişmişti.

```sh
node kur.mjs ayrik
```

```
rapor       GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti
raporun degistirdigi kayit = 0
sevk 1 cikan GN-1 GN-5
sevk 1 gunluk GN-1=yola_cikti GN-2=agirlik_siniri GN-3=depoda_degil GN-4=odenmedi GN-5=yola_cikti
sevk 2 cikan (bos)
```

Rapor satırı aynı, ama değiştirdiği kayıt sayısı sıfır. Günlük satırı da gerçeği yazıyor.
Sevk sonucu iki sürümde de aynı; ayrım davranışı değiştirmedi, yalnız gözlemi güvenli hâle
getirdi.

#### Taramayla Bulunması

İhlal koşum beklemeden de bulunabilir. Aşağıdaki betik nesne yöntemlerinin gövdesine bakar:
gövdede kayda yazma varsa yöntem komuttur, değer döndüren `return` varsa sorgudur, ikisi de
varsa bulgudur.

```js
// komut-sorgu-tara.mjs — hem durum degistiren hem deger donduren yontemleri bulur
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";

const YONTEM = /^ {2}(\w+)\(/gm;

function govde(metin, acilis) {
  let i = acilis;
  let derinlik = 0;
  do {
    if (metin[i] === "{") derinlik += 1;
    else if (metin[i] === "}") derinlik -= 1;
    i += 1;
  } while (i < metin.length && derinlik > 0);
  return metin.slice(acilis, i);
}

const kok = process.argv[2];
let bulgu = 0;

for (const d of readdirSync(kok).filter((a) => a.endsWith(".mjs")).sort()) {
  const metin = readFileSync(join(kok, d), "utf8");
  for (const m of metin.matchAll(YONTEM)) {
    const g = govde(metin, metin.indexOf("{", m.index + m[0].length));
    const yazar = /\bkayit\.\w+\s*=[^=]/.test(g);
    const doner = /\breturn\s+[^;\s]/.test(g);
    const tur = yazar && doner ? "komut+sorgu" : yazar ? "komut" : doner ? "sorgu" : "bos";
    if (yazar && doner) bulgu += 1;
    console.log(`  ${d.padEnd(12)} ${m[1].padEnd(12)} yazar=${yazar} doner=${doner}  ${tur}`);
  }
}
console.log(`${kok.padEnd(8)} hem yazan hem donduren yontem = ${bulgu}`);
```

```sh
node komut-sorgu-tara.mjs karisik
node komut-sorgu-tara.mjs ayrik
```

```
  gonderi.mjs  yolaCikar    yazar=true doner=true  komut+sorgu
karisik  hem yazan hem donduren yontem = 1
  gonderi.mjs  sevkEngeli   yazar=false doner=true  sorgu
  gonderi.mjs  yolaCikar    yazar=true doner=false  komut
ayrik    hem yazan hem donduren yontem = 0
```

#### Ayrımın Kabul Edilen İstisnaları

Ayrım mutlak uygulanamaz. Bir yığıttan öge çıkaran işlem hem durumu değiştirir hem çıkardığı
ögeyi döndürür; ayrıştırıldığında iki çağrı arasında yarış oluşabilir. Aynı biçimde bir
kayıt ekleyip üretilen tanıtıcıyı döndüren işlem de karışıktır ve bölünmesi tanıtıcıyı ikinci
bir sorguyla aramayı gerektirir.

İstisnayı kabul edilebilir yapan iki koşul vardır. Birincisi, döndürülen değerin işlemin
**sonucu** olmasıdır — çıkarılan öge, üretilen tanıtıcı — yeniden hesaplanan bir durum
değil. İkincisi, çağrının okuma amacıyla tekrarlanmamasıdır. Yukarıdaki karışık sürümde iki
koşul da ihlal edilmişti: dönen değer sevkin sonucu değil koşulların yeniden değerlendirmesiydi
ve günlük satırı için yeniden çağrıldı.

Adlandırma bu yüzden ölçünün bir parçasıdır. `sevkEngeli` bir soru sorar, `yolaCikar` bir emir
verir; ikisinin karışması genellikle adın da karışmasıyla başlar.

#### Özet

- Komut–sorgu ayrımı bir işlemin ya durumu değiştirmesini ve değer döndürmemesini, ya da değer
  döndürüp durumu değiştirmemesini ister.
- Tarama, gövdesinde hem kayda yazma hem değer döndürme bulunan yöntemleri sayar: karışık
  tasarımda 1, ayrık tasarımda 0 bulgu çıktı.
- Yalnız rapor üreten kod yolu karışık tasarımda 2 kaydın durumunu değiştirdi, ayrık tasarımda
  0 kayıt değiştirdi.
- Günlük satırı karışık tasarımda gerçeğin tersini yazdı: sevk edilen iki gönderi için
  `depoda_degil` göründü, çünkü sebep sorulduğunda durum çoktan değişmişti.
- İstisna, dönen değerin işlemin sonucu olduğu ve çağrının okuma amacıyla tekrarlanmadığı
  durumlarda kabul edilir; adlandırma ayrımın görünür parçasıdır.

#### Sonraki Adım

Ayrık tasarımda `AGIRLIK_SINIRI` sabiti gönderi modülünün içinde duruyor ve sevk kuralı da
orada. Bu iki karar aynı dosyada, ama aynı nedenle değişmiyorlar: sınır fiyat listesi
yenilendiğinde, kural kurum politikası değiştiğinde oynar. Bir modülde birden çok değişim
ekseni bulunduğunda, iki ayrı eksenden gelen iki değişiklik aynı dosyalara dokunur ve
birbirinin sınamasını kırar. Sonraki ders iki eksende gelen değişikliğin dokunduğu dosya
kümelerini çıkarır, kesişimlerini ölçer ve eksenleri yalıtmanın bu kesişimi nasıl boşalttığını
gösterir.

### Değişeni Kapsülle

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/degiseni-kapsulle

Değişim eksenlerinin yalıtılması: fiyat listesi ve çıktı biçimi eksenlerinden gelen iki değişikliğin dokunduğu dosya kümelerinin çıkarılması, kesişimlerinin sayılması ve her ekseni kendi modülüne çeken yerleşimde kesişimin sıfıra inmesi.

Bir önceki dersin ayrık tasarımında ağırlık sınırı sabiti ile sevk kuralı aynı dosyadaydı.
İkisi aynı nedenle değişmiyor: sınır fiyat listesi yenilendiğinde, kural kurum politikası
değiştiğinde oynar. Bu farkın adı **değişim eksenidir**: bir modüle değişiklik isteği
gönderen ayrı kaynakların her biri bir eksendir.

**Değişeni kapsülle** (encapsulate what varies) ilkesi, her ekseni kendi modülünde toplamayı
söyler. Tek sorumluluk ilkesi aynı fikri değişim nedeni sayısıyla ölçmüştü; buradaki ölçü
farklı ve daha doğrudan: iki ayrı eksenden gelen iki değişikliğin dokunduğu dosya
kümelerinin **kesişimi**. Kesişim boş değilse, iki bağımsız değişiklik aynı metni paylaşıyor
demektir.

#### İki Eksen, Dağıtık Yerleşim

Ölçüm, ücretlendirme kitaplığının en sık değişen iki ekseniyle yapılır: **fiyat listesi** ve
**çıktı biçimi**. Birincisini fiyatlandırma birimi, ikincisini muhasebe raporlaması ister;
ikisi aynı anda gelmez. Kitaplığın üç istemcisi var: HTTP ucu, toplu işleme ve rapor. Birinci
yerleşimde üçü de ücreti kendi içinde hesaplıyor ve satırı kendi içinde biçimliyor.

```sh
mkdir -p dagitik yalitilmis
```

```js
// kayit.mjs — iki yerlesimin de kullandigi ornek gonderiler
export const KAYITLAR = [
  { kod: "GN-1", agirlik: 0.8, adres: "34100" },
  { kod: "GN-2", agirlik: 3.0, adres: "06500" },
  { kod: "GN-3", agirlik: 12.0, adres: "65200" },
];
```

```js
// dagitik/web.mjs — fiyat tablosu da cikti bicimi de burada
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function satir(gonderi) {
  const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
  const kurus = Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 160)) / 100);
  return `${gonderi.kod} ${kurus} kurus`;
}
```

```js
// dagitik/toplu.mjs — ayni fiyat tablosu, ayni bicim, ikinci kopya
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function satirlar(gonderiler) {
  return gonderiler.map((g) => {
    const taban = KADEME.find(([ust]) => g.agirlik <= ust)?.[1] ?? 15000;
    const kurus = Math.round((taban * (BOLGE[g.adres.slice(0, 2)] ?? 160)) / 100);
    return `${g.kod} ${kurus} kurus`;
  });
}
```

```js
// dagitik/rapor.mjs — ucuncu kopya
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function toplamSatiri(gonderiler) {
  let toplam = 0;
  for (const g of gonderiler) {
    const taban = KADEME.find(([ust]) => g.agirlik <= ust)?.[1] ?? 15000;
    toplam += Math.round((taban * (BOLGE[g.adres.slice(0, 2)] ?? 160)) / 100);
  }
  const kurus = toplam;
  return `TOPLAM ${kurus} kurus`;
}
```

#### Yalıtılmış Yerleşim

İkinci yerleşimde her eksen kendi modülünde durur: fiyat listesi bir modülde, çıktı biçimi
bir modülde. İstemciler ikisini de dışarıdan alır.

```js
// yalitilmis/fiyat.mjs — birinci eksen: fiyat listesi yalniz burada
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function kurusHesabi(gonderi) {
  const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
  return Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 160)) / 100);
}
```

```js
// yalitilmis/bicim.mjs — ikinci eksen: cikti bicimi yalniz burada
export const tutar = (kurus) => `${kurus} kurus`;
```

```js
// yalitilmis/web.mjs — iki ekseni de disaridan alir
import { kurusHesabi } from "./fiyat.mjs";
import { tutar } from "./bicim.mjs";

export const satir = (gonderi) => `${gonderi.kod} ${tutar(kurusHesabi(gonderi))}`;
```

```js
// yalitilmis/toplu.mjs — iki ekseni de disaridan alir
import { kurusHesabi } from "./fiyat.mjs";
import { tutar } from "./bicim.mjs";

export const satirlar = (gonderiler) =>
  gonderiler.map((g) => `${g.kod} ${tutar(kurusHesabi(g))}`);
```

```js
// yalitilmis/rapor.mjs — iki ekseni de disaridan alir
import { kurusHesabi } from "./fiyat.mjs";
import { tutar } from "./bicim.mjs";

export const toplamSatiri = (gonderiler) =>
  `TOPLAM ${tutar(gonderiler.reduce((t, g) => t + kurusHesabi(g), 0))}`;
```

```js
// kur.mjs — iki yerlesimin ayni ciktiyi urettigini gosterir
import { KAYITLAR } from "./kayit.mjs";

const kok = process.argv[2];
const { satir } = await import(`./${kok}/web.mjs`);
const { satirlar } = await import(`./${kok}/toplu.mjs`);
const { toplamSatiri } = await import(`./${kok}/rapor.mjs`);

console.log(`${kok}: ${satir(KAYITLAR[0])} | ${satirlar(KAYITLAR).join(" ")} | ${toplamSatiri(KAYITLAR)}`);
```

```sh
node kur.mjs dagitik
node kur.mjs yalitilmis
```

```
dagitik: GN-1 3900 kurus | GN-1 3900 kurus GN-2 7360 kurus GN-3 16520 kurus | TOPLAM 27780 kurus
yalitilmis: GN-1 3900 kurus | GN-1 3900 kurus GN-2 7360 kurus GN-3 16520 kurus | TOPLAM 27780 kurus
```

#### İki Değişiklik, İki Küme

Eksen A fiyat listesidir: orta kademe ücreti 6400'den 7100'e, Ankara bölge katsayısı 115'ten
120'ye çıkıyor. Eksen B çıktı biçimidir: tutarlar kuruş yerine iki ondalıklı lira olarak
yazılacak. İki değişiklik ayrı kopyalara ayrı ayrı uygulanır ve her birinin dokunduğu dosya
kümesi çıkarılır. Yerinde düzenlemede yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde
aynı davranır.

```sh
for k in dagitik yalitilmis; do
  cp -r $k $k-a
  cp -r $k $k-b
  sed -i.y 's/\[5, 6400\]/[5, 7100]/; s/"06": 115/"06": 120/' $k-a/*.mjs
  sed -i.y 's/\${kurus} kurus/${(kurus \/ 100).toFixed(2)} TL/' $k-b/*.mjs
  rm -f $k-a/*.y $k-b/*.y
  node kur.mjs $k-a
  node kur.mjs $k-b
done

degisen() { diff -rq "$1" "$2" | sed 's#^Files [^/]*/\([^ ]*\) and .*#\1#' | sort; }
for k in dagitik yalitilmis; do
  degisen $k $k-a > eksen-a.txt
  degisen $k $k-b > eksen-b.txt
  echo "$k eksen A -> $(tr '\n' ' ' < eksen-a.txt)"
  echo "$k eksen B -> $(tr '\n' ' ' < eksen-b.txt)"
  echo "$k kesisim = $(comm -12 eksen-a.txt eksen-b.txt | wc -l | tr -d ' ') $(comm -12 eksen-a.txt eksen-b.txt | tr '\n' ' ')"
  echo "$k bolge katsayisini bilen dosya = $(grep -l BOLGE $k/*.mjs | wc -l | tr -d ' ')"
done
```

```
dagitik-a: GN-1 3900 kurus | GN-1 3900 kurus GN-2 8520 kurus GN-3 16520 kurus | TOPLAM 28940 kurus
dagitik-b: GN-1 39.00 TL | GN-1 39.00 TL GN-2 73.60 TL GN-3 165.20 TL | TOPLAM 277.80 TL
yalitilmis-a: GN-1 3900 kurus | GN-1 3900 kurus GN-2 8520 kurus GN-3 16520 kurus | TOPLAM 28940 kurus
yalitilmis-b: GN-1 39.00 TL | GN-1 39.00 TL GN-2 73.60 TL GN-3 165.20 TL | TOPLAM 277.80 TL
dagitik eksen A -> rapor.mjs toplu.mjs web.mjs
dagitik eksen B -> rapor.mjs toplu.mjs web.mjs
dagitik kesisim = 3 rapor.mjs toplu.mjs web.mjs
dagitik bolge katsayisini bilen dosya = 3
yalitilmis eksen A -> fiyat.mjs
yalitilmis eksen B -> bicim.mjs
yalitilmis kesisim = 0
yalitilmis bolge katsayisini bilen dosya = 1
```

İki yerleşim de her iki değişiklikten sonra aynı sonucu üretiyor; fark maliyette. Dağıtık
yerleşimde her iki eksen de aynı üç dosyaya dokundu ve kesişim üç. Yalıtılmış yerleşimde
kümeler ayrık ve kesişim sıfır.

Son satır kesişimin nedenini gösteriyor: dağıtık yerleşimde bölge katsayısı üç dosyada
tanımlıydı, yalıtılmışta bir dosyada. Kesişimin kaynağı bilgiyi kopyalamaktır.

#### Kesişimin Anlamı

Kesişim sayısı üç ayrı riskin ortak ölçüsüdür. Birincisi, iki bağımsız değişikliğin aynı anda
gelmesi durumunda aynı dosyaların düzenlenmesi gerekir; ikisi de aynı satırlara dokunuyorsa
birleştirme el işine döner. İkincisi, fiyat değişikliğini yapan kişi biçim kodunu da okumak
zorundadır — okuma yüzeyi, değişikliğin kendisiyle orantısız büyür. Üçüncüsü ve en pahalısı,
bir eksende yapılan değişikliğin diğer eksenin sınamalarını kırma olasılığıdır; kesişim sıfır
olduğunda bu olasılık yapısal olarak ortadan kalkar.

Ölçü tersinden de okunur. Kesişim sıfırsa ve bir eksende gelen değişiklik yine de iki dosyayı
düzenlettiyse, o eksen henüz tam yalıtılmamıştır. Yalıtımın hedefi dosya sayısını azaltmak
değil, kümeleri ayrık tutmaktır.

#### Eksenin Doğru Seçilmesi

Yanlış eksen seçmek maliyeti azaltmaz, yerini değiştirir. Yukarıdaki bölme "hesap" ve
"biçim" olarak yapıldı; bunun yerine "web tarafı" ve "toplu taraf" biçiminde bir bölme
yapılsaydı iki eksenden gelen değişiklikler yine aynı dosyalara düşerdi, çünkü fiyat listesi
de biçim de her iki tarafta kullanılıyor.

Ekseni bulmanın yolu, tek bir kod tabanına bakmak değil, gelen değişiklik isteklerine
bakmaktır. İki istek hiç aynı anda gelmiyorsa ve farklı kişilerden geliyorsa iki ayrı
eksendir. Aynı kişiden, aynı gerekçeyle ve birlikte geliyorsa tek eksendir ve bölünmeleri
yalnız dosya sayısını artırır. Fiyat listesiyle çıktı biçimi bu ölçüte göre ayrıdır: birini
fiyatlandırma, diğerini muhasebe raporlaması ister.

#### Özet

- Değişim ekseni, bir modüle değişiklik isteği gönderen ayrı kaynaklardır; ilke her ekseni
  kendi modülünde toplamayı söyler.
- Ölçü, iki ayrı eksenden gelen iki değişikliğin dokunduğu dosya kümelerinin kesişimidir.
- Dağıtık yerleşimde iki eksen de aynı üç dosyaya dokundu ve kesişim 3 çıktı; yalıtılmış
  yerleşimde kümeler ayrıktı ve kesişim 0 oldu.
- Kesişimin kaynağı bilgiyi kopyalamaktır: bölge katsayısı dağıtık yerleşimde 3 dosyada,
  yalıtılmışta 1 dosyada tanımlıydı.
- Eksen, kod yapısına değil gelen değişiklik isteklerinin kaynağına göre seçilir; yanlış eksen
  kesişimi azaltmaz.

#### Sonraki Adım

Yalıtılmış yerleşimde üç istemci de `fiyat.mjs` modülünü adıyla ithal ediyor ve `kurusHesabi`
işlevini doğrudan çağırıyor. Fiyat hesabının ikinci bir gerçekleştirimi gerektiğinde — sözleşmeli
müşteriye özel bir liste, ya da sınama için sabit bir liste — istemcilerin ithal satırı
değişmek zorunda. Somut bir modüle bağlanmanın bedeli, gerçekleştirim değiştiğinde dokunulan
satır sayısıyla ölçülebilir. Sonraki ders bu bedeli sayar ve istemcinin somut tip yerine bir
soyutlamaya bağlandığı sürümle karşılaştırır.

### Soyutlamalara Karşı Programlama

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/soyutlamalara-karsi-programlama

Somut tipe bağlanmanın maliyetinin sayılması: ikinci bir fiyat listesi eklendiğinde düzenlenen dosya ve dokunulan satır sayısının iki yerleşimde karşılaştırılması, seçim yapan yer sayısının bulunması ve sahte bir fiyat listesinin yalnız soyutlamaya bağlı istemcide etkili olduğunun gösterilmesi.

Bir önceki dersin yalıtılmış yerleşiminde üç istemci de fiyat modülünü adıyla ithal ediyor ve
işlevini doğrudan çağırıyordu. Fiyat listesi tek olduğu sürece bu görünmez. Sözleşmeli
müşteriler için ikinci bir liste geldiğinde soru keskinleşir: istemci hangi listenin
kullanılacağını nereden bilecek?

**Soyutlamalara karşı programlama**, istemcinin somut bir modüle değil, birden çok
gerçekleştirimin karşılayabileceği bir sözleşmeye bağlanmasıdır. Bağımlılığın ters çevrilmesi
dersi sözleşmenin hangi pakette **tanımlandığını** ölçmüştü; buradaki ölçü istemci tarafında:
gerçekleştirim değiştiğinde ya da ikincisi eklendiğinde kaç satıra dokunuluyor?

#### Somut Modüle Bağlanan Yerleşim

```sh
mkdir -p somut soyut
```

```js
// kayit.mjs — iki yerlesimin de kullandigi ornek gonderiler
export const KAYITLAR = [
  { kod: "GN-1", agirlik: 0.8, adres: "34100", sozlesmeli: false },
  { kod: "GN-2", agirlik: 3.0, adres: "06500", sozlesmeli: true },
  { kod: "GN-3", agirlik: 12.0, adres: "65200", sozlesmeli: true },
];
```

```js
// somut/fiyat.mjs — standart fiyat listesi
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export function kurusHesabi(gonderi) {
  const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
  return Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 160)) / 100);
}
```

```js
// somut/web.mjs — somut fiyat modulunu adiyla ithal eder
import { kurusHesabi } from "./fiyat.mjs";

export const satir = (gonderi) => `${gonderi.kod} ${kurusHesabi(gonderi)} kurus`;
```

```js
// somut/toplu.mjs — somut fiyat modulunu adiyla ithal eder
import { kurusHesabi } from "./fiyat.mjs";

export const satirlar = (gonderiler) =>
  gonderiler.map((g) => `${g.kod} ${kurusHesabi(g)} kurus`);
```

```js
// somut/rapor.mjs — somut fiyat modulunu adiyla ithal eder
import { kurusHesabi } from "./fiyat.mjs";

export const toplamSatiri = (gonderiler) =>
  `TOPLAM ${gonderiler.reduce((t, g) => t + kurusHesabi(g), 0)} kurus`;
```

```js
// somut/kur.mjs — bilesim koku
import { KAYITLAR } from "../kayit.mjs";
import { satir } from "./web.mjs";
import { satirlar } from "./toplu.mjs";
import { toplamSatiri } from "./rapor.mjs";

console.log(satir(KAYITLAR[1]));
console.log(satirlar(KAYITLAR).join(" | "));
console.log(toplamSatiri(KAYITLAR));
```

#### Soyutlamaya Bağlanan Yerleşim

İkinci yerleşimde fiyat listesi bir nesnedir ve istemcilere dışarıdan verilir. İstemciler
`kurusHesabi` adında bir yöntemin var olduğunu bilir; hangi listenin geldiğini bilmez.

```js
// soyut/fiyat-standart.mjs — sozlesmeyi karsilayan standart liste
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export const standart = {
  ad: "standart",
  kurusHesabi(gonderi) {
    const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 15000;
    return Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 160)) / 100);
  },
};
```

```js
// soyut/web.mjs — fiyati disaridan alir, gerceklestirimin adini bilmez
export const satir = (fiyat, gonderi) => `${gonderi.kod} ${fiyat.kurusHesabi(gonderi)} kurus`;
```

```js
// soyut/toplu.mjs — fiyati disaridan alir
export const satirlar = (fiyatSec, gonderiler) =>
  gonderiler.map((g) => `${g.kod} ${fiyatSec(g).kurusHesabi(g)} kurus`);
```

```js
// soyut/rapor.mjs — fiyati disaridan alir
export const toplamSatiri = (fiyatSec, gonderiler) =>
  `TOPLAM ${gonderiler.reduce((t, g) => t + fiyatSec(g).kurusHesabi(g), 0)} kurus`;
```

```js
// soyut/kur.mjs — bilesim koku: hangi fiyat listesinin kullanilacagini yalniz burasi bilir
import { KAYITLAR } from "../kayit.mjs";
import { standart } from "./fiyat-standart.mjs";
import { satir } from "./web.mjs";
import { satirlar } from "./toplu.mjs";
import { toplamSatiri } from "./rapor.mjs";

const fiyatSec = () => standart;

console.log(satir(fiyatSec(KAYITLAR[1]), KAYITLAR[1]));
console.log(satirlar(fiyatSec, KAYITLAR).join(" | "));
console.log(toplamSatiri(fiyatSec, KAYITLAR));
```

```sh
node somut/kur.mjs
node soyut/kur.mjs
```

```
GN-2 7360 kurus
GN-1 3900 kurus | GN-2 7360 kurus | GN-3 16520 kurus
TOPLAM 27780 kurus
GN-2 7360 kurus
GN-1 3900 kurus | GN-2 7360 kurus | GN-3 16520 kurus
TOPLAM 27780 kurus
```

#### İkinci Gerçekleştirim

Sözleşmeli müşteri listesi geliyor: kademe ücretleri ve bölge katsayıları farklı. İki ağaç
kopyalanır ve gereksinim ikisine de uygulanır.

```sh
cp -r somut somut-yeni
cp -r soyut soyut-yeni
```

```js
// somut-yeni/fiyat-sozlesmeli.mjs — sozlesmeli musteri listesi
const KADEME = [[1, 3200], [5, 5300], [20, 9700]];
const BOLGE = { "34": 100, "06": 110, "65": 130 };

export function kurusHesabi(gonderi) {
  const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 13000;
  return Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 150)) / 100);
}
```

```js
// somut-yeni/web.mjs — iki somut modulu de ithal eder ve arasinda secim yapar
import { kurusHesabi } from "./fiyat.mjs";
import { kurusHesabi as sozlesmeliHesabi } from "./fiyat-sozlesmeli.mjs";

export const satir = (gonderi) =>
  `${gonderi.kod} ${gonderi.sozlesmeli ? sozlesmeliHesabi(gonderi) : kurusHesabi(gonderi)} kurus`;
```

```js
// somut-yeni/toplu.mjs — ayni secim ikinci kez
import { kurusHesabi } from "./fiyat.mjs";
import { kurusHesabi as sozlesmeliHesabi } from "./fiyat-sozlesmeli.mjs";

export const satirlar = (gonderiler) =>
  gonderiler.map((g) =>
    `${g.kod} ${g.sozlesmeli ? sozlesmeliHesabi(g) : kurusHesabi(g)} kurus`);
```

```js
// somut-yeni/rapor.mjs — ayni secim ucuncu kez
import { kurusHesabi } from "./fiyat.mjs";
import { kurusHesabi as sozlesmeliHesabi } from "./fiyat-sozlesmeli.mjs";

export const toplamSatiri = (gonderiler) =>
  `TOPLAM ${gonderiler.reduce((t, g) =>
    t + (g.sozlesmeli ? sozlesmeliHesabi(g) : kurusHesabi(g)), 0)} kurus`;
```

Soyut yerleşimde istemciler açılmaz. Yeni liste aynı sözleşmeyi karşılayan ikinci bir nesnedir
ve seçim bileşim kökünde yapılır.

```js
// soyut-yeni/fiyat-sozlesmeli.mjs — ayni sozlesmeyi karsilayan ikinci liste
const KADEME = [[1, 3200], [5, 5300], [20, 9700]];
const BOLGE = { "34": 100, "06": 110, "65": 130 };

export const sozlesmeli = {
  ad: "sozlesmeli",
  kurusHesabi(gonderi) {
    const taban = KADEME.find(([ust]) => gonderi.agirlik <= ust)?.[1] ?? 13000;
    return Math.round((taban * (BOLGE[gonderi.adres.slice(0, 2)] ?? 150)) / 100);
  },
};
```

```js
// soyut-yeni/kur.mjs — bilesim koku: hangi fiyat listesinin kullanilacagini yalniz burasi bilir
import { KAYITLAR } from "../kayit.mjs";
import { standart } from "./fiyat-standart.mjs";
import { sozlesmeli } from "./fiyat-sozlesmeli.mjs";
import { satir } from "./web.mjs";
import { satirlar } from "./toplu.mjs";
import { toplamSatiri } from "./rapor.mjs";

const fiyatSec = (g) => (g.sozlesmeli ? sozlesmeli : standart);

console.log(satir(fiyatSec(KAYITLAR[1]), KAYITLAR[1]));
console.log(satirlar(fiyatSec, KAYITLAR).join(" | "));
console.log(toplamSatiri(fiyatSec, KAYITLAR));
```

#### Bedelin Sayılması

```sh
node somut-yeni/kur.mjs
node soyut-yeni/kur.mjs
for k in somut soyut; do
  echo "$k: duzenlenen dosya = $(diff -rq $k $k-yeni | grep -c '^Files')" \
    " yeni dosya = $(diff -rq $k $k-yeni | grep -c '^Only in')" \
    " dokunulan satir = $(diff -rU0 $k $k-yeni | grep -cE '^[+-][^+-]')" \
    " secim yapan dosya = $(grep -l 'sozlesmeli ?' $k-yeni/*.mjs | wc -l | tr -d ' ')"
done
```

```
GN-2 5830 kurus
GN-1 3900 kurus | GN-2 5830 kurus | GN-3 12610 kurus
TOPLAM 22340 kurus
GN-2 5830 kurus
GN-1 3900 kurus | GN-2 5830 kurus | GN-3 12610 kurus
TOPLAM 22340 kurus
somut: duzenlenen dosya = 3  yeni dosya = 1  dokunulan satir = 18  secim yapan dosya = 3
soyut: duzenlenen dosya = 1  yeni dosya = 1  dokunulan satir = 5  secim yapan dosya = 1
```

Aynı gereksinim, aynı sonuç, farklı maliyet: on sekiz satıra karşı beş satır, üç düzenlenen
dosyaya karşı bir. Son sütun farkın nedenini veriyor — somut yerleşimde seçim üç ayrı yerde
yapılıyor. Üçüncü bir liste eklendiğinde bu üç yerin üçü de yeniden düzenlenecek; soyut
yerleşimde düzenlenecek yer bileşim kökü olarak kalacak.

#### Yerine Koyabilme

İkinci ölçü, gerçekleştirimin sınama sırasında değiştirilebilmesidir. Aynı istemciye sabit
değer üreten sahte bir fiyat listesi verilir.

```js
// sahte-dene.mjs — istemciler sahte bir fiyat listesiyle calisabiliyor mu
const SAHTE = { ad: "sahte", kurusHesabi: () => 1000 };
const G = { kod: "GN-9", agirlik: 3.0, adres: "06500", sozlesmeli: false };

const somutWeb = await import("./somut/web.mjs");
const soyutWeb = await import("./soyut/web.mjs");

console.log("somut ->", somutWeb.satir(G));
console.log("soyut ->", soyutWeb.satir(SAHTE, G));
```

```sh
node sahte-dene.mjs
```

```
somut -> GN-9 7360 kurus
soyut -> GN-9 1000 kurus
```

Somut yerleşimde sahte listenin verilecek bir yeri yok; istemci onu görmezden geldi ve gerçek
fiyatı hesapladı. Soyut yerleşimde aynı istemci sahteyi kullandı. Fark bir kolaylık değil,
yapısal: somut ithal bir seçim noktası bırakmaz.

#### İlkenin Bedeli

Soyutlama karşılıksız değildir. Soyut yerleşimde üç istemcinin imzası bir parametre uzadı ve
`fiyatSec` işlevi bileşim kökünde ek bir dolaylılık kurdu. Gerçekleştirim tek kalacaksa bu
fazlalık geri kazanılmaz: ölçüm, ikinci bir gerçekleştirim geldiğinde kazanç verdi, hiç
gelmeseydi yalnız parametre sayısını artırmış olurdu.

Ölçüt bu yüzden gerçekleştirim sayısının geçmişidir. Bir modülün ikinci bir gerçekleştirimi
hiç istenmediyse ve sınamada da değiştirilmesi gerekmiyorsa somut ithal doğru seçimdir.
Sınamada değiştirilmesi gerekiyorsa gerçekleştirim sayısı zaten ikidir; sahte de bir
gerçekleştirimdir.

#### Özet

- Soyutlamalara karşı programlama, istemcinin somut bir modüle değil, birden çok
  gerçekleştirimin karşılayabileceği bir sözleşmeye bağlanmasıdır.
- İkinci fiyat listesi eklendiğinde somut yerleşimde 3 dosya ve 18 satır, soyut yerleşimde
  1 dosya ve 5 satır dokunuldu.
- Farkın kaynağı seçim noktasıdır: somut yerleşimde seçim 3 dosyada, soyut yerleşimde 1
  dosyada yapılıyor.
- Sahte bir fiyat listesi yalnız soyut yerleşimde etkili oldu; somut ithal, gerçekleştirimin
  değiştirileceği bir nokta bırakmıyor.
- Soyutlamanın bedeli parametre sayısı ve bir dolaylılık katmanıdır; ikinci gerçekleştirim
  hiç gelmeyecekse bu bedel geri kazanılmaz.

#### Sonraki Adım

Soyut yerleşimde bileşim kökü hangi fiyat listesinin kullanılacağına karar veriyor, ama
istemcileri hâlâ kendisi çağırıyor: önce `satir`, sonra `satirlar`, sonra `toplamSatiri`.
Adımların sırası ve sayısı çağıranın gövdesinde duruyor. Bu akışa yeni bir adım eklemek —
sözleşmeli müşteri indirimi, sigorta primi, bir denetim satırı — çağıranı her seferinde
düzenlemeyi gerektirir. Sonraki ders çağıran ile çağrılan arasındaki yönü tersine çevirir:
adımların sırasını bir iskelete, adımların kendisini iskelete verilen parçalara taşır ve yeni
bir adım eklemenin maliyetini iki düzende karşılaştırır.

### Hollywood İlkesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/hollywood-ilkesi

Çağıran ile çağrılan arasındaki yönün tersine çevrilmesi: adımların sırasını üç ayrı çağıranın yazdığı düzen ile adımların kendini bir iskelete kaydettiği düzenin karşılaştırılması, sırayı bilen dosya sayısının sayılması ve yeni bir adım eklendiğinde düzenlenen dosya ile dokunulan satır sayısının ölçülmesi.

Bir önceki dersin soyut yerleşiminde bileşim kökü hangi fiyat listesinin kullanılacağına karar
veriyordu, ama istemcileri hâlâ kendisi çağırıyordu. Adımların sırası ve sayısı çağıranın
gövdesinde duruyor. Bu akışa yeni bir adım eklemek, çağıranın gövdesini her seferinde
düzenlemeyi gerektirir.

**Hollywood ilkesi** yönü tersine çevirir: "bizi arama, biz seni ararız". Parçalar üst düzey
akışı çağırmaz; üst düzey akış parçalara çağrılma noktası verir. Eşzamansız JavaScript ve
Çalışma Zamanı kursunda tanıtılan **denetimin devri** (inversion of control) bu ilkenin genel
adıdır; burada ölçülen biçimi, adım sırasının hangi modülde yazılı olduğudur.

#### Sırayı Çağıranın Yazdığı Düzen

Ücret hesabı üç adımdan oluşuyor: taban ücret, bölge katsayısı, sözleşmeli müşteri indirimi.
Üç ayrı çağıran bu sırayı kendi gövdesinde yazıyor.

```sh
mkdir -p cagiran iskelet
```

```js
// kayit.mjs — iki duzenin de kullandigi ornek gonderiler
export const KAYITLAR = [
  { kod: "GN-1", agirlik: 0.8, adres: "34100", sozlesmeli: false },
  { kod: "GN-2", agirlik: 3.0, adres: "06500", sozlesmeli: true },
  { kod: "GN-3", agirlik: 12.0, adres: "65200", sozlesmeli: true },
];
```

```js
// cagiran/adimlar.mjs — adimlar; sirayi bilmezler, cagirani da bilmezler
const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

export const taban = (kurus, g) => KADEME.find(([ust]) => g.agirlik <= ust)?.[1] ?? 15000;
export const bolge = (kurus, g) => Math.round((kurus * (BOLGE[g.adres.slice(0, 2)] ?? 160)) / 100);
export const indirim = (kurus, g) => Math.round((kurus * (g.sozlesmeli ? 90 : 100)) / 100);
```

```js
// cagiran/fatura.mjs — sirayi cagiran yaziyor
import { taban, bolge, indirim } from "./adimlar.mjs";

export function fatura(gonderi) {
  let kurus = taban(0, gonderi);
  kurus = bolge(kurus, gonderi);
  kurus = indirim(kurus, gonderi);
  return `${gonderi.kod} ${kurus} kurus`;
}
```

```js
// cagiran/onizleme.mjs — ayni sirayi ikinci kez yaziyor
import { taban, bolge, indirim } from "./adimlar.mjs";

export function onizleme(gonderi) {
  let kurus = taban(0, gonderi);
  kurus = bolge(kurus, gonderi);
  kurus = indirim(kurus, gonderi);
  return `~${kurus}`;
}
```

```js
// cagiran/toplu.mjs — ayni sirayi ucuncu kez yaziyor
import { taban, bolge, indirim } from "./adimlar.mjs";

export function toplam(gonderiler) {
  let genel = 0;
  for (const g of gonderiler) {
    let kurus = taban(0, g);
    kurus = bolge(kurus, g);
    kurus = indirim(kurus, g);
    genel += kurus;
  }
  return `TOPLAM ${genel} kurus`;
}
```

```js
// cagiran/kur.mjs — bilesim koku
import { KAYITLAR } from "../kayit.mjs";
import { fatura } from "./fatura.mjs";
import { onizleme } from "./onizleme.mjs";
import { toplam } from "./toplu.mjs";

console.log(fatura(KAYITLAR[1]), onizleme(KAYITLAR[1]), toplam(KAYITLAR));
```

#### Sırayı İskeletin Bildiği Düzen

İkinci düzende sıra bir iskelettedir. Adımlar iskeleti çağırmaz; iskelete kendilerini kaydeder
ve sıraları geldiğinde çağrılırlar.

```js
// iskelet/boru.mjs — iskelet: adimlari sirayla cagirir, adlarini onceden bilmez
const ADIMLAR = [];

export const adimEkle = (ad, islev) => ADIMLAR.push({ ad, islev });
export const adimAdlari = () => ADIMLAR.map((a) => a.ad);

export function calistir(gonderi) {
  let kurus = 0;
  for (const a of ADIMLAR) kurus = a.islev(kurus, gonderi);
  return kurus;
}
```

```js
// iskelet/adimlar.mjs — adimlar kendini iskelete kaydeder; iskeleti cagirmazlar
import { adimEkle } from "./boru.mjs";

const KADEME = [[1, 3900], [5, 6400], [20, 11800]];
const BOLGE = { "34": 100, "06": 115, "65": 140 };

adimEkle("taban", (kurus, g) => KADEME.find(([ust]) => g.agirlik <= ust)?.[1] ?? 15000);
adimEkle("bolge", (kurus, g) => Math.round((kurus * (BOLGE[g.adres.slice(0, 2)] ?? 160)) / 100));
adimEkle("indirim", (kurus, g) => Math.round((kurus * (g.sozlesmeli ? 90 : 100)) / 100));
```

```js
// iskelet/fatura.mjs — sirayi bilmez, iskeleti cagirir
import { calistir } from "./boru.mjs";

export const fatura = (gonderi) => `${gonderi.kod} ${calistir(gonderi)} kurus`;
```

```js
// iskelet/onizleme.mjs — sirayi bilmez
import { calistir } from "./boru.mjs";

export const onizleme = (gonderi) => `~${calistir(gonderi)}`;
```

```js
// iskelet/toplu.mjs — sirayi bilmez
import { calistir } from "./boru.mjs";

export const toplam = (gonderiler) =>
  `TOPLAM ${gonderiler.reduce((t, g) => t + calistir(g), 0)} kurus`;
```

```js
// iskelet/kur.mjs — bilesim koku: adimlari yukler, sonra istemcileri cagirir
import "./adimlar.mjs";
import { KAYITLAR } from "../kayit.mjs";
import { adimAdlari } from "./boru.mjs";
import { fatura } from "./fatura.mjs";
import { onizleme } from "./onizleme.mjs";
import { toplam } from "./toplu.mjs";

console.log(fatura(KAYITLAR[1]), onizleme(KAYITLAR[1]), toplam(KAYITLAR));
console.log("adimlar:", adimAdlari().join(" -> "));
```

```sh
node cagiran/kur.mjs
node iskelet/kur.mjs
```

```
GN-2 6624 kurus ~6624 TOPLAM 25392 kurus
GN-2 6624 kurus ~6624 TOPLAM 25392 kurus
adimlar: taban -> bolge -> indirim
```

Aynı sonuç. Fark, `adimAdlari()` satırında görünen şeydir: ikinci düzende adım listesi çalışma
zamanında sorulabilen bir veridir, birincisinde üç ayrı gövdeye dağılmış bir denetim akışıdır.

#### Yeni Bir Adımın Maliyeti

Sigorta primi adımı ekleniyor: indirimden sonra tutar yüzde iki artıyor. Birinci düzende yeni
işlev yazılır ve üç çağıranın gövdesine eklenir; ikincisinde tek bir kayıt satırı yeter.
Yerinde düzenlemede yedek uzantısı verilmiştir; GNU ve BSD `sed` bu biçimde aynı davranır.

```sh
cp -r cagiran cagiran-yeni
cp -r iskelet iskelet-yeni

cat >> cagiran-yeni/adimlar.mjs <<'EOF'
export const sigorta = (kurus) => Math.round((kurus * 102) / 100);
EOF
sed -i.y 's/taban, bolge, indirim/taban, bolge, indirim, sigorta/' cagiran-yeni/*.mjs
sed -i.y 's/^  kurus = indirim(kurus, gonderi);/&\n  kurus = sigorta(kurus);/' cagiran-yeni/*.mjs
sed -i.y 's/^    kurus = indirim(kurus, g);/&\n    kurus = sigorta(kurus);/' cagiran-yeni/*.mjs
rm -f cagiran-yeni/*.y

cat >> iskelet-yeni/adimlar.mjs <<'EOF'
adimEkle("sigorta", (kurus) => Math.round((kurus * 102) / 100));
EOF

node cagiran-yeni/kur.mjs
node iskelet-yeni/kur.mjs
for k in cagiran iskelet; do
  echo "$k: duzenlenen dosya = $(diff -rq $k $k-yeni | grep -c '^Files')" \
    " dokunulan satir = $(diff -rU0 $k $k-yeni | grep -cE '^[+-][^+-]')" \
    " sirayi bilen dosya = $(grep -l 'kurus = ' $k/*.mjs | wc -l | tr -d ' ')"
done
```

```
GN-2 6756 kurus ~6756 TOPLAM 25899 kurus
GN-2 6756 kurus ~6756 TOPLAM 25899 kurus
adimlar: taban -> bolge -> indirim -> sigorta
cagiran: duzenlenen dosya = 4  dokunulan satir = 10  sirayi bilen dosya = 3
iskelet: duzenlenen dosya = 1  dokunulan satir = 1  sirayi bilen dosya = 1
```

İki düzen de aynı yeni sonucu üretiyor. Maliyet dört dosya ve on satıra karşı bir dosya ve bir
satır. Son sütun nedeni veriyor: birinci düzende sırayı bilen üç dosya vardı, ikincisinde bir.

Ölçü, açık–kapalı ilkesinin ölçtüğünden farklı bir şeyi söylüyor. Orada yeni bir **tür**
eklemenin maliyeti sayılmıştı; burada sayılan, var olan bir akışa yeni bir **adım**
eklemenin maliyeti. İkisi de aynı yöne çıkar: değişikliğin metne değil veriye düşmesi.

#### Yönün Tersine Çevrilmesi

İki düzen arasındaki fark ithal grafiğinde de görünür. Birinci düzende `fatura.mjs` adımları
ithal eder ve onları çağırır; denetim yukarıdan aşağı akar. İkinci düzende `adimlar.mjs`
iskeleti ithal eder ve ona kaydolur; çağrı yönü aşağıdan yukarı kurulur, çalıştırma yönü
yukarıdan aşağı kalır. Çağıran ile çağrılan arasındaki ilişki tersine döndüğü için ilke bu
adı taşır.

Bu tersine çevirme, bağımlılığın tersine çevrilmesiyle aynı şey değildir. Orada soru
soyutlamanın hangi tarafta **tanımlandığıydı**; burada soru denetim akışının hangi tarafta
**yazıldığı**. İki ilke birlikte kullanılır: iskelet adımların sözleşmesini tanımlar
(bağımlılığın tersine çevrilmesi) ve adımları çağırma sırasını da o tutar (Hollywood ilkesi).

#### İskeletin Bedeli

Tersine çevirmenin bedeli okunabilirlikte ödenir. Birinci düzende akışı görmek için tek bir
gövdeyi okumak yeterlidir; ikincisinde sıra, kayıt çağrılarının çalışma anındaki düzenine
bağlıdır ve bu düzen bileşim kökünün ithal sırasından gelir. `adimAdlari()` gibi bir sorgunun
bulunması bu yüzden zorunludur: sıra bir veri hâline geldiğinde, o verinin okunabilir olması
gerekir.

İkinci bedel hata ayıklamadadır. Bir adım yanlış sonuç ürettiğinde birinci düzende çağrı
zinciri yığıt izinde doğrudan görünür; ikincisinde iskeletin döngüsü araya girer. Ölçüt yine
sayıdır: sırayı bilen dosya sayısı birden büyükse ve akışa adım ekleme isteği tekrarlıyorsa
iskelet kazanır; akış sabitse ve tek çağıran varsa iskelet yalnız bir dolaylılık katmanıdır.

#### Özet

- Hollywood ilkesi çağıran ile çağrılan arasındaki yönü tersine çevirir: parçalar akışı
  çağırmaz, akış parçalara çağrılma noktası verir.
- Ölçü, adım sırasının kaç dosyada yazılı olduğudur: çağıran düzeninde 3, iskelet düzeninde 1
  dosya sırayı biliyordu.
- Akışa yeni bir adım eklendiğinde çağıran düzeninde 4 dosya ve 10 satır, iskelet düzeninde
  1 dosya ve 1 satır düzenlendi.
- İskelet düzeninde adım listesi çalışma zamanında sorulabilen bir veridir; çağıran düzeninde
  üç ayrı gövdeye dağılmış bir denetim akışıdır.
- Bedel okunabilirlik ve hata ayıklamadadır; tek çağıranlı ve sabit bir akışta iskelet yalnız
  bir dolaylılık katmanı ekler.

#### Sonraki Adım

Buraya kadarki bütün ölçüler **modül çiftleri** üzerindeydi: iki modül arasındaki bağın gücü,
bir modülün içindeki adların ilgililiği, bir çağrının kaç nesne üzerinden geçtiği, bir
değişikliğin dokunduğu dosya kümesi. Kod tabanı büyüdüğünde asıl soru biçim değiştirir: artık
hangi modülün hangisine bağlanacağı değil, sınırın **nereye çizileceği** sorulur. Bu sorunun
cevabı kararların ne kadar sık değiştiğinde saklıdır — hangi karar yıllarca duran kararlı bir
politikadır, hangisi fiyat listesi her yenilendiğinde oynayan bir ayrıntı. Sonraki konu bu
ayrımı kurar ve sınırın iki tarafını değişim sıklığıyla ölçer.

## Sınırlar ve Bileşen İlkeleri

Kararlı politika ile değişken ayrıntının ayrılması, modüller arası sözleşme yüzeyinin daraltılması, dış araç bağımlılığının bağdaştırıcı arkasına alınması, birlikte değişen dosyalardan bileşen sınırı çıkarılması, bileşen kararlılığının ölçülmesi ve bağımlılık çizgesinde döngü ile katman ihlallerinin bulunması.

### Politika ve Ayrıntı Ayrımı

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/politika-ve-ayrinti-ayrimi

Kararlı kural ile değişken ayrıntının ayrılması: gönderi ücretlendirme kitaplığının değişiklik günlüğünden karar başına değişim sayısının çıkarılması, tek dosyalı ve ayrılmış düzende dosya dokunuşunun sayılması ve ayrımın davranışı değiştirmediğinin gösterilmesi.

Bağlaşım ve Uyum konusu denetimin ters çevrilmesiyle kapandı: üst düzey parçanın alt düzey
parçayı çağırmak yerine ona çağrılma noktası vermesi. Buraya kadarki bütün ölçüler **modül
çiftleri** üzerindeydi — iki modül arasındaki bağın gücü, bir modülün içindeki adların
ilgililiği, bir çağrının kaç nesne üzerinden geçtiği.

Kod tabanı büyüdüğünde soru biçim değiştirir. Artık hangi modülün hangisine bağlanacağı
değil, sınırın **nereye çizileceği** sorulur. Bu sorunun cevabı, kod parçalarının ne kadar
sık değiştiğinde saklıdır: bir kısım karar yıllarca aynı kalır, bir kısmı ayda bir yenilenir.
Birincisi **politika** (policy), ikincisi **ayrıntı** (detail) adını alır ve sınır ikisinin
arasına çizilir.

#### Ayrımın Ölçütü

Politika ile ayrıntının farkı önem değil, **değişim sıklığıdır**. Gönderi ücretlendirme
kitaplığında "ağırlık kademesinin ücreti bölge katsayısıyla çarpılır, sonuç yuvarlanır,
asgari ücretin altına inemez" cümlesi kurumun kuralıdır ve kurum kural değiştirmedikçe
durur. Kademe sınırlarının nerede olduğu, katsayıların kaç olduğu, yuvarlamanın kaç kuruşa
yapıldığı ise fiyat listesi her yenilendiğinde değişir.

Aynı ayrımın bir de yön tarafı vardır: ayrıntı politikayı bilebilir, politika ayrıntıyı
bilemez. Kural hangi kademede hangi ücretin olduğunu bilmeden de yazılabilir; tablo ise
kuralın ondan ne beklediğini bilmek zorundadır.

#### Karışık Düzen

Kitaplığın ilk sürümünde iki taraf aynı dosyada duruyor.

```sh
mkdir -p tek-dosya
```

```js
// tek-dosya/ucret.mjs — kural, tarife, bolge ve yuvarlama tek dosyada
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 },
  { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 },
  { ustAgirlik: 30, ucret: 24990 },
];
const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export function ucretHesapla(gonderi) {
  const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik);
  if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
  const bolge = POSTA_BOLGE[gonderi.postaKodu.slice(0, 2)] ?? "uzak";
  const ham = kademe.ucret * KATSAYI[bolge];
  return Math.max(Math.round(ham / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI, ASGARI_UCRET);
}
```

Dosya kısa ve çalışıyor. Sorun onun okunmasında değil, dokunulma sıklığındadır. Bunu iddia
etmek yerine saymak gerekir.

#### Değişikliğin Sayılması

Aşağıdaki modül kitaplığın on bir aylık geçmişini bir **değişiklik günlüğü** (change log)
olarak taşıyor: her satır bir değişikliğin tarihi ve o değişikliğin dokunduğu kararlardır.

```js
// degisiklik-gunlugu.mjs — kitapligin 24 degisikligi ve her birinin dokundugu kararlar
export const KARARLAR = ["kural", "kademe-tablosu", "bolge-tablosu", "yuvarlama-adimi", "asgari-tutar"];

export const GUNLUK = [
  ["2024-01-08", ["kademe-tablosu"]],
  ["2024-01-22", ["bolge-tablosu"]],
  ["2024-02-05", ["kademe-tablosu", "asgari-tutar"]],
  ["2024-02-19", ["kademe-tablosu"]],
  ["2024-03-04", ["bolge-tablosu"]],
  ["2024-03-18", ["kademe-tablosu"]],
  ["2024-04-01", ["kural"]],
  ["2024-04-15", ["kademe-tablosu"]],
  ["2024-04-29", ["bolge-tablosu"]],
  ["2024-05-13", ["kademe-tablosu", "asgari-tutar"]],
  ["2024-05-27", ["yuvarlama-adimi", "bolge-tablosu"]],
  ["2024-06-10", ["kademe-tablosu"]],
  ["2024-06-24", ["bolge-tablosu"]],
  ["2024-07-08", ["kademe-tablosu"]],
  ["2024-07-22", ["kural", "kademe-tablosu"]],
  ["2024-08-05", ["bolge-tablosu"]],
  ["2024-08-19", ["kademe-tablosu"]],
  ["2024-09-02", ["kademe-tablosu", "asgari-tutar"]],
  ["2024-09-16", ["kademe-tablosu"]],
  ["2024-09-30", ["bolge-tablosu"]],
  ["2024-10-14", ["kademe-tablosu", "yuvarlama-adimi"]],
  ["2024-10-28", ["bolge-tablosu"]],
  ["2024-11-11", ["kademe-tablosu", "asgari-tutar"]],
  ["2024-11-25", ["kural"]],
];
```

Günlük, kararları dosyalara eşleyen iki düzenle birleştirilir. Birincisi yukarıdaki tek
dosyalı düzen, ikincisi ayrımın uygulandığı düzendir.

```js
// dokunma-say.mjs — karar basina degisiklik sayisi ve iki duzende dosya dokunusu
import { KARARLAR, GUNLUK } from "./degisiklik-gunlugu.mjs";

const TEK_DOSYA = Object.fromEntries(KARARLAR.map((k) => [k, "ucret.mjs"]));
const AYRILMIS = {
  "kural": "politika/ucret-kurali.mjs",
  "kademe-tablosu": "ayrinti/tarife-tablosu.mjs",
  "yuvarlama-adimi": "ayrinti/tarife-tablosu.mjs",
  "asgari-tutar": "ayrinti/tarife-tablosu.mjs",
  "bolge-tablosu": "ayrinti/bolge-tablosu.mjs",
};

function dosyaDokunusu(esleme) {
  const sayac = new Map();
  for (const [, kararlar] of GUNLUK) {
    for (const dosya of new Set(kararlar.map((k) => esleme[k]))) {
      sayac.set(dosya, (sayac.get(dosya) ?? 0) + 1);
    }
  }
  return [...sayac].sort((a, b) => b[1] - a[1]);
}

for (const karar of KARARLAR) {
  const n = GUNLUK.filter(([, k]) => k.includes(karar)).length;
  console.log(`${karar.padEnd(16)} ${String(n).padStart(2)} degisiklik`);
}
console.log(`toplam degisiklik = ${GUNLUK.length}`);

for (const [ad, esleme] of [["tek dosya", TEK_DOSYA], ["ayrilmis", AYRILMIS]]) {
  console.log(`\n${ad} duzeni`);
  for (const [dosya, n] of dosyaDokunusu(esleme)) {
    console.log(`  ${dosya.padEnd(28)} ${String(n).padStart(2)} / ${GUNLUK.length}`);
  }
}
```

```sh
node dokunma-say.mjs
```

```
kural             3 degisiklik
kademe-tablosu   14 degisiklik
bolge-tablosu     8 degisiklik
yuvarlama-adimi   2 degisiklik
asgari-tutar      4 degisiklik
toplam degisiklik = 24

tek dosya duzeni
  ucret.mjs                    24 / 24

ayrilmis duzeni
  ayrinti/tarife-tablosu.mjs   15 / 24
  ayrinti/bolge-tablosu.mjs     8 / 24
  politika/ucret-kurali.mjs     3 / 24
```

İlk beş satır ayrımın ölçütünü veriyor: `kural` yirmi dört değişikliğin üçünde,
`kademe-tablosu` on dördünde dokunulmuş. Aradaki dört katlık fark iki kararın aynı dosyada
durmasının gerekçesini ortadan kaldırıyor.

Alt bölümdeki iki blok sonucu gösteriyor. Tek dosyalı düzende kararlı kuralı içeren dosya
24 değişikliğin 24'ünde açılıyor. Ayrılmış düzende aynı kural 24 değişikliğin 3'ünde
açılıyor; kalan 21 değişiklik ona hiç dokunmuyor.

#### Kararlı Kuralın Ayrılması

Ayrım, tabloları başka dosyaya taşımaktan ibaret değildir. Kuralın tabloları **ithal
etmemesi**, ihtiyaç duyduğu işlemleri parametre olarak alması gerekir; aksi hâlde dosya
ayrılır ama bağımlılık yönü aynı kalır.

```sh
mkdir -p ayrilmis/politika ayrilmis/ayrinti
```

```js
// ayrilmis/politika/ucret-kurali.mjs — kararli kural: adimlarin kendisi ve sirasi
export function ucretHesapla(gonderi, tarife, bolgeler) {
  const kademe = tarife.kademeBul(gonderi.agirlik);
  if (kademe === null) throw new RangeError("agirlik kademesi yok");
  const katsayi = bolgeler.katsayi(gonderi.postaKodu);
  const yuvarlanmis = tarife.yuvarla(kademe.ucret * katsayi);
  return Math.max(yuvarlanmis, tarife.asgariUcret());
}
```

Dosyada tek bir sayı yok. Kural yalnız adımların kendisini ve sırasını söylüyor: kademeyi
bul, katsayıyla çarp, yuvarla, asgarinin altına düşürme.

```js
// ayrilmis/ayrinti/tarife-tablosu.mjs — degisken ayrinti: kademeler, adim, asgari tutar
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 },
  { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 },
  { ustAgirlik: 30, ucret: 24990 },
];
const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;

export const tarife = {
  kademeBul: (agirlik) => KADEMELER.find((k) => agirlik <= k.ustAgirlik) ?? null,
  yuvarla: (tutar) => Math.round(tutar / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI,
  asgariUcret: () => ASGARI_UCRET,
};
```

```js
// ayrilmis/ayrinti/bolge-tablosu.mjs — degisken ayrinti: posta kodu esdegeri ve katsayilar
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const bolgeler = {
  bolgeBul: (postaKodu) => POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak",
  katsayi(postaKodu) { return KATSAYI[this.bolgeBul(postaKodu)]; },
};
```

İki ayrıntı dosyası da politikanın beklediği adları karşılıyor. Bu, Bağımlılığın Ters
Çevrilmesi dersindeki yönün bu ölçekteki karşılığıdır: kararsız taraf kararlı tarafa
bakıyor.

#### Ayrımın Bedeli Var mı

Bir yeniden düzenlemenin davranışı koruduğu iddia edilmez, sınanır. İki sürüm aynı
gönderilerle çağrılır.

```js
// esdeger-mi.mjs — ayrim davranisi degistirdi mi: iki surumu ayni gonderilerle karsilastirir
import { ucretHesapla as tekDosya } from "./tek-dosya/ucret.mjs";
import { ucretHesapla as ayrilmis } from "./ayrilmis/politika/ucret-kurali.mjs";
import { tarife } from "./ayrilmis/ayrinti/tarife-tablosu.mjs";
import { bolgeler } from "./ayrilmis/ayrinti/bolge-tablosu.mjs";

const GONDERILER = [
  { agirlik: 0.4, postaKodu: "34710" }, { agirlik: 3, postaKodu: "06800" },
  { agirlik: 12, postaKodu: "65100" }, { agirlik: 28, postaKodu: "35400" },
  { agirlik: 0.9, postaKodu: "81600" }, { agirlik: 5, postaKodu: "34100" },
];

let ayrilan = 0;
for (const g of GONDERILER) {
  const a = tekDosya(g), b = ayrilmis(g, tarife, bolgeler);
  if (a !== b) ayrilan += 1;
  console.log(`${String(g.agirlik).padStart(4)} kg  ${g.postaKodu}  ${String(a).padStart(6)}  ${String(b).padStart(6)}`);
}
console.log(`ayrilan sonuc = ${ayrilan} / ${GONDERILER.length}`);
```

```sh
node esdeger-mi.mjs
```

```
 0.4 kg  34710    5000    5000
   3 kg  06800   11450   11450
  12 kg  65100   27000   27000
  28 kg  35400   33750   33750
 0.9 kg  81600    9000    9000
   5 kg  34100    8500    8500
ayrilan sonuc = 0 / 6
```

Altı gönderinin altısında sonuç aynı. Ayrımın getirdiği tek şey dosya sayısının birden üçe
çıkması; buna karşılık kararlı kurala dokunan değişiklik oranı yüzde yüzden yüzde on ikiye
inmiş durumda.

#### Politika Düzeyi

Ayrım ikili değildir. Bir kitaplıkta birden çok politika düzeyi bulunur ve düzeyin ölçüsü
karara girdi-çıktıdan olan uzaklıktır. Ücret kuralı en yüksek düzeydedir: hiçbir dış
kaynağa dokunmaz. Tarife tablosu bir düzey aşağıdadır: değerler dışarıdan gelir ama biçim
kitaplığın kendi biçimidir. Posta kodunun ilk iki basamağının bölgeyi verdiği varsayımı en
alt düzeydir; adres biçimi değişirse yalnız o kural değişir.

Kural tek cümleyle yazılır: bağımlılık okları daima daha yüksek politika düzeyine bakar.
Bu ilkenin ihlali, bir sonraki dersin ölçtüğü şeyi bozar — kararlı tarafın karşı taraf
hakkında bilmek zorunda olduğu ayrıntı sayısını artırır.

#### Özet

- Politika ile ayrıntının ayrımı önem sırasına göre değil, değişim sıklığına göre yapılır;
  ölçü değişiklik günlüğünden karar başına değişim sayısıdır.
- Kitaplığın yirmi dört değişikliğinde kural üç kez, kademe tablosu on dört kez değişti;
  dört katlık fark ikisinin aynı dosyada durmasını gerekçesiz bıraktı.
- Tek dosyalı düzende kararlı kurala 24 değişikliğin 24'ünde dokunuluyordu; ayrım sonrası
  bu sayı 3'e indi.
- Ayrım, tabloları taşımakla bitmez; kuralın tabloları ithal etmemesi, ihtiyaç duyduğu
  işlemleri parametre olarak alması gerekir.
- Altı gönderide iki sürümün sonucu aynı çıktı: yeniden düzenleme davranışı değiştirmedi,
  yalnız dokunuş dağılımını değiştirdi.

#### Sonraki Adım

Ayrılmış düzende kural, tarife nesnesinden üç ad bekliyor: kademe bulma, yuvarlama ve
asgari ücret. Bu üç ad iki modül arasındaki sözleşmedir ve şu ana kadar sayılmadı. Sözleşme
kaç addan oluşuyor, sınırdan kaç tip geçiyor, karşı tarafın bilmek zorunda olduğu alan
sayısı kaç? Sonraki ders sözleşme yüzeyini ölçen bir araç yazar, geniş ve dar iki sınır
tanımını aynı ölçüyle karşılaştırır ve sınır daraltıldığında karşı tarafa sızan ayrıntı
sayısının nasıl düştüğünü gösterir.

### Sınırların Çizilmesi

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/sinirlarin-cizilmesi

Modüller arası sözleşme yüzeyinin ölçülmesi: dışa açılan ad sayısı, sınırdan geçen alan sayısı ve karşı tarafın bilmek zorunda olduğu ad sayısı; geniş ile dar sınır tanımının aynı ölçüyle karşılaştırılması ve iç adlandırma değiştiğinde hangi tarafın kırıldığının sınanması.

Bir önceki ders kararlı kuralı değişken ayrıntıdan ayırdı ve kuralın tarife nesnesinden üç
ad beklediğini gösterdi: kademe bulma, yuvarlama, asgari ücret. Bu üç ad iki modül arasındaki
**sözleşmedir** ve sözleşmenin genişliği o ana kadar ölçülmedi.

Sınır, iki modülün birbiriyle konuştuğu noktadır ve genişliği somut bir sayıyla verilir:
kaç ad dışa açılıyor, o adlardan geçen değerlerde kaç alan var, karşı taraf bunların kaçını
bilmek zorunda. Bu ders aynı işi yapan iki tasarımı bu üç ölçüyle karşılaştırıyor.

#### Ölçülecek İki Tasarım

Kitaplığın ücretlendirme modülü ile taşıyıcı seçimi modülü arasındaki sınır ele alınıyor.
Seçim modülü bir gönderi için üç taşıyıcı seçeneğinden en ucuzunu bulur ve sözleşmeli
müşteri indirimini uygular.

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

İlk tasarımda ücretlendirme modülü tablolarını ve ara adımlarını da dışa açıyor.

```js
// genis/ucret-modulu.mjs — genis sinir: tablolar, ara adimlar ve dokum disa acik
export const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
export const ASGARI_UCRET = 3990;
export const YUVARLAMA_ADIMI = 50;
export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
export const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const kademeBul = (agirlik) => KADEMELER.find((k) => agirlik <= k.ustAgirlik) ?? null;
export const bolgeBul = (postaKodu) => POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak";
export const yuvarla = (t) => Math.round(t / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;

export function ucretDokumu(gonderi) {
  const kademe = kademeBul(gonderi.agirlik);
  if (kademe === null) throw new RangeError("agirlik kademesi yok");
  const bolge = bolgeBul(gonderi.postaKodu);
  const katsayi = KATSAYI[bolge];
  const hamTutar = kademe.ucret * katsayi;
  const yuvarlanmis = yuvarla(hamTutar);
  return {
    kademeUst: kademe.ustAgirlik, kademeUcret: kademe.ucret, bolge, katsayi,
    hamTutar, yuvarlanmis, asgariUygulandi: yuvarlanmis < ASGARI_UCRET,
    toplam: Math.max(yuvarlanmis, ASGARI_UCRET),
  };
}
```

Seçim modülü indirimi uygulayabilmek için dökümün ara alanına iniyor ve yuvarlama ile
asgari ücret adımlarını kendi tarafında tekrarlıyor.

```js
// genis/tasiyici-secimi.mjs — indirimi uygulamak icin karsi tarafin ic adimlarini tekrarlar
import { ucretDokumu, yuvarla, ASGARI_UCRET } from "./ucret-modulu.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function enUygunTasiyici(gonderi, indirimOrani) {
  const dokum = ucretDokumu(gonderi);
  return TASIYICILAR
    .map((t) => ({
      tasiyici: t.ad,
      tutar: Math.max(yuvarla(dokum.hamTutar * t.carpan * (1 - indirimOrani)), ASGARI_UCRET),
      bolge: dokum.bolge,
    }))
    .sort((a, b) => a.tutar - b.tutar)[0];
}
```

İkinci tasarımda sınırdan **niyet** geçiyor: taşıyıcı çarpanı ve indirim oranı ücretlendirme
modülüne parametre olarak veriliyor, ara adımlar dışarı çıkmıyor.

```js
// dar/ucret-modulu.mjs — dar sinir: tek ad disa acik, tablolar iceride
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
const ASGARI_UCRET = 3990;
const YUVARLAMA_ADIMI = 50;
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };

export const ucretlendirici = {
  fiyatla(gonderi, { hizCarpani = 1, indirimOrani = 0 } = {}) {
    const kademe = KADEMELER.find((k) => gonderi.agirlik <= k.ustAgirlik);
    if (kademe === undefined) throw new RangeError("agirlik kademesi yok");
    const katsayi = KATSAYI[POSTA_BOLGE[gonderi.postaKodu.slice(0, 2)] ?? "uzak"];
    const hamTutar = kademe.ucret * katsayi * hizCarpani * (1 - indirimOrani);
    const yuvarlanmis = Math.round(hamTutar / YUVARLAMA_ADIMI) * YUVARLAMA_ADIMI;
    return { tutar: Math.max(yuvarlanmis, ASGARI_UCRET), paraBirimi: "kurus" };
  },
};
```

```js
// dar/tasiyici-secimi.mjs — sinirdan yalniz niyet gecer, ara adim gecmez
import { ucretlendirici } from "./ucret-modulu.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function enUygunTasiyici(gonderi, indirimOrani) {
  return TASIYICILAR
    .map((t) => {
      const fiyat = ucretlendirici.fiyatla(gonderi, { hizCarpani: t.carpan, indirimOrani });
      return { tasiyici: t.ad, tutar: fiyat.tutar };
    })
    .sort((a, b) => a.tutar - b.tutar)[0];
}
```

İki tasarımın karşılaştırılabilmesi için aynı sonucu üretmeleri gerekir.

```js
// secim-karsilastir.mjs — iki sinir tanimi ayni sonucu mu veriyor
import { enUygunTasiyici as genis } from "./genis/tasiyici-secimi.mjs";
import { enUygunTasiyici as dar } from "./dar/tasiyici-secimi.mjs";

const DENEMELER = [
  [{ agirlik: 3, postaKodu: "06800" }, 0],
  [{ agirlik: 12, postaKodu: "65100" }, 0.1],
  [{ agirlik: 0.4, postaKodu: "34710" }, 0.25],
];
for (const [g, indirim] of DENEMELER) {
  const a = genis(g, indirim), b = dar(g, indirim);
  console.log(`${String(g.agirlik).padStart(4)} kg ${g.postaKodu} indirim ${indirim}  ${a.tasiyici}/${a.tutar}  ${b.tasiyici}/${b.tutar}`);
}
```

```
   3 kg 06800 indirim 0  ekonomik/9750  ekonomik/9750
  12 kg 65100 indirim 0.1  ekonomik/20650  ekonomik/20650
 0.4 kg 34710 indirim 0.25  standart/3990  standart/3990
```

Üç denemede de aynı taşıyıcı ve aynı tutar. Fark davranışta değil, sınırda.

#### Sözleşme Yüzeyinin Ölçülmesi

Ölçüm aracı üç şey sayar: modülün dışa açtığı adlar, sınırdan geçen değerin alanları ve
tüketicinin bu ikisinden fiilen andığı adlar.

```js
// yuzey-olc.mjs — bir sinirin sozlesme yuzeyini sayan olcum araci
import { readFileSync } from "node:fs";

const DISA_ACILAN = /^export\s+(?:const|let|function|class)\s+(\w+)/gm;

export const disaAcilanlar = (yol) =>
  [...readFileSync(yol, "utf8").matchAll(DISA_ACILAN)].map((m) => m[1]);

export function ithalEdilenler(tuketici, kaynakEki) {
  const kalip = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*"[^"]*${kaynakEki}"`, "g");
  return [...readFileSync(tuketici, "utf8").matchAll(kalip)]
    .flatMap((m) => m[1].split(",").map((s) => s.trim())).filter(Boolean);
}

export const okunanAlanlar = (tuketici, degisken) =>
  [...new Set([...readFileSync(tuketici, "utf8")
    .matchAll(new RegExp(`\\b${degisken}\\.(\\w+)`, "g"))].map((m) => m[1]))];
```

Sınırdan geçen alan sayısı kaynak metinden değil, çağrının **gerçek dönüş değerinden**
alınır; böylece dolaylı olarak eklenen alanlar da sayıya girer.

```js
// yuzey-raporu.mjs — iki sinir tanimini ayni olculerle karsilastirir
import { disaAcilanlar, ithalEdilenler, okunanAlanlar } from "./yuzey-olc.mjs";
import { ucretDokumu } from "./genis/ucret-modulu.mjs";
import { ucretlendirici } from "./dar/ucret-modulu.mjs";

const ORNEK = { agirlik: 3, postaKodu: "06800" };
const SINIRLAR = [
  { ad: "genis", modul: "genis/ucret-modulu.mjs", tuketici: "genis/tasiyici-secimi.mjs",
    degisken: "dokum", gecen: ucretDokumu(ORNEK) },
  { ad: "dar  ", modul: "dar/ucret-modulu.mjs", tuketici: "dar/tasiyici-secimi.mjs",
    degisken: "fiyat", gecen: ucretlendirici.fiyatla(ORNEK) },
];

for (const s of SINIRLAR) {
  const disa = disaAcilanlar(s.modul);
  const gecen = Object.keys(s.gecen);
  const ithal = ithalEdilenler(s.tuketici, "ucret-modulu.mjs");
  const okunan = okunanAlanlar(s.tuketici, s.degisken);
  console.log(`${s.ad} sinir`);
  console.log(`  disa acilan ad        ${String(disa.length).padStart(2)}   ${disa.join(", ")}`);
  console.log(`  sinirdan gecen alan   ${String(gecen.length).padStart(2)}   ${gecen.join(", ")}`);
  console.log(`  tuketicinin ithali    ${String(ithal.length).padStart(2)}   ${ithal.join(", ")}`);
  console.log(`  tuketicinin okudugu   ${String(okunan.length).padStart(2)}   ${okunan.join(", ")}`);
  console.log(`  bilinmesi zorunlu ad  ${String(ithal.length + okunan.length).padStart(2)}`);
}
```

```
genis sinir
  disa acilan ad         9   KADEMELER, ASGARI_UCRET, YUVARLAMA_ADIMI, KATSAYI, POSTA_BOLGE, kademeBul, bolgeBul, yuvarla, ucretDokumu
  sinirdan gecen alan    8   kademeUst, kademeUcret, bolge, katsayi, hamTutar, yuvarlanmis, asgariUygulandi, toplam
  tuketicinin ithali     3   ucretDokumu, yuvarla, ASGARI_UCRET
  tuketicinin okudugu    2   hamTutar, bolge
  bilinmesi zorunlu ad   5
dar   sinir
  disa acilan ad         1   ucretlendirici
  sinirdan gecen alan    2   tutar, paraBirimi
  tuketicinin ithali     1   ucretlendirici
  tuketicinin okudugu    1   tutar
  bilinmesi zorunlu ad   2
```

Dışa açılan ad dokuzdan bire, sınırdan geçen alan sekizden ikiye, karşı tarafın bilmek
zorunda olduğu ad beşten ikiye indi. Üç ölçünün üçü de aynı yöne bakıyor.

Dışa açılan ad sayısı ile bilinmesi zorunlu ad sayısı arasındaki fark ayrıca anlamlıdır.
Geniş sınırda dokuz ad açık ama tüketici beşini anıyor; kalan dört ad kullanılmadığı hâlde
sözleşmenin parçasıdır, çünkü başka bir tüketici onlara bağlanabilir. Sözleşme, fiilen
kullanılan yüzey değil, **kullanılabilir** yüzeydir.

#### Sınır Neyi Korur

Ölçünün karşılığı, modül sahibinin iç adlandırmayı değiştirmekte ne kadar özgür olduğudur.
Aşağıdaki komut her iki tasarımın bir kopyasını alıp ücretlendirme modülündeki `hamTutar`
adını `araToplam` yapıyor. Tüketici dosyalarına dokunulmuyor.

```sh
cp -r genis genis-v2 && cp -r dar dar-v2
sed -i.yedek 's/hamTutar/araToplam/g' genis-v2/ucret-modulu.mjs
sed -i.yedek 's/hamTutar/araToplam/g' dar-v2/ucret-modulu.mjs
grep -c araToplam genis-v2/ucret-modulu.mjs dar-v2/ucret-modulu.mjs
```

```
genis-v2/ucret-modulu.mjs:3
dar-v2/ucret-modulu.mjs:2
```

BSD ve GNU `sed` arasında yerinde düzenleme seçeneğinin yazımı farklıdır; `-i.yedek`
biçimi ikisinde de çalışır ve yanına bir yedek dosya bırakır.

```js
// kirilma-dene.mjs — modul sahibi bir ic adi degistirdiginde hangi taraf kiriliyor
import { enUygunTasiyici as genisV2 } from "./genis-v2/tasiyici-secimi.mjs";
import { enUygunTasiyici as darV2 } from "./dar-v2/tasiyici-secimi.mjs";

const G = { agirlik: 3, postaKodu: "06800" };
console.log("genis-v2 ->", JSON.stringify(genisV2(G, 0)));
console.log("dar-v2   ->", JSON.stringify(darV2(G, 0)));
```

```
genis-v2 -> {"tasiyici":"hizli","tutar":null,"bolge":"orta"}
dar-v2   -> {"tasiyici":"ekonomik","tutar":9750}
```

Geniş sınırda üç satırlık bir iç yeniden adlandırma tüketiciyi bozdu; üstelik hata
fırlatmadan bozdu. Okunmayan alan `undefined` döndü, çarpım `NaN` üretti ve JSON'a
dönüştüğünde `null` oldu. Dar sınırda aynı değişiklik hiçbir şeye dokunmadı, çünkü
`hamTutar` sınırın dışına hiç çıkmamıştı.

Sessiz bozulma buradaki asıl bulgudur. Geniş bir sınır yalnız değişimi zorlaştırmaz;
değişimin sonucunu da gizler.

#### Sınır Nereye Çizilir

Ölçüler bir tasarımın iyiliğini kanıtlamaz, karşılaştırma için ortak zemin verir. Sınırın
yerini seçerken üç soru sorulur.

Birincisi: sınırdan **veri** mi geçiyor, **niyet** mi? Geniş tasarımda döküm nesnesi bir
hesap adımının fotoğrafıdır; dar tasarımda parametreler bir isteğin tarifidir. Bir önceki
konudaki sorma, söyle ilkesinin modül ölçeğindeki karşılığı budur.

İkincisi: aynı hesap iki tarafta da yazılıyor mu? Geniş tasarımda yuvarlama ve asgari ücret
adımları hem ücretlendirme modülünde hem seçim modülünde duruyordu. Kuralın iki yerde
durması, sınırın yanlış yerden geçtiğinin en somut belirtisidir.

Üçüncüsü: sınır **hangi eksende** değişime dayanıklı? Dar tasarım iç adlandırmaya ve ara
adımların sırasına dayanıklıdır ama taşıyıcı çarpanının parametre olduğu varsayımına
bağlıdır. Her sınır bir şeyi korur, hepsini değil; korunan şeyin ne olduğu yazılabilir
olmalıdır.

#### Özet

- Sözleşme yüzeyi üç sayıyla ölçülür: dışa açılan ad sayısı, sınırdan geçen değerin alan
  sayısı ve karşı tarafın bilmek zorunda olduğu ad sayısı.
- Aynı davranışı veren iki tasarımda bu üç sayı sırasıyla 9→1, 8→2 ve 5→2 olarak değişti.
- Dışa açılan ad sayısı ile fiilen kullanılan ad sayısı ayrıdır; sözleşme kullanılabilir
  yüzeydir, kullanılan yüzey değil.
- Ücretlendirme modülünde üç satırlık bir iç yeniden adlandırma geniş sınırda tüketiciyi
  sessizce bozdu, dar sınırda hiçbir etki yapmadı.
- Sınırın yeri üç soruyla seçilir: veri mi niyet mi geçiyor, aynı hesap iki tarafta da var
  mı, sınır hangi eksende değişime dayanıklı.

#### Sonraki Adım

Dar sınırın koruduğu şey kitaplığın kendi iç adlandırmasıydı. Kitaplığın dışında ise
kendisinin yazmadığı bir kod sınıfı duruyor: istek yönlendiren, süreç başlatan, yaşam
döngüsünü yöneten araçlar. Bunlar bir sözleşme sunar ve o sözleşme kitaplığın denetiminde
değildir. Sonraki ders küçük bir dış araç tanımlar, iki düzende ona kaç dosyanın dokunduğunu
sayar, aracın sözleşmesi değiştiğinde her iki düzende kaç dosyanın düzeltilmesi gerektiğini
ölçer ve yalıtımın bedelini de aynı ölçekte gösterir.

### Çerçeve Kodunu Uzakta Tutmak

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/cerceve-kodunu-uzakta-tutmak

Çerçeve bağımlılığının yalıtımı: küçük bir dış aracın sözleşmesine dokunan dosyaların iki düzende sayılması, aracın sözleşmesi değiştiğinde düzeltilmesi gereken dosya sayısının karşılaştırılması ve bağdaştırıcının dosya ile satır cinsinden bedelinin ölçülmesi.

Bir önceki ders sözleşme yüzeyini üç sayıyla ölçtü ve dar sınırın kitaplığın kendi iç
adlandırmasını koruduğunu gösterdi. O sınırın iki tarafı da kitaplığın kendi kodudur:
sözleşmeyi kim yazacaksa kitaplık yazar, daraltma kararı da kitaplığındır.

Kitaplığın dışında ise sözleşmesini başkasının yazdığı bir kod sınıfı durur — isteği
karşılayan, yolu eşleştiren, işleyiciyi çağıran araçlar. Bunlar bir sözleşme dayatır ve o
sözleşme değişirken kitaplığa danışılmaz. Bu ders böyle bir aracın sözleşmesine dokunan
dosya sayısını iki düzende sayar, sonra sözleşmeyi değiştirip düzeltilmesi gereken dosyaları
karşılaştırır.

#### Çerçeve ile Kütüphanenin Farkı

Ayrım işlevde değil, denetimin yönündedir. Bir kütüphane **çağrılır**: çağrıların sırasını
yazan taraf kitaplıktır. Bir **çerçeve** (framework) ise **çağırır**: yazılan şey onun
beklediği biçimde bir işleyicidir, ne zaman çalışacağına çerçeve karar verir. Bağlaşım ve Uyum
konusundaki Hollywood ilkesi bu yönün adıdır; aynı terslik arayüz tarafında Bileşen Tabanlı
Arayüz Geliştirme kursunda kurulmuştu.

Yön tersliğinin bedeli, bağımlılığın imzada durmamasıdır. Kütüphane değişirse çağrı yerleri
düzeltilir ve çağrı yerleri aranabilir. Çerçeve değişirse, çerçevenin verdiği **bağlam**
nesnesinin alanlarını okuyan her yer düzeltilir. Kaç yer olduğu bir ürün seçimi değil,
tasarım kararıdır — ölçülebilir bir karardır.

Ölçüm için küçük bir dış araç yazılıyor: yol desenlerini eşleştiren ve eşleşen işleyiciyi
kendi bağlam nesnesiyle çağıran bir yönlendirici. Sözlüğü altı addan oluşuyor.

```sh
mkdir -p arac dogrudan bagdastirici
```

```js
// arac/istek-yonlendirici.mjs — dis arac v1: yol desenini eslestirir, isleyiciyi kendi baglamiyla cagirir
export function yonlendiriciOlustur() {
  const kayitlar = [];
  const eslestir = (desen, yol) => {
    const d = desen.split("/"), p = yol.split("/");
    if (d.length !== p.length) return null;
    const cikan = {};
    for (let i = 0; i < d.length; i += 1) {
      if (d[i][0] === ":") cikan[d[i].slice(1)] = p[i];
      else if (d[i] !== p[i]) return null;
    }
    return cikan;
  };
  return {
    desenEkle: (desen, isleyici) => kayitlar.push([desen, isleyici]),
    istekIsle(yol, alanlar = {}) {
      for (const [desen, isleyici] of kayitlar) {
        const eslesme = eslestir(desen, yol);
        if (eslesme === null) continue;
        let yanit = null;
        isleyici({ parametreler: eslesme, sorgu: alanlar,
          yanitla: (kod, govde) => { yanit = { kod, govde }; } });
        return yanit ?? { kod: 500, govde: { hata: "yanit uretilmedi" } };
      }
      return { kod: 404, govde: { hata: "yol bulunamadi" } };
    },
  };
}
```

Ücret tabloları iki düzende de ortaktır ve aracı hiç anmaz.

```js
// tarife.mjs — iki duzenin de kullandigi tablolar; dis araci hic anmaz
const KADEMELER = [
  { ustAgirlik: 1, ucret: 4990 }, { ustAgirlik: 5, ucret: 8490 },
  { ustAgirlik: 15, ucret: 14990 }, { ustAgirlik: 30, ucret: 24990 },
];
const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
const POSTA_BOLGE = { 34: "yakin", "06": "orta", 35: "orta", 65: "uzak" };
const ASGARI_UCRET = 3990;

export function tabanUcret(agirlik, postaKodu) {
  const kademe = KADEMELER.find((k) => agirlik <= k.ustAgirlik);
  if (kademe === undefined) return null;
  const katsayi = KATSAYI[POSTA_BOLGE[postaKodu.slice(0, 2)] ?? "uzak"];
  return Math.max(Math.round((kademe.ucret * katsayi) / 50) * 50, ASGARI_UCRET);
}
```

#### Doğrudan Bağlanan Düzen

İlk düzende iş kuralı doğrudan aracın bağlamını okuyor ve yanıtını yine onun üzerinden
veriyor. Doğrulama bile aracın sözlüğüne bağlı.

```js
// dogrudan/dogrulama.mjs — dogrulama da yanitini aracin baglami uzerinden veriyor
export function agirligiDogrula(baglam) {
  const agirlik = Number(baglam.parametreler.agirlik);
  if (Number.isFinite(agirlik) && agirlik > 0) return agirlik;
  baglam.yanitla(400, { hata: "agirlik gecersiz" });
  return null;
}
```

```js
// dogrudan/ucret-ucnoktasi.mjs — hesap dogrudan aracin baglamindan okur ve ona yazar
import { tabanUcret } from "../tarife.mjs";
import { agirligiDogrula } from "./dogrulama.mjs";

export function ucretIsleyici(baglam) {
  const agirlik = agirligiDogrula(baglam);
  if (agirlik === null) return;
  const tutar = tabanUcret(agirlik, baglam.parametreler.postaKodu);
  if (tutar === null) { baglam.yanitla(422, { hata: "agirlik kademesi yok" }); return; }
  const indirim = Number(baglam.sorgu.indirim ?? 0);
  baglam.yanitla(200, { tutar: Math.round(tutar * (1 - indirim)), paraBirimi: "kurus" });
}
```

```js
// dogrudan/tasiyici-ucnoktasi.mjs — ikinci uc nokta, ayni baglam sozlesmesine bagli
import { tabanUcret } from "../tarife.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function tasiyiciIsleyici(baglam) {
  const taban = tabanUcret(Number(baglam.parametreler.agirlik), baglam.parametreler.postaKodu);
  if (taban === null) { baglam.yanitla(422, { hata: "agirlik kademesi yok" }); return; }
  const enUcuz = TASIYICILAR
    .map((t) => ({ tasiyici: t.ad, tutar: Math.round(taban * t.carpan) }))
    .sort((a, b) => a.tutar - b.tutar)[0];
  baglam.yanitla(200, enUcuz);
}
```

```js
// dogrudan/uygulama.mjs — araci kuran ve uc noktalari kaydeden dosya
import { yonlendiriciOlustur } from "../arac/istek-yonlendirici.mjs";
import { ucretIsleyici } from "./ucret-ucnoktasi.mjs";
import { tasiyiciIsleyici } from "./tasiyici-ucnoktasi.mjs";

export function uygulama() {
  const y = yonlendiriciOlustur();
  y.desenEkle("/ucret/:agirlik/:postaKodu", ucretIsleyici);
  y.desenEkle("/tasiyici/:agirlik/:postaKodu", tasiyiciIsleyici);
  return (yol, alanlar) => y.istekIsle(yol, alanlar);
}
```

#### Bağdaştırıcı Arkasındaki Düzen

İkinci düzende iş kuralı düz bir işlemdir: `(istek) => yanit`. İsteğin de yanıtın da alan
adları kitaplığın kendi seçimidir, aracınki değil. Araç ile bu biçim arasındaki çeviriyi tek
bir dosya yapar; bu dosyaya **bağdaştırıcı** (adapter) denir.

```js
// bagdastirici/dogrulama.mjs — dogrulama duz bir sonuc dondurur, arac adi gecmez
export function agirligiDogrula(degerler) {
  const agirlik = Number(degerler.agirlik);
  if (Number.isFinite(agirlik) && agirlik > 0) return { gecerli: true, agirlik };
  return { gecerli: false, yanit: { durum: 400, icerik: { hata: "agirlik gecersiz" } } };
}
```

```js
// bagdastirici/ucret-islemi.mjs — islem duz bir istek alir, duz bir yanit dondurur
import { tabanUcret } from "../tarife.mjs";
import { agirligiDogrula } from "./dogrulama.mjs";

export function ucretIslemi(istek) {
  const d = agirligiDogrula(istek.degerler);
  if (!d.gecerli) return d.yanit;
  const tutar = tabanUcret(d.agirlik, istek.degerler.postaKodu);
  if (tutar === null) return { durum: 422, icerik: { hata: "agirlik kademesi yok" } };
  const indirim = Number(istek.secenekler.indirim ?? 0);
  return { durum: 200, icerik: { tutar: Math.round(tutar * (1 - indirim)), paraBirimi: "kurus" } };
}
```

```js
// bagdastirici/tasiyici-islemi.mjs — ikinci islem, ayni duz sozlesme
import { tabanUcret } from "../tarife.mjs";

const TASIYICILAR = [
  { ad: "hizli", carpan: 1.25 }, { ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 },
];

export function tasiyiciIslemi(istek) {
  const taban = tabanUcret(Number(istek.degerler.agirlik), istek.degerler.postaKodu);
  if (taban === null) return { durum: 422, icerik: { hata: "agirlik kademesi yok" } };
  const enUcuz = TASIYICILAR
    .map((t) => ({ tasiyici: t.ad, tutar: Math.round(taban * t.carpan) }))
    .sort((a, b) => a.tutar - b.tutar)[0];
  return { durum: 200, icerik: enUcuz };
}
```

```js
// bagdastirici/arac-koprusu.mjs — araca dokunan tek dosya: baglami duz isteke, duz yaniti baglama cevirir
import { yonlendiriciOlustur } from "../arac/istek-yonlendirici.mjs";

export function uygulamaKur(kayitlar) {
  const y = yonlendiriciOlustur();
  for (const [desen, islem] of kayitlar) {
    y.desenEkle(desen, (baglam) => {
      const yanit = islem({ degerler: baglam.parametreler, secenekler: baglam.sorgu });
      baglam.yanitla(yanit.durum, yanit.icerik);
    });
  }
  return (yol, alanlar) => y.istekIsle(yol, alanlar);
}
```

```js
// bagdastirici/uygulama.mjs — kayit dosyasi; aracin sozlugunden hicbir ad gecmez
import { uygulamaKur } from "./arac-koprusu.mjs";
import { ucretIslemi } from "./ucret-islemi.mjs";
import { tasiyiciIslemi } from "./tasiyici-islemi.mjs";

export const uygulama = () => uygulamaKur([
  ["/ucret/:agirlik/:postaKodu", ucretIslemi],
  ["/tasiyici/:agirlik/:postaKodu", tasiyiciIslemi],
]);
```

Karşılaştırmanın anlamlı olması için iki düzenin aynı istekleri aynı biçimde yanıtlaması
gerekir.

```js
// kosum.mjs — iki duzen ayni istekleri ayni sekilde yanitliyor mu
import { uygulama as dogrudanUygulama } from "./dogrudan/uygulama.mjs";
import { uygulama as koprulu } from "./bagdastirici/uygulama.mjs";

const ISTEKLER = [
  ["/ucret/3/06800", {}], ["/ucret/12/65100", { indirim: 0.1 }],
  ["/ucret/0/34710", {}], ["/tasiyici/28/35400", {}], ["/rota/1/34000", {}],
];

const a = dogrudanUygulama(), b = koprulu();
let ayrilan = 0;
for (const [yol, sorgu] of ISTEKLER) {
  const x = JSON.stringify(a(yol, sorgu)), y = JSON.stringify(b(yol, sorgu));
  if (x !== y) ayrilan += 1;
  console.log(`${yol.padEnd(22)} ${x}`);
}
console.log(`ayrilan yanit = ${ayrilan} / ${ISTEKLER.length}`);
```

```
/ucret/3/06800         {"kod":200,"govde":{"tutar":11450,"paraBirimi":"kurus"}}
/ucret/12/65100        {"kod":200,"govde":{"tutar":24300,"paraBirimi":"kurus"}}
/ucret/0/34710         {"kod":400,"govde":{"hata":"agirlik gecersiz"}}
/tasiyici/28/35400     {"kod":200,"govde":{"tasiyici":"ekonomik","tutar":28688}}
/rota/1/34000          {"kod":404,"govde":{"hata":"yol bulunamadi"}}
ayrilan yanit = 0 / 5
```

Beş istekte de aynı durum kodu ve aynı gövde. Geçersiz ağırlık, kademe dışı ağırlık ve
tanımsız yol dâhil, fark davranışta değil.

#### Araca Dokunan Dosya Sayısı

Yalıtımın ölçüsü, aracın sözlüğünden en az bir ad geçen dosya sayısıdır. Sözlük altı addan
oluşuyor: kurucu, kayıt ve çalıştırma adları ile bağlamın üç alanı.

```js
// dokunan-say.mjs — aracin sozlugunden ad gecen dosyalari sayar
import { readdirSync, readFileSync } from "node:fs";

const SOZLUK = ["yonlendiriciOlustur", "desenEkle", "istekIsle", "parametreler", "sorgu", "yanitla"];

for (const dizin of ["dogrudan", "bagdastirici"]) {
  const dosyalar = readdirSync(dizin).filter((d) => d.endsWith(".mjs")).sort();
  let dokunan = 0;
  console.log(`${dizin}/`);
  for (const d of dosyalar) {
    const metin = readFileSync(`${dizin}/${d}`, "utf8");
    const gecen = SOZLUK.filter((ad) => new RegExp(`\\b${ad}\\b`).test(metin));
    if (gecen.length > 0) dokunan += 1;
    console.log(`  ${d.padEnd(24)} ${gecen.length > 0 ? gecen.join(" ") : "-"}`);
  }
  console.log(`  araca dokunan dosya = ${dokunan} / ${dosyalar.length}`);
}
```

```
dogrudan/
  dogrulama.mjs            parametreler yanitla
  tasiyici-ucnoktasi.mjs   parametreler yanitla
  ucret-ucnoktasi.mjs      parametreler sorgu yanitla
  uygulama.mjs             yonlendiriciOlustur desenEkle istekIsle
  araca dokunan dosya = 4 / 4
bagdastirici/
  arac-koprusu.mjs         yonlendiriciOlustur desenEkle istekIsle parametreler sorgu yanitla
  dogrulama.mjs            -
  tasiyici-islemi.mjs      -
  ucret-islemi.mjs         -
  uygulama.mjs             -
  araca dokunan dosya = 1 / 5
```

İlk düzende dört dosyanın dördü de araca bağlı; ikincisinde beş dosyanın biri. Dikkate değer
satır `dogrudan/dogrulama.mjs`: ortada uç nokta olmadığı hâlde ağırlık doğrulaması aracın
sözlüğüne girmiş. Çerçeveye bağlanma uç noktalarda kalmaz, hata yolunu izleyerek içeri
yürür.

#### Aracın Sözleşmesi Değişince

Sayılar ancak bir değişiklik geldiğinde karşılığını verir. Aracın ikinci sürümü bağlamın üç
adını değiştiriyor: `parametreler` yerine `yolDegerleri`, `sorgu` yerine `sorguDegerleri`,
iki argümanlı `yanitla` yerine tek nesne alan `gonder`. Yeni sürüm birincisinden türetilip
ayrı bir ağaca konuyor.

```sh
mkdir -p v2 && cp -r arac dogrudan bagdastirici tarife.mjs kosum.mjs v2/
sed -e 's/parametreler: eslesme, sorgu: alanlar,/yolDegerleri: eslesme, sorguDegerleri: alanlar,/' \
    -e 's/yanitla: (kod, govde) => { yanit = { kod, govde }; }/gonder: ({ durum, govde }) => { yanit = { kod: durum, govde }; }/' \
    arac/istek-yonlendirici.mjs > v2/arac/istek-yonlendirici.mjs
grep -n "yolDegerleri\|gonder:" v2/arac/istek-yonlendirici.mjs
```

```
21:        isleyici({ yolDegerleri: eslesme, sorguDegerleri: alanlar,
22:          gonder: ({ durum, govde }) => { yanit = { kod: durum, govde }; } });
```

Aracın kendi içinde değişen tek yer bağlam nesnesinin kurulduğu iki satır. Şimdi aynı geçiş
her iki düzene uygulanıyor ve düzeltilen dosyalar sayılıyor. Yerinde düzenleme seçeneğinin
yazımı BSD ile GNU `sed` arasında ayrıştığı için `-i.yedek` biçimi kullanılıyor; yanına
yedek dosya bırakır ve `.mjs` süzgecine takılmaz.

```sh
sed -i.yedek -e 's/baglam\.parametreler/baglam.yolDegerleri/g' \
             -e 's/baglam\.sorgu/baglam.sorguDegerleri/g' \
             -e 's/baglam\.yanitla(\([^,]*\), \(.*\))/baglam.gonder({ durum: \1, govde: \2 })/g' \
             v2/dogrudan/*.mjs v2/bagdastirici/*.mjs
for d in dogrudan bagdastirici; do
  n=0; t=0
  for f in "$d"/*.mjs; do
    t=$((t + 1)); cmp -s "$f" "v2/$f" || n=$((n + 1))
  done
  echo "$d: duzeltilen dosya = $n / $t"
done
cd v2 && node kosum.mjs
```

```
dogrudan: duzeltilen dosya = 3 / 4
bagdastirici: duzeltilen dosya = 1 / 5
/ucret/3/06800         {"kod":200,"govde":{"tutar":11450,"paraBirimi":"kurus"}}
/ucret/12/65100        {"kod":200,"govde":{"tutar":24300,"paraBirimi":"kurus"}}
/ucret/0/34710         {"kod":400,"govde":{"hata":"agirlik gecersiz"}}
/tasiyici/28/35400     {"kod":200,"govde":{"tasiyici":"ekonomik","tutar":28688}}
/rota/1/34000          {"kod":404,"govde":{"hata":"yol bulunamadi"}}
ayrilan yanit = 0 / 5
```

Doğrudan bağlanan düzende üç dosya düzeltildi, bağdaştırıcılı düzende bir. Kayıt dosyası
`dogrudan/uygulama.mjs` bu sefer değişmedi, çünkü değişen adlar bağlamın alanlarıydı, kurucu
ve kayıt adları değil. Aracın kurucu adını da değiştiren bir sürüm gelseydi doğrudan düzende
dört, bağdaştırıcılı düzende yine bir dosya düzeltilecekti: köprünün sayısı aracın
sözleşmesindeki değişikliğin büyüklüğünden bağımsızdır.

İki sürümün beş isteğe verdiği yanıtlar da aynı kaldı; geçiş davranışı değiştirmedi.

#### Yalıtımın Bedeli

Bağdaştırıcı bedava değildir ve bedeli aynı ölçekte sayılabilir.

```sh
for d in dogrudan bagdastirici; do
  echo "$d: $(ls "$d"/*.mjs | wc -l | tr -d ' ') dosya, $(cat "$d"/*.mjs | wc -l | tr -d ' ') satir"
done
```

```
dogrudan: 4 dosya, 45 satir
bagdastirici: 5 dosya, 55 satir
```

Bir dosya ve on satır fazladan. Buna karşılık aracın sözleşmesindeki bir değişiklikte
düzeltilen dosya sayısı üçten bire indi. On satırlık sabit maliyet, sözleşme her
değiştiğinde tekrarlanan iki dosyalık farkla karşılaştırılır; aracın sözleşmesi hiç
değişmeyecekse yalıtım karşılığını vermez.

İkinci bedel sayıyla değil, kapsamla ölçülür: köprü yalnız çevirdiği kadarını geçirir.
Aracın sunduğu ama köprünün eşlemediği bir yetenek — akış hâlinde yanıt, istek iptali — iş
kuralı tarafından kullanılamaz; kullanılacaksa önce köprünün sözleşmesine eklenmesi gerekir.
Bu, ilkeyi "her çerçeve mutlaka sarmalanır" biçiminde okumayı engeller. Yalıtımın gerekçesi,
aracın sözleşmesinin kitaplığın denetiminde olmaması ve değişme geçmişinin bulunmasıdır;
gerekçe yoksa köprü yalnız bir dolaylılık katmanıdır.

#### Özet

- Çerçeve ile kütüphane işlevle değil denetimin yönüyle ayrılır: kütüphane çağrılır, çerçeve
  çağırır; bu yüzden çerçeve bağımlılığı çağrı yerlerinde değil, bağlamı okuyan her yerde
  durur.
- Yalıtımın ölçüsü, aracın sözlüğünden en az bir ad geçen dosya sayısıdır: doğrudan bağlanan
  düzende 4/4, bağdaştırıcılı düzende 1/5.
- Çerçeveye bağlanma uç noktalarda kalmaz; hata yolunu izleyerek doğrulama modülüne kadar
  yürüdü.
- Aracın bağlam sözleşmesindeki üç ad değişince doğrudan düzende 3, bağdaştırıcılı düzende 1
  dosya düzeltildi; beş isteğin yanıtı iki düzende de değişmedi.
- Yalıtımın bedeli 1 dosya ve 10 satırdır, üstelik köprü yalnız eşlediği yeteneği geçirir;
  aracın sözleşmesi değişmiyorsa bu bedel karşılıksız kalır.

#### Sonraki Adım

Buraya kadarki üç ders sınırın nereye çizileceğini tek tek dosyalar üzerinde tartıştı:
politika hangi dosyada, sözleşme kaç addan oluşuyor, araca kaç dosya dokunuyor. Kod tabanı
büyüdüğünde birim dosya olmaktan çıkar; birlikte paketlenen, birlikte sürümlenen ve birlikte
yayımlanan dosya kümesi bir **bileşen** olur. O zaman soru şudur: hangi dosyalar aynı
bileşene girmelidir? Sonraki ders bu soruyu değişiklik günlüğünden yanıtlar — birlikte
değişen dosyaları kümeleyen bir araç yazar, bileşen sınırı bu kümelere göre çizildiğinde
bileşen sayısını ve bileşenler arası bağ sayısını ölçer, bir değişikliğin kaç bileşene
dokunduğunu ve tek bir yeteneğin kaç dosyaya bağladığını üç bölünmede karşılaştırır.

### Bileşen Uyum İlkeleri

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/bilesen-uyum-ilkeleri

Bileşen sınırının birlikte değişen dosyalardan çıkarılması: değişiklik günlüğünden birlikte değişim sayısının hesaplanması, dosyaların eşikle kümelenmesi ve dizin bölünmesi ile birlikte-değişim bölünmesinin bileşen sayısı, yeniden yayın sayısı ve bağlanılan dosya sayısıyla karşılaştırılması.

Buraya kadarki üç ders sınırı tek tek dosyalar üzerinde tartıştı: politika hangi dosyada
duruyor, iki modül arasındaki sözleşme kaç addan oluşuyor, dış aracın sözlüğüne kaç dosya
dokunuyor. Bu ölçeklerde birim dosyadır ve sınır bir ithal satırıdır.

Kod tabanı büyüdüğünde birim değişir. Dosyalar tek tek değil, kümeler hâlinde paketlenir,
sürümlenir ve yayınlanır; bu kümeye **bileşen** (component) denir. Yeni soru şudur: hangi
dosyalar aynı bileşene girmelidir? Bu ders yanıtı tahminden değil, kitaplığın değişiklik
günlüğünden çıkarır.

#### Bileşen Yeniden Kullanım Birimidir

Bir bileşenin ilk ölçütü teknik değil, yayın ile ilgilidir: bir kitaplığı kullanan taraf onu
dosya dosya değil, sürüm sürüm alır. Bu yüzden **yeniden kullanım birimi yayın birimine
eşittir** — yeniden kullanım–yayın eşdeğerliği ilkesi (reuse/release equivalence principle)
budur. Ayrı sürümlenemeyen bir küme yeniden kullanılamaz, çünkü kullanan tarafın neye
bağlandığını söyleyecek bir adı yoktur.

Bu ölçüt tek başına sınırı belirlemez, ama iki sonuç doğurur. Birincisi: bir bileşendeki tek
bir dosya değişse bileşenin tamamı yeniden yayınlanır ve bileşene bağlı olan herkes yeni
sürümü karşılamak zorunda kalır. İkincisi: bir bileşene bağlanan taraf, içindeki her dosyaya
bağlanmış olur.

Bu iki sonuç iki ayrı ilkeye dönüşür. **Ortak kapanış ilkesi** (common closure principle)
aynı nedenle aynı zamanda değişen dosyaların aynı bileşende toplanmasını ister; ölçüsü, bir
değişikliğin kaç bileşene dağıldığıdır. **Ortak yeniden kullanım ilkesi** (common reuse
principle) birlikte kullanılmayan dosyaların ayrı bileşenlerde durmasını ister; ölçüsü, bir
yeteneğe ihtiyaç duyan tarafın kaç dosyaya bağlandığıdır.

Bu üçü birlikte bileşenin **uyumunu** belirler. Bağlaşım ve Uyum konusunda uyum bir modülün
içindeki adların birbiriyle ilgililiği olarak ölçülmüştü; buradaki soru aynıdır, yalnız
kapsayan birim modül değil bileşendir ve ilgililiğin kanıtı adlar değil değişim geçmişidir.

#### Kitaplığın Dosyaları ve Değişiklik Günlüğü

Kitaplık on iki dosyaya ulaşmış durumda. Aşağıdaki modül üç şeyi taşıyor: dosya listesi,
dosyalar arasındaki ithal kenarları ve yirmi iki değişikliğin her birinde birlikte dokunulan
dosyalar. Değişiklik günlüğü birinci derste karar düzeyinde tutulmuştu; burada dosya
düzeyindedir.

```js
// kitaplik.mjs — kitapligin dosyalari, ithal kenarlari ve yirmi iki degisiklik
export const DOSYALAR = [
  "ucret/tarife-tablosu.mjs", "ucret/ucret-kurali.mjs", "ucret/indirim.mjs",
  "bolge/posta-eslemesi.mjs", "bolge/katsayi.mjs",
  "tasiyici/secim.mjs", "tasiyici/tasiyici-listesi.mjs",
  "rota/aktarma-noktalari.mjs", "rota/rota-kurma.mjs",
  "teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs", "ortak/para.mjs",
];

export const ITHAL = [
  ["ucret/ucret-kurali.mjs", "ucret/tarife-tablosu.mjs"],
  ["ucret/ucret-kurali.mjs", "bolge/katsayi.mjs"],
  ["ucret/ucret-kurali.mjs", "ortak/para.mjs"],
  ["ucret/indirim.mjs", "ortak/para.mjs"],
  ["ucret/tarife-tablosu.mjs", "ortak/para.mjs"],
  ["bolge/katsayi.mjs", "bolge/posta-eslemesi.mjs"],
  ["tasiyici/secim.mjs", "ucret/ucret-kurali.mjs"],
  ["tasiyici/secim.mjs", "ucret/indirim.mjs"],
  ["tasiyici/secim.mjs", "tasiyici/tasiyici-listesi.mjs"],
  ["rota/rota-kurma.mjs", "rota/aktarma-noktalari.mjs"],
  ["rota/rota-kurma.mjs", "tasiyici/tasiyici-listesi.mjs"],
  ["teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs"],
  ["teslimat/durum-akisi.mjs", "rota/rota-kurma.mjs"],
];

export const GUNLUK = [
  ["ucret/tarife-tablosu.mjs", "bolge/katsayi.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/posta-eslemesi.mjs", "bolge/katsayi.mjs"],
  ["ucret/ucret-kurali.mjs", "ucret/indirim.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/katsayi.mjs"],
  ["rota/aktarma-noktalari.mjs", "rota/rota-kurma.mjs"],
  ["ucret/indirim.mjs", "tasiyici/secim.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/posta-eslemesi.mjs"],
  ["teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs"],
  ["ucret/ucret-kurali.mjs", "ucret/indirim.mjs", "tasiyici/secim.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/katsayi.mjs", "bolge/posta-eslemesi.mjs"],
  ["rota/rota-kurma.mjs", "tasiyici/tasiyici-listesi.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/katsayi.mjs"],
  ["teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs"],
  ["ucret/indirim.mjs", "tasiyici/secim.mjs", "ucret/ucret-kurali.mjs"],
  ["rota/aktarma-noktalari.mjs", "rota/rota-kurma.mjs", "tasiyici/tasiyici-listesi.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/posta-eslemesi.mjs", "bolge/katsayi.mjs"],
  ["teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs"],
  ["ucret/indirim.mjs", "tasiyici/secim.mjs"],
  ["rota/aktarma-noktalari.mjs", "tasiyici/tasiyici-listesi.mjs", "rota/rota-kurma.mjs"],
  ["ucret/tarife-tablosu.mjs", "bolge/katsayi.mjs"],
  ["ucret/ucret-kurali.mjs", "ucret/indirim.mjs"],
  ["ortak/para.mjs", "ucret/ucret-kurali.mjs"],
];
```

Dizin adları bir bölünme öneriyor: `ucret`, `bolge`, `tasiyici`, `rota`, `teslimat`, `ortak`.
Bu bölünme dosyaların ne hakkında olduğuna bakılarak yapılmış. Günlük ise başka bir şey
söylüyor olabilir.

#### Birlikte Değişenin Bulunması

**Birlikte değişim** (co-change), iki dosyanın aynı değişiklikte birlikte dokunulma
sayısıdır. Aşağıdaki araç bütün çiftler için bu sayıyı hesaplar, sonra bir eşiğin üstünde
kalan çiftleri aynı kümeye bağlar. Bağlama işlemi ayrık kümeler üzerinde yapılır: her dosya
kendi kökünde başlar, eşiği aşan her çift iki kökü birleştirir.

```js
// birlikte-degisim.mjs — birlikte degisen dosyalari esikle kumeler
import { DOSYALAR, GUNLUK } from "./kitaplik.mjs";

const ESIK = 3;
const anahtar = (a, b) => [a, b].sort().join(" + ");

const cift = new Map();
for (const dosyalar of GUNLUK) {
  for (let i = 0; i < dosyalar.length; i += 1) {
    for (let j = i + 1; j < dosyalar.length; j += 1) {
      const k = anahtar(dosyalar[i], dosyalar[j]);
      cift.set(k, (cift.get(k) ?? 0) + 1);
    }
  }
}

const kok = new Map(DOSYALAR.map((d) => [d, d]));
const bul = (d) => (kok.get(d) === d ? d : bul(kok.get(d)));
for (const [k, n] of cift) {
  if (n < ESIK) continue;
  const [a, b] = k.split(" + ");
  kok.set(bul(a), bul(b));
}

const kume = new Map();
for (const d of DOSYALAR) {
  const k = bul(d);
  kume.set(k, [...(kume.get(k) ?? []), d]);
}

console.log("birlikte degisim sayisi (esik >= " + ESIK + ")");
for (const [k, n] of [...cift].sort((a, b) => b[1] - a[1])) {
  console.log(`  ${n >= ESIK ? "*" : " "} ${String(n).padStart(2)}  ${k}`);
}
console.log(`\nkume sayisi = ${kume.size}`);
for (const [, uyeler] of kume) console.log(`  ${uyeler.join(", ")}`);
```

```
birlikte degisim sayisi (esik >= 3)
  *  7  bolge/katsayi.mjs + ucret/tarife-tablosu.mjs
  *  4  bolge/posta-eslemesi.mjs + ucret/tarife-tablosu.mjs
  *  4  ucret/indirim.mjs + ucret/ucret-kurali.mjs
  *  4  tasiyici/secim.mjs + ucret/indirim.mjs
  *  3  bolge/katsayi.mjs + bolge/posta-eslemesi.mjs
  *  3  rota/aktarma-noktalari.mjs + rota/rota-kurma.mjs
  *  3  teslimat/durum-akisi.mjs + teslimat/zaman-damgasi.mjs
  *  3  rota/rota-kurma.mjs + tasiyici/tasiyici-listesi.mjs
     2  tasiyici/secim.mjs + ucret/ucret-kurali.mjs
     2  rota/aktarma-noktalari.mjs + tasiyici/tasiyici-listesi.mjs
     1  ortak/para.mjs + ucret/ucret-kurali.mjs

kume sayisi = 5
  ucret/tarife-tablosu.mjs, bolge/posta-eslemesi.mjs, bolge/katsayi.mjs
  ucret/ucret-kurali.mjs, ucret/indirim.mjs, tasiyici/secim.mjs
  tasiyici/tasiyici-listesi.mjs, rota/aktarma-noktalari.mjs, rota/rota-kurma.mjs
  teslimat/durum-akisi.mjs, teslimat/zaman-damgasi.mjs
  ortak/para.mjs
```

Beş kümeden üçü dizin sınırlarını kesiyor. En yüksek sayı olan yedi, ücret tablosu ile bölge
katsayısı arasındadır: fiyat listesi yenilendiğinde ikisi birlikte yenileniyor, oysa ayrı
dizinlerde duruyorlar. İndirim ile taşıyıcı seçimi de dört kez birlikte değişmiş; indirim
oranı taşıyıcı sıralamasını etkilediği için ikisi tek bir ticari karardır.

Eşiğin üçte olması bir karardır, veri değil. İki dosyanın bir kez birlikte değişmesi tesadüf
olabilir; üç kez birlikte değişmesi bir nedene işaret eder. Eşik yükseltilirse küme sayısı
artar, düşürülürse kümeler birbirine yapışır.

#### Üç Bölünmenin Ölçülmesi

Şimdi üç bölünme aynı ölçülerle karşılaştırılıyor: dizinlere göre bölünme, birlikte değişime
göre bölünme ve her şeyi tek bileşende toplayan bölünme. Ölçüler dörttür — bileşen sayısı,
bileşenler arası bağ sayısı, bir değişikliğin dağıldığı bileşen sayısı ve rota kurma
yeteneğine ihtiyaç duyan bir tüketicinin bağlanmak zorunda kaldığı dosya sayısı.

Son ölçü, bileşen çizgesinde rota bileşeninden çıkan kenarların ardışık izlenmesiyle bulunur:
bir bileşene bağlanan taraf, o bileşenin bağlandığı bileşenlere de bağlanmış olur.

```js
// bilesen-olc.mjs — uc bolunmeyi ayni uc olcuyle karsilastirir
import { DOSYALAR, ITHAL, GUNLUK } from "./kitaplik.mjs";

const dizinBolunmesi = Object.fromEntries(DOSYALAR.map((d) => [d, d.split("/")[0]]));
const KUMELER = [
  ["tarife", ["ucret/tarife-tablosu.mjs", "bolge/posta-eslemesi.mjs", "bolge/katsayi.mjs"]],
  ["ticari-kural", ["ucret/ucret-kurali.mjs", "ucret/indirim.mjs", "tasiyici/secim.mjs"]],
  ["ag", ["tasiyici/tasiyici-listesi.mjs", "rota/aktarma-noktalari.mjs", "rota/rota-kurma.mjs"]],
  ["izleme", ["teslimat/durum-akisi.mjs", "teslimat/zaman-damgasi.mjs"]],
  ["para", ["ortak/para.mjs"]],
];
const birlikteBolunmesi = Object.fromEntries(
  KUMELER.flatMap(([ad, uyeler]) => uyeler.map((d) => [d, ad])));
const tekBolunme = Object.fromEntries(DOSYALAR.map((d) => [d, "kitaplik"]));

function kapanis(bolunme, baslangic) {
  const kenar = new Set(ITHAL
    .filter(([a, b]) => bolunme[a] !== bolunme[b])
    .map(([a, b]) => `${bolunme[a]} -> ${bolunme[b]}`));
  const gorulen = new Set([bolunme[baslangic]]);
  for (const b of gorulen) {
    for (const k of kenar) {
      const [sol, sag] = k.split(" -> ");
      if (sol === b) gorulen.add(sag);
    }
  }
  return { kenarSayisi: kenar.size, dosyaSayisi: DOSYALAR.filter((d) => gorulen.has(bolunme[d])).length };
}

for (const [ad, bolunme] of [["dizin", dizinBolunmesi], ["birlikte", birlikteBolunmesi], ["tek", tekBolunme]]) {
  const bilesen = new Set(Object.values(bolunme));
  const dokunulan = GUNLUK.map((d) => new Set(d.map((f) => bolunme[f])).size);
  const yayin = dokunulan.reduce((a, b) => a + b, 0);
  const dagilan = dokunulan.filter((n) => n > 1).length;
  const { kenarSayisi, dosyaSayisi } = kapanis(bolunme, "rota/rota-kurma.mjs");
  console.log(`${ad.padEnd(9)} bilesen ${bilesen.size}  bilesenler arasi bag ${kenarSayisi}`);
  console.log(`          birden cok bilesene dokunan degisiklik ${dagilan} / ${GUNLUK.length}`);
  console.log(`          toplam yeniden yayin ${yayin}  ortalama ${(yayin / GUNLUK.length).toFixed(2)}`);
  console.log(`          rota kurmak icin baglanilan dosya ${dosyaSayisi} / ${DOSYALAR.length}`);
}
```

```
dizin     bilesen 6  bilesenler arasi bag 5
          birden cok bilesene dokunan degisiklik 16 / 22
          toplam yeniden yayin 38  ortalama 1.73
          rota kurmak icin baglanilan dosya 10 / 12
birlikte  bilesen 5  bilesenler arasi bag 5
          birden cok bilesene dokunan degisiklik 1 / 22
          toplam yeniden yayin 23  ortalama 1.05
          rota kurmak icin baglanilan dosya 3 / 12
tek       bilesen 1  bilesenler arasi bag 0
          birden cok bilesene dokunan degisiklik 0 / 22
          toplam yeniden yayin 22  ortalama 1.00
          rota kurmak icin baglanilan dosya 12 / 12
```

Dizin bölünmesinde yirmi iki değişikliğin on altısı birden çok bileşene dağılıyor; birlikte
değişim bölünmesinde bir tanesi. Toplam yeniden yayın sayısı 38'den 23'e iniyor. İki
bölünmenin bileşen sayıları ve bileşenler arası bağ sayıları neredeyse aynı — kazanç bileşen
saymaktan değil, sınırın nereden geçtiğinden geliyor.

Rota kurma ölçüsü daha keskin. Dizin bölünmesinde rota bileşeni taşıyıcı bileşenine
bağlanıyor, taşıyıcı bileşeni içindeki seçim dosyası ücrete, ücret bölgeye ve paraya; sonuçta
yalnız rota kurmak isteyen bir tüketici on iki dosyanın onuna bağlanmış oluyor. Birlikte
değişim bölünmesinde taşıyıcı listesi rota dosyalarıyla aynı bileşende olduğu için bu zincir
kuruluyor bile: üç dosya yetiyor.

#### Üç İlkenin Gerilimi

Üçüncü satır ilkelerin birbirini ittiğini gösteriyor. Tek bileşenli bölünme ortak kapanış
ölçüsünde kusursuzdur — hiçbir değişiklik birden çok bileşene dağılmaz, çünkü tek bileşen
vardır. Aynı bölünme ortak yeniden kullanım ölçüsünde en kötü sonucu verir: bir tek yetenek
için on iki dosyanın hepsine bağlanılır.

Gerilim şu üç yönlüdür. Ortak kapanış bileşenleri **büyütmek** ister, çünkü birlikte değişen
her şey içeride olsun. Ortak yeniden kullanım **küçültmek** ister, çünkü kullanılmayan hiçbir
şey içeride olmasın. Yeniden kullanım–yayın eşdeğerliği ikisine de bir taban koyar: bölünme
ne olursa olsun, ayrı yayınlanamayacak kadar iç içe geçmiş bir küme bileşen sayılmaz.

Bu üçü aynı anda enbüyütülemez. Hangisinin ağır bastığı kitaplığın hangi aşamada olduğuna
bağlıdır: bileşen dışarıya henüz yayınlanmıyorsa geliştirmeyi zorlaştıran şey dağılan
değişikliklerdir ve ortak kapanış öne çıkar; bileşen çok sayıda tüketiciye yayınlanıyorsa
gereksiz bağımlılık pahalıdır ve ortak yeniden kullanım öne çıkar.

#### Ölçünün Sınırı

Birlikte değişim bir işaret verir, karar vermez. Üç durumda yanıltır.

Birincisi, bütün dosyalara dokunan değişikliklerdir — toplu bir yeniden adlandırma ya da
biçim düzeltmesi bütün çiftlerin sayısını birden yükseltir ve kümeleri birbirine yapıştırır.
Böyle değişiklikler günlükten ayıklanmadan ölçüm anlamlı olmaz.

İkincisi, geçmişi az olan dosyalardır: bir dosyanın hiçbir çifti eşiği aşmıyorsa tek başına
bir kümede kalır. Yukarıdaki çıktıda `ortak/para.mjs` bu durumdadır — tek bir kez ücret
kuralıyla birlikte değişmiş ve kendi kümesinde kalmıştır. Sonuç bu örnekte doğrudur, çünkü
para birimi işlemleri gerçekten ayrı bir nedenle değişir; ama aynı biçim yeni eklenmiş her
dosya için de çıkar ve orada bir bilgi taşımaz.

Üçüncüsü, günlüğün geleceği değil geçmişi anlatmasıdır. Kitaplığın yeni bir alana açılması
kümeleri yeniden çizer. Bileşen sınırı bu yüzden bir kez çizilip bırakılan bir şey değil,
günlük büyüdükçe yeniden hesaplanan bir şeydir.

#### Özet

- Bileşen, birlikte paketlenen ve birlikte yayınlanan dosya kümesidir; yeniden kullanım
  birimi yayın birimine eşittir.
- Ortak kapanış ilkesi birlikte değişenleri aynı bileşende toplar, ortak yeniden kullanım
  ilkesi birlikte kullanılmayanları ayırır; ikisi zıt yönde çeker.
- Yirmi iki değişikliğin birlikte değişim sayıları hesaplandığında beş küme çıktı ve üçü
  dizin sınırlarını kesti; en yüksek sayı ücret tablosu ile bölge katsayısı arasındaydı.
- Dizin bölünmesinde 22 değişikliğin 16'sı birden çok bileşene dağılıyordu, birlikte değişim
  bölünmesinde 1'i; toplam yeniden yayın 38'den 23'e indi.
- Rota kurma yeteneği için bağlanılan dosya sayısı 10'dan 3'e indi; her şeyi tek bileşende
  toplayan bölünmede aynı sayı 12 oldu ve iki ilkenin gerilimi görünür hâle geldi.

#### Sonraki Adım

Bu dersin ölçüleri bileşenlerin **içine** bakıyordu: hangi dosyalar birlikte durmalı. Bileşen
çizgesinin kenarları ise sayıldı ama yönü sorgulanmadı. Bir kenarın hangi yöne bakması
gerektiğinin bir ölçütü var mıdır? Sık değişen bir bileşene bağlanan kararlı bir bileşen,
kendi kararlılığını kaybeder. Sonraki ders her bileşen için gelen ve giden bağ sayısından bir
kararlılık ölçüsü hesaplar, bağların kararlı yöne akıp akmadığını sayar ve aynı çizgede
bileşenler arası döngünün neden yayın sırasını olanaksız kıldığını gösterir.

### Bileşen Bağlaşım İlkeleri

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/bilesen-baglasim-ilkeleri

Bileşenler arası bağın yönünün ölçülmesi: gelen ve giden bağ sayısından kararsızlık ölçüsünün hesaplanması, kararlı yöne akmayan kenarların sayılması, topolojik sıralamayla yayın sırasının çıkarılması ve kararlı çekirdeğe eklenen tek bir ithalin sıralamayı nasıl olanaksız kıldığının gösterilmesi.

Bir önceki ders bileşen sınırını birlikte değişen dosyalardan çıkardı ve iki bölünmeyi
yeniden yayın sayısıyla karşılaştırdı. O ölçüler bileşenlerin **içine** bakıyordu: hangi
dosyalar birlikte durmalı. Bileşenler arası kenarlar sayıldı, ama yönleri sorgulanmadı.

Bileşenler arası kenar, Bağlaşım ve Uyum konusunda modül çiftleri için ölçülen bağlaşımın
bileşen ölçeğindeki karşılığıdır. Aradaki fark yöndür: orada bağın gücü sayılıyordu, burada
bağın hangi yöne baktığı sayılacak. Bir kenarın hangi yöne bakması gerektiğinin bir ölçütü
vardır ve ölçüt yine değişimle ilgilidir. Sık değişen bir bileşene bağlanan bir bileşen, kendisi hiç değişmese bile o
değişikliklerin peşinden sürüklenir. Bu ders her bileşen için bir kararlılık ölçüsü
hesaplar, bağların doğru yöne akıp akmadığını sayar ve kararlılığın karşılığını yayın
sırasında gösterir.

#### Kararlılık Ölçülebilir Bir Şeydir

Bir bileşenin kararlılığı, değişmemesi değil, **değişmesinin zor olmasıdır**. İki sayı bunu
belirler: kaç bileşen ona bağlı (gelen bağ sayısı) ve o kaç bileşene bağlı (giden bağ
sayısı). Gelen bağı çok olan bir bileşen kolay değiştirilemez, çünkü değişimi başkalarını
kırar. Giden bağı çok olan bir bileşen kolay değiştirilir, çünkü değişimi kimseyi kırmaz —
ama aynı bileşen başkalarının değişiminden etkilenmeye açıktır.

İkisi tek bir orana indirgenir. **Kararsızlık ölçüsü**, giden bağ sayısının toplam bağ
sayısına oranıdır:

$$I = \frac{\text{giden}}{\text{gelen} + \text{giden}}$$

$I = 0$ olan bileşen en kararlıdır: ona bağlanan vardır, o kimseye bağlanmaz. $I = 1$ olan
bileşen en kararsızdır: hiçbir şey ona bağlı değildir, kendisi başkalarına bağlıdır. Ölçü
kalite değil konum bildirir; bir kitaplıkta her iki uçtan da bileşenlere ihtiyaç vardır.

**Kararlı bağımlılıklar ilkesi** (stable dependencies principle) bu ölçünün üzerine kurulur:
bir kenar, $I$'nin **düştüğü** yöne akmalıdır. Kararlı bir bileşenin kararsız bir bileşene
bağlanması, kararlılığını bir kurgu hâline getirir — bileşen değişmeye kapalı görünür ama
bağlandığı taraf her değiştiğinde yeniden yayınlanır.

#### Ölçülecek Üç Çizge

Bir önceki dersteki iki bölünme aynı kitaplığın iki bileşen çizgesini verir. Üçüncü çizge,
ikincisine tek bir ithal eklenerek üretiliyor: para bileşeni tutarları bölgeye göre
biçimlendirmeye başlıyor ve tarife bileşenini ithal ediyor.

```js
// bilesen-cizgesi.mjs — ayni kitapligin uc bilesen cizgesi
export const DIZIN = {
  bilesenler: ["ucret", "bolge", "tasiyici", "rota", "teslimat", "ortak"],
  kenarlar: [["ucret", "bolge"], ["ucret", "ortak"], ["tasiyici", "ucret"],
    ["rota", "tasiyici"], ["teslimat", "rota"]],
};

export const BIRLIKTE = {
  bilesenler: ["tarife", "ticari-kural", "ag", "izleme", "para"],
  kenarlar: [["ticari-kural", "tarife"], ["ticari-kural", "para"], ["ticari-kural", "ag"],
    ["tarife", "para"], ["izleme", "ag"]],
};

export const BOZUK = {
  bilesenler: BIRLIKTE.bilesenler,
  kenarlar: [...BIRLIKTE.kenarlar, ["para", "tarife"]],
};
```

Ölçüm aracı iki şey hesaplar. Birincisi kararsızlık ölçüsü ve kararlı yöne akmayan kenarlar.
İkincisi **yayın sırası**: bir bileşen, bağlandığı bileşenlerin hepsi yayınlandıktan sonra
yayınlanabilir. Bu, Veri Yapıları kursunda kurulan topolojik sıralamanın bileşenler
üzerindeki karşılığıdır; sıralanamayan bileşen kalırsa çizgede döngü var demektir.

```js
// kararlilik.mjs — kararsizlik olcusu, kararli yon ihlali ve yayin sirasi
export function olcu(bilesenler, kenarlar) {
  const gelen = Object.fromEntries(bilesenler.map((b) => [b, 0]));
  const giden = Object.fromEntries(bilesenler.map((b) => [b, 0]));
  for (const [a, b] of kenarlar) { giden[a] += 1; gelen[b] += 1; }
  const I = Object.fromEntries(bilesenler.map((b) => {
    const toplam = gelen[b] + giden[b];
    return [b, toplam === 0 ? 0 : giden[b] / toplam];
  }));
  return { gelen, giden, I, ihlal: kenarlar.filter(([a, b]) => I[a] < I[b]) };
}

export function yayinSirasi(bilesenler, kenarlar) {
  const kalan = new Set(bilesenler);
  const sira = [];
  for (;;) {
    const hazir = [...kalan]
      .filter((b) => kenarlar.every(([a, h]) => a !== b || !kalan.has(h))).sort();
    if (hazir.length === 0) return { sira, kalan: [...kalan].sort() };
    for (const b of hazir) { sira.push(b); kalan.delete(b); }
    if (kalan.size === 0) return { sira, kalan: [] };
  }
}
```

```js
// kararlilik-raporu.mjs — uc cizgeyi ayni olculerle raporlar
import { DIZIN, BIRLIKTE, BOZUK } from "./bilesen-cizgesi.mjs";
import { olcu, yayinSirasi } from "./kararlilik.mjs";

for (const [ad, cizge] of [["dizin", DIZIN], ["birlikte", BIRLIKTE], ["bozuk", BOZUK]]) {
  const { gelen, giden, I, ihlal } = olcu(cizge.bilesenler, cizge.kenarlar);
  console.log(`${ad} cizgesi — ${cizge.bilesenler.length} bilesen, ${cizge.kenarlar.length} kenar`);
  for (const b of [...cizge.bilesenler].sort((x, y) => I[x] - I[y] || x.localeCompare(y))) {
    console.log(`  ${b.padEnd(13)} gelen ${gelen[b]}  giden ${giden[b]}  I = ${I[b].toFixed(2)}`);
  }
  console.log(`  kararli yon ihlali = ${ihlal.length} / ${cizge.kenarlar.length}` +
    (ihlal.length ? `  ${ihlal.map(([a, b]) => `${a} -> ${b}`).join(", ")}` : ""));
  const { sira, kalan } = yayinSirasi(cizge.bilesenler, cizge.kenarlar);
  console.log(`  yayin sirasi: ${sira.join(" -> ")}` +
    (kalan.length ? `  || siralanamayan: ${kalan.join(", ")}` : ""));
}
```

```
dizin cizgesi — 6 bilesen, 5 kenar
  bolge         gelen 1  giden 0  I = 0.00
  ortak         gelen 1  giden 0  I = 0.00
  rota          gelen 1  giden 1  I = 0.50
  tasiyici      gelen 1  giden 1  I = 0.50
  ucret         gelen 1  giden 2  I = 0.67
  teslimat      gelen 0  giden 1  I = 1.00
  kararli yon ihlali = 1 / 5  tasiyici -> ucret
  yayin sirasi: bolge -> ortak -> ucret -> tasiyici -> rota -> teslimat
birlikte cizgesi — 5 bilesen, 5 kenar
  ag            gelen 2  giden 0  I = 0.00
  para          gelen 2  giden 0  I = 0.00
  tarife        gelen 1  giden 1  I = 0.50
  izleme        gelen 0  giden 1  I = 1.00
  ticari-kural  gelen 0  giden 3  I = 1.00
  kararli yon ihlali = 0 / 5
  yayin sirasi: ag -> para -> izleme -> tarife -> ticari-kural
bozuk cizgesi — 5 bilesen, 6 kenar
  ag            gelen 2  giden 0  I = 0.00
  para          gelen 2  giden 1  I = 0.33
  tarife        gelen 2  giden 1  I = 0.33
  izleme        gelen 0  giden 1  I = 1.00
  ticari-kural  gelen 0  giden 3  I = 1.00
  kararli yon ihlali = 0 / 6
  yayin sirasi: ag -> izleme  || siralanamayan: para, tarife, ticari-kural
```

#### Yönün Yanlış Olduğu Yer

Dizin çizgesinde beş kenarın biri kararlı yöne akmıyor: `tasiyici -> ucret`. Taşıyıcı
bileşeninin kararsızlığı 0.50, ücret bileşeninin 0.67. Bağ, daha kararsız olana doğru
gidiyor.

Bunun somut anlamı şudur: rota bileşeni taşıyıcıya, taşıyıcı da ücrete bağlı olduğu için,
ücret bileşenine gelen her ticari kural değişikliği rota bileşenine kadar yürür. Ücret
bileşeni bir önceki dersin günlüğünde en sık dokunulan yerdi; en sık değişen bileşen, zincirin
ortasına yerleşmiş durumda.

Birlikte değişim çizgesinde aynı sayı sıfır. Beş kenarın beşi de kararsızdan kararlıya
akıyor: en kararsız iki bileşen (`ticari-kural` ve `izleme`) hiçbir bileşene girdi vermiyor,
en kararlı iki bileşen (`ag` ve `para`) hiçbir şeye bağlanmıyor. Ölçü, bir önceki dersin
bölünmesini bağımsız bir ölçütle daha doğruluyor.

#### Kararlılığın Karşılığı

Kararlı yön yalnız bir estetik tercih olsaydı ölçmeye değmezdi. Karşılığı yayın sırasında
görünür: birlikte çizgesinde beş bileşen `ag -> para -> izleme -> tarife -> ticari-kural`
sırasıyla yayınlanabilir. Bu sıra, her bileşenin bağlandığı her şeyin ondan önce
yayınlandığını garanti eder. Bir bileşenin yeni sürümü çıktığında hangi bileşenlerin yeniden
sınanacağı da aynı sıradan okunur.

Sıranın var olabilmesinin koşulu **döngüsüz bağımlılıklar ilkesidir** (acyclic dependencies
principle): bileşen çizgesinde döngü bulunmamalıdır. Döngü varsa yayın sırası yoktur, çünkü
döngüdeki her bileşen bir diğerinin önce yayınlanmasını bekler.

#### Kararlı Çekirdeğe Tek Bir İthal

Bozuk çizge tam olarak bunu gösteriyor. Eklenen tek kenar `para -> tarife`. Sonuçlar:

Para bileşeninin kararsızlığı 0.00'dan 0.33'e çıktı. Kitaplığın en kararlı parçası artık
tarife tablosuna bağlı; fiyat listesi her yenilendiğinde para bileşeni de yeniden
yayınlanacak. Tarife bileşeni de simetrik olarak 0.50'den 0.33'e indi — sayı düştüğü hâlde
durum iyileşmedi, çünkü iki bileşen artık birbirinin içine geçmiş durumda.

Kararlı yön ihlali **sıfır** çıktı. İki bileşenin kararsızlığı eşit olduğu için hiçbir kenar
"kararsızdan kararlıya akmıyor" ölçütüne takılmıyor. Bu, kararsızlık ölçüsünün tek başına
yetmediğinin kanıtıdır: döngü, ölçüyü ihlal etmez, ölçüyü **anlamsızlaştırır**.

Yayın sırası ise durumu saklamıyor. Beş bileşenin yalnız ikisi sıralanabildi; `para`,
`tarife` ve `ticari-kural` sıralanamayan olarak kaldı. `ticari-kural` döngünün içinde
olmadığı hâlde listede: döngüye bağlanan her bileşen de sıralanamaz hâle gelir. Bir bileşenin
tek bir ithal satırı, ona uzaktan bağlı üçüncü bileşeni yayınlanamaz yapıyor.

#### Döngünün Kırılması

Döngüyü kaldırmanın iki bilinen yolu vardır ve ikisi de daha önce kurulmuş hareketlerdir.

Birincisi yönü çevirmektir: para bileşeni tarifeyi ithal etmek yerine, biçimlendirme için
ihtiyaç duyduğu bilgiyi parametre olarak alır. Bu, birinci dersteki hareketin bileşen
ölçeğindeki karşılığıdır — kural tabloyu ithal etmez, tablo kurala verilir. SOLID'in
bağımlılığın ters çevrilmesi maddesi ile M16/K04'teki katman kuralı aynı hareketi tarif eder.

İkincisi araya bir bileşen koymaktır: iki bileşenin ortak ihtiyacı üçüncü bir bileşene
ayrılır ve ikisi de ona bağlanır. Döngü, iki kenarın da aynı yöne bakmasıyla kırılır.

Birinci yol uygulandığında çizge yeniden birlikte değişim çizgesi olur: `para` yeniden
$I = 0$ değerine döner, kenar sayısı altıdan beşe iner ve beş bileşenin tamamı sıralanır.
Ölçülerin üçü de aynı anda düzelir.

#### Özet

- Bir bileşenin kararsızlığı $I = \text{giden} / (\text{gelen} + \text{giden})$ ile
  hesaplanır; 0 en kararlı, 1 en kararsız konumu gösterir.
- Kararlı bağımlılıklar ilkesi kenarların $I$'nin düştüğü yöne akmasını ister; dizin
  bölünmesinde 5 kenarın 1'i bu yöne akmıyordu, birlikte değişim bölünmesinde 0'ı.
- Kararlılığın karşılığı yayın sırasıdır: birlikte çizgesinde beş bileşen tek bir sırayla
  yayınlanabiliyor ve bir sürüm çıktığında yeniden sınanacaklar aynı sıradan okunuyor.
- Kararlı çekirdeğe eklenen tek bir ithal (`para -> tarife`) para bileşeninin kararsızlığını
  0.00'dan 0.33'e çıkardı ve beş bileşenin üçünü sıralanamaz yaptı.
- Aynı bozuk çizgede kararlı yön ihlali 0 çıktı; kararsızlık ölçüsü döngüyü yakalamaz, döngü
  ölçünün kendisini anlamsızlaştırır.

#### Sonraki Adım

Yayın sırası döngünün **var olduğunu** söyledi ama nerede olduğunu söylemedi: sıralanamayan
üç bileşenin hangisi döngünün içinde, hangisi ona bağlı olduğu için listede, çıktıdan
okunamıyor. Ayrıca bu bölümün bütün ölçüleri elle yazılmış bir bileşen çizgesi üzerindeydi;
gerçek bir kod tabanında çizge ithal satırlarından çıkarılır. Sonraki ders ithal grafiğini
kaynak dosyalardan okuyan, döngüleri derine aramayla adlarıyla listeleyen ve katman kuralına
aykırı ithalleri sayan bir araç yazar; aracı önce bozuk bir sürümde koşturur, sonra
düzeltilmiş sürümde iki sayının da sıfıra indiğini gösterir.

### Bağımlılık Çizgesi Sağlığı

Source: https://academia.sh/tr/kurslar/tasarim-ilkeleri/bagimlilik-cizgesi-sagligi

İthal grafiğinin kaynak dosyalardan çıkarılması ve bileşen düzeyinde denetlenmesi: derine aramayla döngülerin adlarıyla listelenmesi, katman sıralarına aykırı kenarların sayılması, döngünün çalışma zamanındaki karşılığının gösterilmesi ve onarımdan sonra iki sayının da sıfıra inmesi.

Bir önceki ders yayın sırasının bozulmasından döngünün **var olduğunu** okudu ama yerini
söyleyemedi: sıralanamayan üç bileşenin hangisi döngünün içinde, hangisi ona bağlı olduğu
için listede kaldı, çıktıdan anlaşılmıyordu. Ayrıca oradaki bütün ölçüler elle yazılmış bir
bileşen çizgesi üzerindeydi.

Gerçek bir kod tabanında çizge elle yazılmaz, ithal satırlarından çıkarılır. Bu ders o
çıkarımı yapan, döngüleri adlarıyla listeleyen ve katman sırasına aykırı kenarları sayan bir
araç yazar; aracı önce bozuk bir sürümde koşturur, sonra onarılmış sürümde iki sayının da
sıfıra indiğini gösterir.

#### Denetlenecek Kaynak Ağacı

İthal grafiğinin dosya düzeyinde çıkarılması ve katman kuralının denetlenmesi Veri Erişim
Katmanı ve İş Mantığı kursundaki Katman Sorumlulukları dersinde kurulmuştu. Buradaki iş
farklıdır: aynı grafik **bileşen düzeyine** indirgenir ve üzerinde döngü aranır. Katman
denetimi de bileşen sıralarına göre yapılır, dosya yollarına göre değil.

Aşağıdaki blok kitaplığın bozuk sürümünü kurar. Dokuz dosya, beş bileşen ve dört katman
sırası var: `para` en altta (0), `tarife` ile `ag` bir üstte (1), `ticari-kural` onların
üstünde (2), giriş dosyası en üstte (3).

```sh
mkdir -p bozuk
cat > bozuk/para.mjs <<'EOF'
import { KADEMELER } from "./tarife-tablosu.mjs";
export const kurus = (tl) => Math.round(tl * 100);
export const ENUST_AGIRLIK = KADEMELER.at(-1).ustAgirlik;
EOF
cat > bozuk/tarife-tablosu.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const KADEMELER = [{ ustAgirlik: 5, ucret: kurus(84.9) }, { ustAgirlik: 30, ucret: kurus(249.9) }];
EOF
cat > bozuk/katsayi.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const KATSAYI = { yakin: 1, orta: 1.35, uzak: 1.8 };
export const ASGARI = kurus(39.9);
EOF
cat > bozuk/ucret-kurali.mjs <<'EOF'
import { KADEMELER } from "./tarife-tablosu.mjs";
import { KATSAYI, ASGARI } from "./katsayi.mjs";
export const ucret = (agirlik, bolge) =>
  Math.max((KADEMELER.find((k) => agirlik <= k.ustAgirlik)?.ucret ?? 0) * KATSAYI[bolge], ASGARI);
EOF
cat > bozuk/indirim.mjs <<'EOF'
import { kurus } from "./para.mjs";
export const indirimli = (tutar, oran) => Math.max(Math.round(tutar * (1 - oran)), kurus(39.9));
EOF
cat > bozuk/secim.mjs <<'EOF'
import { ucret } from "./ucret-kurali.mjs";
import { indirimli } from "./indirim.mjs";
import { TASIYICILAR } from "./tasiyici-listesi.mjs";
export const enUcuz = (agirlik, bolge, oran) => TASIYICILAR
  .map((t) => ({ ad: t.ad, tutar: indirimli(ucret(agirlik, bolge) * t.carpan, oran) }))
  .sort((a, b) => a.tutar - b.tutar)[0];
EOF
cat > bozuk/tasiyici-listesi.mjs <<'EOF'
import { enUcuz } from "./secim.mjs";
export const TASIYICILAR = [{ ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 }];
export const varsayilan = (agirlik) => enUcuz(agirlik, "orta", 0).ad;
EOF
cat > bozuk/rota-kurma.mjs <<'EOF'
import { TASIYICILAR } from "./tasiyici-listesi.mjs";
export const rota = (bolge) => [bolge, TASIYICILAR[0].ad];
EOF
cat > bozuk/dene.mjs <<'EOF'
import { enUcuz } from "./secim.mjs";
console.log("enUcuz(3, 'orta', 0) =", JSON.stringify(enUcuz(3, "orta", 0)));
EOF
ls bozuk | tr '\n' ' '
```

```
dene.mjs indirim.mjs katsayi.mjs para.mjs rota-kurma.mjs secim.mjs tarife-tablosu.mjs tasiyici-listesi.mjs ucret-kurali.mjs 
```

İki ithal satırı fazladan konmuş durumda: `para.mjs` tarife tablosunu ithal ediyor ve
`tasiyici-listesi.mjs` seçim modülünü ithal ediyor. İkisi de tek başına bakıldığında masum
görünür — para modülü en üst ağırlık kademesini bilmek istiyor, taşıyıcı listesi varsayılan
taşıyıcıyı hesaplamak istiyor.

#### Döngünün Çalışma Zamanındaki Karşılığı

Bu iki satırın bedeli çizge üzerinde kalmıyor. Giriş dosyası çalıştırıldığında modüller
birbirini bekliyor.

```sh
node bozuk/dene.mjs 2>&1 | grep -m1 "^ReferenceError"
```

```
ReferenceError: Cannot access 'KADEMELER' before initialization
```

Hata, döngüdeki modüllerden birinin gövdesi çalışırken diğerinin dışa açtığı sabitin henüz
tanımlanmamış olmasından çıkıyor. Hangi modülün önce değerlendirileceği giriş noktasına bağlı
olduğu için, aynı döngü başka bir giriş noktasında başka bir adla ya da hiç hata vermeden
geçebilir. Döngünün en zor tarafı budur: belirtisi kararlı değildir.

#### Döngü Tespiti ve Katman Denetimi

Araç üç adım yapar. Önce her dosyanın ithal satırlarını okuyup bileşen kenarlarını çıkarır;
aynı bileşen içindeki ithaller elenir. Sonra bu kenarlar üzerinde **derine arama** yapar —
Veri Yapıları kursundaki Derine Arama dersinde kurulan üç renkli düzen: gri düğüm hâlen
işlenmektedir, siyah düğüm bitmiştir, gri bir düğüme geri gelinmesi döngüdür. Son olarak her
kenarın katman sıralarını karşılaştırır.

```js
// cizge-sagligi.mjs — ithal grafiginden bilesen dongusunu ve katman ihlalini cikarir
import { readdirSync, readFileSync } from "node:fs";

const BILESEN = {
  "para.mjs": "para", "tarife-tablosu.mjs": "tarife", "katsayi.mjs": "tarife",
  "ucret-kurali.mjs": "ticari-kural", "indirim.mjs": "ticari-kural", "secim.mjs": "ticari-kural",
  "tasiyici-listesi.mjs": "ag", "rota-kurma.mjs": "ag", "dene.mjs": "giris",
};
const KATMAN = { para: 0, tarife: 1, ag: 1, "ticari-kural": 2, giris: 3 };

function bilesenKenarlari(dizin) {
  const kenar = new Set();
  for (const dosya of readdirSync(dizin).filter((d) => d.endsWith(".mjs")).sort()) {
    const metin = readFileSync(`${dizin}/${dosya}`, "utf8");
    for (const m of metin.matchAll(/^import\s.*?from\s+"\.\/([\w.-]+)"/gm)) {
      const [a, b] = [BILESEN[dosya], BILESEN[m[1]]];
      if (a !== b) kenar.add(`${a} ${b}`);
    }
  }
  return [...kenar].map((k) => k.split(" "));
}

function dongular(kenarlar) {
  const komsu = new Map();
  for (const [a, b] of kenarlar) komsu.set(a, [...(komsu.get(a) ?? []), b]);
  const durum = new Map(), yol = [], bulunan = [];
  const derineAra = (d) => {
    durum.set(d, "gri"); yol.push(d);
    for (const k of komsu.get(d) ?? []) {
      if (durum.get(k) === "gri") bulunan.push([...yol.slice(yol.indexOf(k)), k]);
      else if (durum.get(k) !== "siyah") derineAra(k);
    }
    yol.pop(); durum.set(d, "siyah");
  };
  for (const d of [...new Set(kenarlar.flat())].sort()) if (!durum.has(d)) derineAra(d);
  return bulunan;
}

for (const dizin of process.argv.slice(2)) {
  const kenarlar = bilesenKenarlari(dizin);
  const dongu = dongular(kenarlar);
  const ihlal = kenarlar.filter(([a, b]) => KATMAN[a] < KATMAN[b]);
  console.log(`${dizin}/  bilesen kenari = ${kenarlar.length}`);
  console.log(`  dongu = ${dongu.length}`);
  for (const d of dongu) console.log(`    ${d.join(" -> ")}`);
  console.log(`  katman ihlali = ${ihlal.length}`);
  for (const [a, b] of ihlal) console.log(`    ${a}(${KATMAN[a]}) -> ${b}(${KATMAN[b]})`);
}
```

```sh
node cizge-sagligi.mjs bozuk
```

```
bozuk/  bilesen kenari = 7
  dongu = 2
    para -> tarife -> para
    ag -> ticari-kural -> ag
  katman ihlali = 2
    para(0) -> tarife(1)
    ag(1) -> ticari-kural(2)
```

Bir önceki dersin yayın sırası üç bileşeni "sıralanamayan" diye vermişti; buradaki çıktı
döngünün içindeki bileşenleri ayrı ayrı adlandırıyor ve `ticari-kural`'ın döngüde değil,
döngüye bağlı olduğunu ortaya koyuyor.

İki sayı aynı iki kenarı işaret ediyor ama aynı şeyi ölçmüyor. Katman ihlali bir **kural**
ihlalidir: sıralar önceden ilan edilmiştir, kenar onlara uymamıştır. Döngü ise kuraldan
bağımsızdır; hiçbir katman ilan edilmemiş olsa da döngü yine bir kusurdur, çünkü yayın sırası
ve değerlendirme sırası ondan bağımsız olarak bozulur. Katman kuralı olmayan bir kitaplıkta
ikinci sayı hesaplanamaz, birincisi hesaplanır.

#### Onarım

İki ithal iki farklı hareketle kaldırılıyor. `para.mjs` en üst ağırlık kademesini ithal
etmek yerine kademeleri parametre olarak alıyor — birinci dersteki hareketin aynısı, kural
tabloyu ithal etmez. `tasiyici-listesi.mjs` içindeki varsayılan taşıyıcı hesabı ise seçim
modülüne taşınıyor; hesap zaten seçime bağlıydı, listeye değil.

```sh
cp -r bozuk duzeltilmis
cat > duzeltilmis/para.mjs <<'EOF'
export const kurus = (tl) => Math.round(tl * 100);
export const enUstAgirlik = (kademeler) => kademeler.at(-1).ustAgirlik;
EOF
cat > duzeltilmis/tasiyici-listesi.mjs <<'EOF'
export const TASIYICILAR = [{ ad: "standart", carpan: 1 }, { ad: "ekonomik", carpan: 0.85 }];
EOF
cat >> duzeltilmis/secim.mjs <<'EOF'
export const varsayilan = (agirlik) => enUcuz(agirlik, "orta", 0).ad;
EOF
node duzeltilmis/dene.mjs
node cizge-sagligi.mjs duzeltilmis
```

```
enUcuz(3, 'orta', 0) = {"ad":"ekonomik","tutar":9742}
duzeltilmis/  bilesen kenari = 5
  dongu = 0
  katman ihlali = 0
```

Giriş dosyası artık hata vermeden çalışıyor ve bir sonuç üretiyor. Bileşen kenarı yediden
beşe, döngü ikiden sıfıra, katman ihlali ikiden sıfıra indi. Kaybedilen bir yetenek yok:
varsayılan taşıyıcı hesabı hâlâ var, yalnız başka bir dosyada.

#### Sayının Eşik Olması

Bu iki sayının değeri, tek seferlik bir ölçüm olmamalarındadır. Döngüler tek bir büyük
kararla değil, tek tek masum görünen ithal satırlarıyla birikir; bozuk sürümdeki iki satırın
ikisi de yazıldıkları anda makul bir gerekçeye sahipti. Denetim her değişiklikte
koşturulduğunda ve döngü sayısı için eşik sıfır kabul edildiğinde, gerekçe tartışması satır
yazılmadan önce yapılır.

Aracın çıkardığı çizge, bir önceki dersin kararsızlık ölçüsünün de girdisidir. Aynı ithal
grafiğinden üç sayı okunur: döngü sayısı, katman ihlali sayısı ve bileşen başına kararsızlık.
Üçü birden sıfıra ya da beklenen yöne oturmuşsa bileşen sınırları çalışıyor demektir.

#### Özet

- Bileşen çizgesi elle yazılmaz; ithal satırlarından çıkarılır ve aynı bileşen içindeki
  ithaller elenerek bileşen kenarlarına indirgenir.
- Döngü, derine aramanın üç renkli düzeniyle bulunur: gri bir düğüme geri dönülmesi döngüdür
  ve yığıttaki yol döngünün üyelerini adlarıyla verir.
- Bozuk sürümde iki ithal satırı iki döngü ve iki katman ihlali üretti; aynı döngü çalışma
  zamanında giriş noktasına bağlı bir başlatma hatasına dönüştü.
- Katman ihlali ilan edilmiş bir kuralın ihlalidir, döngü ise kuraldan bağımsız bir
  kusurdur; katman kuralı olmayan bir kitaplıkta da döngü sayılabilir.
- Onarımdan sonra bileşen kenarı 7'den 5'e, döngü ve katman ihlali 2'den 0'a indi; taşınan
  hesap kaybolmadı, yalnız bağımlılığın zaten var olduğu tarafa geçti.

#### Kurs Kapanışı

Kurs bir tasarımın iyi olup olmadığını birimin içine değil, birimler arasındaki bağa bakarak
ölçtü ve her bölümde ölçüyü bir düzey yukarı taşıdı.

**SOLID** bölümü sınıf ve modül düzeyinde beş ölçüt kurdu: sorumluluğun iş miktarı değil
değişim nedeni sayısı olarak sayılması, davranışın var olan kodu düzenlemeden genişletilmesi,
bir alt tipin üst tipin sözleşmesini bozmadan yerine geçmesi, arayüzlerin istemcinin fiilen
kullandığı kadarıyla sınırlanması ve bağımlılığın somut ayrıntıya değil soyutlamaya
kurulması.

**Bağlaşım ve Uyum** bölümü modül çiftlerine geçti: iki modül arasındaki bağın gücünün
ölçülmesi, bir modül içindeki adların birbiriyle ilgililiği, bir çağrının kaç nesne üzerinden
geçtiğinin sınırlanması, veriyi çekip karar vermek yerine kararı veriye söylemek, durum
değiştiren işlemlerle okuyanların ayrılması, değişim eksenlerinin ayrı ayrı kapsüllenmesi,
somut tipe bağlanmanın maliyeti ve denetimin çağrılan tarafa devredilmesi.

**Sınırlar ve Bileşen İlkeleri** bölümü ölçeği dosyadan bileşene çıkardı: kararlı politikanın
değişken ayrıntıdan değişim sıklığıyla ayrılması, sözleşme yüzeyinin ad ve alan sayısıyla
ölçülüp daraltılması, sözleşmesi başkasının denetiminde olan bir aracın bağdaştırıcı arkasına
alınması, bileşen sınırının birlikte değişen dosyalardan çıkarılması, bağların kararlı yöne
akıp akmadığının sayılması ve bağımlılık çizgesinde döngü ile katman ihlalinin bulunması.

Kursun baştan sona koruduğu bir varsayım var ve kapanışta adlandırılması gerekiyor: ilkeler
bir tasarımın **nasıl olması gerektiğini** söyler, ne yapılacağını değil. "Sınır dar olsun",
"bağ kararlı yöne aksın", "birlikte değişenler bir arada dursun" cümlelerinin hiçbiri bir
çözüm vermez; ölçüt verir. Oysa aynı sınır problemi farklı kod tabanlarında defalarca aynı
biçimde çözülmüşse, o çözümün bir adı, bilinen bir uygulanış biçimi ve bilinen sonuçları
vardır.

Sonraki kurs **Tasarım Kalıpları** bunu ele alır: klasik kalıpların çözdükleri probleme göre
sınıflandırılması, bir kalıbın gereksiz karmaşıklık üretmeden — yani ilkelerin ölçüleri
kötüleşmeden — uygulanması ve kurumsal uygulama kalıplarının veri erişim tasarımında
kullanılması. Bu kursun ölçüleri orada da geçerlidir; kalıp, ölçüyü iyileştirdiği ölçüde
kalıptır.

