Ders 05 / 14
Çok Biçimlilik
Alt tip, parametrik ve geçici çok biçimliliğin aynı tarife problemi üzerinde ayrı ayrı gerçekleştirilmesi: sisteme yeni bir tarife türü ve yeni bir işlem eklendiğinde her yolda dokunulan dosya sayısının sayılması ve iki genişleme yönünün karşıtlığının gösterilmesi.
İçindekiler
Önceki derste iki alt sınıf aynı adı — ucret — farklı gerçekleştirdi ve istemci hangisiyle
çalıştığını bilmedi. Bu, tek bir adın birden çok davranışa karşılık gelmesidir. Alt tip
ilişkisi bunun yollarından yalnız biridir ve seçilen yol, sistemin hangi yönde
genişleyebileceğini belirler.
Çok biçimlilik (polymorphism), tek bir arayüzün birden çok tiple çalışabilmesidir. Programlama Temelleri kursunda tanıtılan tanım budur. Bu derste üç ayrı biçimi aynı problem üzerinde kurulur ve aralarındaki seçimin bedeli sayılır.
Üç Ayrı Biçim
Alt tip çok biçimliliği (subtype polymorphism), çağrının nesnenin çalışma zamanındaki tipine göre yönlendirilmesidir. Ad ortak bir üst tipte tanımlıdır, gerçekleştirim alt tiplerdedir.
Parametrik çok biçimlilik (parametric polymorphism), aynı kodun tipten bağımsız çalışmasıdır. Kod hiçbir tipe bakmaz; yaptığı iş tüm tipler için aynıdır. Tipe özgü olan kısım dışarıdan verilir.
Geçici çok biçimlilik (ad hoc polymorphism), her tip için ayrı bir gerçekleştirimin yazılıp ortak bir adla çağrılmasıdır. Aşırı yükleme destekleyen dillerde bu, aynı adın farklı imzalarla tanımlanmasıdır; burada tür etiketine bakan bir yönlendirme tablosuyla gerçekleştirilir.
Problem üçünde de aynıdır: iki tarife türü (kademeli, sabit) ve iki işlem (ucret,
teslimGunu).
Aynı Problem, Üç Gerçekleştirim
Alt tip yolunda üst tip işlemleri bildirir, her tür kendi dosyasında gerçekleştirir.
// v1/alttip/tarife.mjs — ust tip: her tarife turu bu iki yontemi gerceklestirir export class Tarife { ucret(gonderi) { throw new Error(`ucret gerceklestirilmedi: ${gonderi}`); } teslimGunu(gonderi) { throw new Error(`teslimGunu gerceklestirilmedi: ${gonderi}`); } }
// v1/alttip/kademeli.mjs — kademeli tarife turu import { Tarife } from "./tarife.mjs"; export class Kademeli extends Tarife { ucret(g) { return g.agirlikGram <= 1000 ? 4500 : 4500 + Math.ceil((g.agirlikGram - 1000) / 1000) * 1800; } teslimGunu(g) { return g.bolge === 1 ? 1 : 3; } }
// v1/alttip/sabit.mjs — sabit ucretli tarife turu import { Tarife } from "./tarife.mjs"; export class Sabit extends Tarife { ucret() { return 9000; } teslimGunu() { return 2; } }
Geçici yolda tür bir etikettir ve her işlem kendi dosyasında tüm türleri tek tabloda toplar. Bölünme yönü alt tip yolunun tam tersidir.
// v1/gecici/ucret.mjs — ucret islemi: tum turler icin tek tabloda const TABLO = { kademeli: (g) => (g.agirlikGram <= 1000 ? 4500 : 4500 + Math.ceil((g.agirlikGram - 1000) / 1000) * 1800), sabit: () => 9000, }; export const ucret = (tarife, gonderi) => TABLO[tarife.tur](gonderi);
// v1/gecici/teslim-gunu.mjs — teslim gunu islemi: tum turler icin tek tabloda const TABLO = { kademeli: (g) => (g.bolge === 1 ? 1 : 3), sabit: () => 2 }; export const teslimGunu = (tarife, gonderi) => TABLO[tarife.tur](gonderi);
Parametrik yolda tür hiç geçmez. Aşağıdaki iki işlev, ölçütü dışarıdan aldığı için her tarife temsiliyle çalışır: sınıf örnekleriyle de, etiketli düz nesnelerle de.
// v1/parametrik/secim.mjs — tur bilgisi kullanmaz; olcutu disaridan alir export function enUcuz(secenekler, olcut) { let en = secenekler[0]; for (const s of secenekler) if (olcut(s) < olcut(en)) en = s; return en; } export const siraya = (secenekler, olcut) => [...secenekler].sort((a, b) => olcut(a) - olcut(b));
Üç yol aynı gönderi üzerinde aynı sonuçları verir.
// v1/kosum.mjs — uc yolun ayni gonderide ayni sonuclari verdigini gosterir import { Kademeli } from "./alttip/kademeli.mjs"; import { Sabit } from "./alttip/sabit.mjs"; import { ucret } from "./gecici/ucret.mjs"; import { teslimGunu } from "./gecici/teslim-gunu.mjs"; import { enUcuz } from "./parametrik/secim.mjs"; const G = { agirlikGram: 4000, bolge: 2 }; const nesneler = [new Kademeli(), new Sabit()]; const kayitlar = [{ tur: "kademeli" }, { tur: "sabit" }]; console.log("alttip ", nesneler.map((t) => `${t.ucret(G)}/${t.teslimGunu(G)}`).join(" ")); console.log("gecici ", kayitlar.map((t) => `${ucret(t, G)}/${teslimGunu(t, G)}`).join(" ")); console.log("parametrik ", enUcuz(nesneler, (t) => t.ucret(G)).constructor.name, enUcuz(kayitlar, (t) => ucret(t, G)).tur);
node v1/kosum.mjs
alttip 9900/3 9000/2 gecici 9900/3 9000/2 parametrik Sabit sabit
Parametrik satır, o yolun karakterini gösteriyor: enUcuz hem sınıf örnekleri hem etiketli
kayıtlar üzerinde çalıştı, çünkü ikisine de bakmadı — yalnız kendisine verilen ölçütü
çağırdı.
İki Genişleme
Sisteme iki değişiklik gelir. Birincisi yeni bir tür: aktarma noktasından geçen gönderi tarifesi. Aşağıdaki blok ikinci sürümü birinciden kopyalayarak kurar ve bu türü ekler.
cp -r v1 v2 cat > v2/alttip/aktarmali.mjs <<'DOSYA' // v2/alttip/aktarmali.mjs — yeni tur: aktarma noktasindan gecen gonderi import { Tarife } from "./tarife.mjs"; export class Aktarmali extends Tarife { ucret(g) { return 6000 + Math.ceil(g.agirlikGram / 1000) * 1200; } teslimGunu() { return 5; } } DOSYA cat > v2/gecici/ucret.mjs <<'DOSYA' // v2/gecici/ucret.mjs — ucret islemi: yeni tur icin satir eklendi const TABLO = { kademeli: (g) => (g.agirlikGram <= 1000 ? 4500 : 4500 + Math.ceil((g.agirlikGram - 1000) / 1000) * 1800), sabit: () => 9000, aktarmali: (g) => 6000 + Math.ceil(g.agirlikGram / 1000) * 1200, }; export const ucret = (tarife, gonderi) => TABLO[tarife.tur](gonderi); DOSYA cat > v2/gecici/teslim-gunu.mjs <<'DOSYA' // v2/gecici/teslim-gunu.mjs — teslim gunu islemi: yeni tur icin satir eklendi const TABLO = { kademeli: (g) => (g.bolge === 1 ? 1 : 3), sabit: () => 2, aktarmali: () => 5 }; export const teslimGunu = (tarife, gonderi) => TABLO[tarife.tur](gonderi); DOSYA echo "v2 kuruldu"
v2 kuruldu
İkinci değişiklik yeni bir işlem: gönderinin beyan edilen değerine göre sigorta bedeli. Üçüncü sürüm ikinciden kopyalanarak kurulur.
cp -r v2 v3 cat > v3/alttip/tarife.mjs <<'DOSYA' // v3/alttip/tarife.mjs — ust tipe ucuncu yontem eklendi export class Tarife { ucret(gonderi) { throw new Error(`ucret gerceklestirilmedi: ${gonderi}`); } teslimGunu(gonderi) { throw new Error(`teslimGunu gerceklestirilmedi: ${gonderi}`); } sigortaBedeli(gonderi) { throw new Error(`sigortaBedeli gerceklestirilmedi: ${gonderi}`); } } DOSYA cat > v3/alttip/kademeli.mjs <<'DOSYA' // v3/alttip/kademeli.mjs — ucuncu yontem eklendi import { Tarife } from "./tarife.mjs"; export class Kademeli extends Tarife { ucret(g) { return g.agirlikGram <= 1000 ? 4500 : 4500 + Math.ceil((g.agirlikGram - 1000) / 1000) * 1800; } teslimGunu(g) { return g.bolge === 1 ? 1 : 3; } sigortaBedeli(g) { return Math.round(g.deger * 0.01); } } DOSYA cat > v3/alttip/sabit.mjs <<'DOSYA' // v3/alttip/sabit.mjs — ucuncu yontem eklendi import { Tarife } from "./tarife.mjs"; export class Sabit extends Tarife { ucret() { return 9000; } teslimGunu() { return 2; } sigortaBedeli() { return 0; } } DOSYA cat > v3/alttip/aktarmali.mjs <<'DOSYA' // v3/alttip/aktarmali.mjs — ucuncu yontem eklendi import { Tarife } from "./tarife.mjs"; export class Aktarmali extends Tarife { ucret(g) { return 6000 + Math.ceil(g.agirlikGram / 1000) * 1200; } teslimGunu() { return 5; } sigortaBedeli(g) { return Math.round(g.deger * 0.02); } } DOSYA cat > v3/gecici/sigorta-bedeli.mjs <<'DOSYA' // v3/gecici/sigorta-bedeli.mjs — yeni islem: tek dosya, tum turler const TABLO = { kademeli: (g) => Math.round(g.deger * 0.01), sabit: () => 0, aktarmali: (g) => Math.round(g.deger * 0.02), }; export const sigortaBedeli = (tarife, gonderi) => TABLO[tarife.tur](gonderi); DOSYA echo "v3 kuruldu"
v3 kuruldu
Dokunulan Dosya Sayısı
Ölçüm iki sürüm arasındaki farkı sayar. diff -rq hem değişen hem yeni eklenen dosya için
bir satır üretir; sayılan bu satırlardır. Karşılaştırma yalnız üç yolun kendi dizinlerini
kapsar — istemci tarafı üç yolda da aynı biçimde etkilendiği için ölçüme girmez.
for s in alttip gecici parametrik; do echo "$s" echo " yeni tur -> dokunulan dosya = $(diff -rq v1/$s v2/$s | wc -l | tr -d ' ')" echo " yeni islem -> dokunulan dosya = $(diff -rq v2/$s v3/$s | wc -l | tr -d ' ')" done
alttip yeni tur -> dokunulan dosya = 1 yeni islem -> dokunulan dosya = 4 gecici yeni tur -> dokunulan dosya = 2 yeni islem -> dokunulan dosya = 1 parametrik yeni tur -> dokunulan dosya = 0 yeni islem -> dokunulan dosya = 0
Sayılar karşıt. Alt tip yolunda yeni tür bir dosyaya, yeni işlem dört dosyaya mal oldu. Geçici yolda ilişki tersine döndü: yeni tür iki dosya, yeni işlem bir dosya. Parametrik yolda iki genişleme de sıfır dosyaya mal oldu.
Karşıtlığın nedeni bölünme yönüdür. Alt tip yolu kodu türe göre böler; bir türün tüm işlemleri yan yanadır, bu yüzden tür eklemek ucuz, işlem eklemek pahalıdır. Geçici yol kodu işleme göre böler; bir işlemin tüm türleri yan yanadır, bu yüzden ilişki tersine döner. Bir tasarım iki yönde birden ucuz olamaz; seçim, hangi yönde daha sık genişleneceğine göre verilir.
Parametrik yolun sıfırları, o yolun bedava olduğu anlamına gelmez. enUcuz iki genişlemeden
de etkilenmedi çünkü türe özgü hiçbir iş yapmıyor; ücreti hesaplayan ölçütü çağıran taraf
veriyor. Parametrik çok biçimlilik türe özgü işi ortadan kaldırmaz, onu kendi dışına iter.
Bu yüzden bir sistemde tek başına değil, diğer ikisiyle birlikte bulunur.
Genişletilmiş sistemin üç türü ve üç işlemi aynı sonuçları üretiyor.
// v3/kosum.mjs — genisletilmis sistem: uc tur, uc islem, ayni sonuclar import { Kademeli } from "./alttip/kademeli.mjs"; import { Sabit } from "./alttip/sabit.mjs"; import { Aktarmali } from "./alttip/aktarmali.mjs"; import { ucret } from "./gecici/ucret.mjs"; import { teslimGunu } from "./gecici/teslim-gunu.mjs"; import { sigortaBedeli } from "./gecici/sigorta-bedeli.mjs"; import { enUcuz } from "./parametrik/secim.mjs"; const G = { agirlikGram: 4000, bolge: 2, deger: 150000 }; const nesneler = [new Kademeli(), new Sabit(), new Aktarmali()]; const kayitlar = [{ tur: "kademeli" }, { tur: "sabit" }, { tur: "aktarmali" }]; const n = (t) => `${t.ucret(G)}/${t.teslimGunu(G)}/${t.sigortaBedeli(G)}`; const k = (t) => `${ucret(t, G)}/${teslimGunu(t, G)}/${sigortaBedeli(t, G)}`; console.log("alttip ", nesneler.map(n).join(" ")); console.log("gecici ", kayitlar.map(k).join(" ")); console.log("parametrik ", enUcuz(nesneler, (t) => t.ucret(G)).constructor.name, enUcuz(kayitlar, (t) => ucret(t, G)).tur);
node v3/kosum.mjs
alttip 9900/3/1500 9000/2/0 10800/5/3000 gecici 9900/3/1500 9000/2/0 10800/5/3000 parametrik Sabit sabit
Seçim Ölçütü
Ölçüt, sistemin hangi eksende genişlediğidir ve bu tahminle değil geçmişle yanıtlanır: son dönemde kaç tür, kaç işlem eklendiğine bakılır.
Tür ekseninin işlek olduğu yerlerde — taşıyıcı seçenekleri, ödeme yöntemleri, bildirim kanalları — işlem kümesi genelde erken kararlaşır ve alt tip yolu ucuza gelir. İşlem ekseninin işlek olduğu yerlerde — sabit bir tür kümesi üzerinde sürekli yeni rapor, yeni doğrulama, yeni dışa aktarma — geçici yol ucuza gelir; tür kümesi kapalıysa yeni bir işlem eklemek tek dosyaya iner.
Bir de yanlış seçimin sinyali vardır. Alt tip yolunda her yeni işlem için dört dosyaya
dokunuluyorsa ve bu her ay tekrarlanıyorsa, tasarımın yanlış eksende bölündüğü ölçülmüş
demektir. Aynı biçimde, geçici yolda tablolara sürekli yeni tür satırı ekleniyor ve bir tür
eklendiğinde bir tabloyu güncellemek unutuluyorsa — o durumda hata çalışma zamanında
TABLO[tarife.tur] tanımsız çıkarak görünür — eksen tersine dönmüş demektir.
Özet
- Çok biçimliliğin üç biçimi aynı problemi çözebilir: alt tip çalışma zamanı tipine yönlendirir, geçici tür etiketine göre tablo seçer, parametrik hiçbir tipe bakmaz.
- Aynı gönderide üç yol da aynı sonucu verdi; karşılaştırma davranış üzerinden değil genişleme maliyeti üzerinden yapıldı.
- Yeni bir tür eklendiğinde dokunulan dosya: alt tip 1, geçici 2, parametrik 0.
- Yeni bir işlem eklendiğinde dokunulan dosya: alt tip 4, geçici 1, parametrik 0.
- Karşıtlığın nedeni bölünme yönüdür: alt tip yolu türe göre, geçici yol işleme göre böler; bir tasarım iki yönde birden ucuz olamaz.
- Parametrik yolun sıfırları türe özgü işin ortadan kalktığını değil, kodun dışına itildiğini gösterir; tek başına değil diğer ikisiyle birlikte kullanılır.
Sonraki Adım
Alt tip yolundaki Tarife sınıfı iki iş yapıyordu: hangi yöntemlerin bulunacağını bildirmek
ve gerçekleştirilmediğinde hata fırlatmak. Bu iki iş ayrılabilir. Bir soyutlama yalnız
sözleşmeyi bildirebilir, bir başkası sözleşmeyle birlikte ortak gerçekleştirimi de
taşıyabilir. Sonraki ders bu iki aracı — arayüz ile soyut sınıfı — karşılaştırır ve seçimin
kırıldığı noktayı koşturarak gösterir: bir tarife türü iki ayrı soyutlamaya birden ihtiyaç
duyduğunda her düzenin ne yaptığı ve kaç satırın tekrar edildiği ölçülür.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.