---
title: 'Açık–Kapalı İlkesi'
source: 'https://academia.sh/tr/kurslar/tasarim-ilkeleri/acik-kapali-ilkesi'
course: 'Tasarım İlkeleri'
language: tr
updated: '2026-08-17T18:10:40+00:00'
license: 'CC BY-SA 4.0'
---

# Açık–Kapalı İlkesi

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.
