---
title: 'Liskov Yerine Koyma İlkesi'
source: 'https://academia.sh/tr/kurslar/tasarim-ilkeleri/liskov-yerine-koyma-ilkesi'
course: 'Tasarım İlkeleri'
language: tr
updated: '2026-08-17T18:10:40+00:00'
license: 'CC BY-SA 4.0'
---

# Liskov Yerine Koyma İlkesi

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.
