Ders 03 / 19
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ı.
İçindekiler
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.
// 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})`); } }
// 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.
// 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()];
// 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());
// 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);
// 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)}`);
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ı.
// 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.
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.
// 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"; } }
// 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.
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.
// 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}`); } }
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.
// 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"; } }
// 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());
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.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.