İçeriğe geç
academia.sh

Ders 06 / 14

Arayüzler ve Soyut Sınıflar

İki soyutlama aracının seçim ölçütü: ortak davranışın soyut sınıfla ve arayüzle paylaşıldığı iki düzenin kurulması, bir taşıyıcı iki soyutlamaya birden ihtiyaç duyduğunda tek kalıtımın kırıldığının koşturularak gösterilmesi ve tekrar edilen satır sayısının sayılması.

İçindekiler

Önceki dersteki 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. İki araç arasındaki seçim buradan doğar.

Arayüz (interface), bir tipin hangi yöntemleri sunacağını bildiren, hiçbir gerçekleştirim taşımayan sözleşmedir; TypeScript kursunda tanıtılan biçim budur. Soyut sınıf (abstract class) ise sözleşmenin yanında ortak gerçekleştirimi de taşır ve alt sınıflara devreder. Aradaki fark tek satırla özetlenir: bir tip birçok arayüz gerçekleştirebilir, ama yalnız bir sınıftan türeyebilir.

Ortak Davranışı Soyut Sınıfla Paylaşmak

Ücretlendirme kitaplığında taşıyıcılar iki ayrı yetenek taşır. Birincisi ücretlendirilebilir olmaktır: taşıyıcı gönderi başına birim ücreti bilir, toplamı asgari ücretle sınırlayan hesap ise ortaktır.

// soyut/ucretli.mjs — soyut sinif: ortak toplam hesabini tasir, birim ucreti alt sinifa birakir
export const ASGARI = 5000;

export class UcretliTasiyici {
  birimUcret(gonderi) { throw new Error(`birimUcret gerceklestirilmedi: ${gonderi.kod}`); }
  toplamUcret(gonderiler) {
    return Math.max(gonderiler.reduce((t, g) => t + this.birimUcret(g), 0), ASGARI);
  }
}

İkincisi izlenebilir olmaktır: taşıyıcı bir takip kodunun durum kaydını verir, kayıtlar arasından en yenisini seçen mantık ise yine ortaktır.

// soyut/izlenebilir.mjs — soyut sinif: son durumu secen ortak mantigi tasir
export class IzlenebilirTasiyici {
  durumKaydi(kod) { throw new Error(`durumKaydi gerceklestirilmedi: ${kod}`); }
  sonDurum(kodlar) {
    const kayitlar = kodlar.map((k) => this.durumKaydi(k)).filter((k) => k !== null);
    return kayitlar.reduce((en, k) => (k.zaman > en.zaman ? k : en), kayitlar[0]);
  }
}

Aktarmalı taşıyıcı ikisine birden ihtiyaç duyar: hem ücretlendirilir hem izlenir. Doğrudan karşılık, iki soyut sınıftan birden türemektir.

// soyut/aktarmali-hatali.mjs — iki soyut siniftan birden turetme denemesi
import { UcretliTasiyici } from "./ucretli.mjs";
import { IzlenebilirTasiyici } from "./izlenebilir.mjs";

export class AktarmaliTasiyici extends UcretliTasiyici, IzlenebilirTasiyici {
  birimUcret(g) { return 6000 + Math.ceil(g.agirlikGram / 1000) * 1200; }
  durumKaydi(kod) { return { kod, durum: "aktarmada", zaman: kod.length }; }
}

Aşağıdaki komut bu dosyayı çalıştırır. Çıktının dosya yolunu, yığıt izini ve sürüm satırını içeren bölümleri ortama bağlı olduğu için ayıklanmıştır; kalan üç satır hatanın kendisidir.

node soyut/aktarmali-hatali.mjs 2>&1 | sed -n '2,3p;5p'
export class AktarmaliTasiyici extends UcretliTasiyici, IzlenebilirTasiyici {
                                                      ^
SyntaxError: Unexpected token ','

Hata çalışma zamanında değil, dosya derlenirken çıkıyor. Tek kalıtım bir kütüphane kısıtı değil, dilin dilbilgisine yazılmış bir kısıttır. Aynı kısıt tek kalıtımlı her dilde geçerlidir ve ortak gerçekleştirimin sınıf hiyerarşisiyle paylaşıldığı her tasarımın üst sınırını belirler: bir tip ortak gerçekleştirimi en çok bir kaynaktan devralabilir.

Çıkış Yolu ve Bedeli

Kısıtın önündeki yollardan biri, bir soyut sınıftan türeyip ötekinin ortak kodunu kopyalamaktır.

// soyut/aktarmali.mjs — cikis yolu: bir soyut siniftan turetilir, otekinin ortak kodu kopyalanir
import { UcretliTasiyici } from "./ucretli.mjs";

export class AktarmaliTasiyici extends UcretliTasiyici {
  birimUcret(g) { return 6000 + Math.ceil(g.agirlikGram / 1000) * 1200; }
  durumKaydi(kod) { return { kod, durum: "aktarmada", zaman: kod.length }; }
  sonDurum(kodlar) {
    const kayitlar = kodlar.map((k) => this.durumKaydi(k)).filter((k) => k !== null);
    return kayitlar.reduce((en, k) => (k.zaman > en.zaman ? k : en), kayitlar[0]);
  }
}

export class YarimTasiyici extends UcretliTasiyici {
  durumKaydi(kod) { return { kod, durum: "beklemede", zaman: 0 }; }
}

İkinci bir yol, iki soyut sınıfı zincirlemektir: IzlenebilirTasiyici sınıfını UcretliTasiyici sınıfından türetmek. Bu, kısıtı aşar ama izlenen her taşıyıcıyı ücretlendirilebilir yapar; ücretlendirilmeyen bir aktarma noktası izleyicisi de ücret yöntemlerini devralır. Önceki dersin ölçütüyle bu, sözleşmesi tutmayan bir alt tip üretir.

Ortak Davranışı Arayüzle Paylaşmak

İkinci düzende soyutlama yalnız sözleşme bildirir. Sözleşme, gereken yöntem adlarının listesidir; uyum yapısal olarak denetlenir.

// arayuz/sozlesme.mjs — arayuzler yalnizca gereken yontem adlarini bildirir
export const UCRETLENDIRILEBILIR = ["birimUcret"];
export const IZLENEBILIR = ["durumKaydi"];

export const eksikler = (nesne, ...sozlesmeler) =>
  sozlesmeler.flat().filter((ad) => typeof nesne[ad] !== "function");

Ortak davranış kalıtımla değil, taşıyıcıyı argüman alan işlevlerle paylaşılır. Bu işlevler hiçbir sınıfa bağlı değildir; sözleşmeyi karşılayan her nesneyle çalışır.

// arayuz/ortak.mjs — ortak davranis kalitimla degil, tasiyiciyi argüman alan islevlerle paylasilir
export const ASGARI = 5000;

export const toplamUcret = (tasiyici, gonderiler) =>
  Math.max(gonderiler.reduce((t, g) => t + tasiyici.birimUcret(g), 0), ASGARI);

export function sonDurum(tasiyici, kodlar) {
  const kayitlar = kodlar.map((k) => tasiyici.durumKaydi(k)).filter((k) => k !== null);
  return kayitlar.reduce((en, k) => (k.zaman > en.zaman ? k : en), kayitlar[0]);
}

Aktarmalı taşıyıcı iki sözleşmeyi de karşılar ve hiçbir ortak kodu kopyalamaz.

// arayuz/aktarmali.mjs — iki arayuzu birden gerceklestirir; ortak kod kopyalanmaz
export class AktarmaliTasiyici {
  birimUcret(g) { return 6000 + Math.ceil(g.agirlikGram / 1000) * 1200; }
  durumKaydi(kod) { return { kod, durum: "aktarmada", zaman: kod.length }; }
}

export class YarimTasiyici {
  durumKaydi(kod) { return { kod, durum: "beklemede", zaman: 0 }; }
}

Tekrar Edilen Satır

Ölçüm, somut taşıyıcı dosyasındaki satırlardan kaçının ortak davranış dosyasında birebir bulunduğunu sayar; yorum, import satırları ve çok kısa satırlar sayıma girmez.

// tekrar.mjs — iki duzende tekrar edilen ortak davranis satirlarini sayar
import { readFileSync } from "node:fs";

const satirlar = (yol) => readFileSync(yol, "utf8").split("\n")
  .map((s) => s.trim())
  .filter((s) => s.length > 3 && !s.startsWith("//") && !s.startsWith("import"));

const tekrar = (kaynak, ortak) => {
  const kume = new Set(satirlar(ortak));
  return satirlar(kaynak).filter((s) => kume.has(s)).length;
};

console.log(`soyut sinif duzeni  tekrar edilen satir = ${tekrar("soyut/aktarmali.mjs", "soyut/izlenebilir.mjs")}`);
console.log(`arayuz duzeni       tekrar edilen satir = ${tekrar("arayuz/aktarmali.mjs", "arayuz/ortak.mjs")}`);
node tekrar.mjs
soyut sinif duzeni  tekrar edilen satir = 3
arayuz duzeni       tekrar edilen satir = 0

Üç satır küçük bir sayı gibi görünür, ama sabit değildir. İki yeteneğe birden ihtiyaç duyan her taşıyıcı aynı üç satırı kopyalar; on taşıyıcılı bir filoda otuz satır olur ve son durum seçme kuralı değiştiğinde on dosyaya dokunulur. Arayüz düzeninde aynı değişiklik tek dosyadadır, çünkü ortak kod hiç kopyalanmamıştır.

İki Düzen Aynı Sonucu Veriyor

Karşılaştırmanın anlamlı olması için iki düzenin aynı işi yapması gerekir. Aynı koşum, eksik gerçekleştirimin ne zaman görüldüğünü de ölçer: her iki filoda üçüncü taşıyıcı birimUcret yöntemini gerçekleştirmemiştir.

// kosum.mjs — iki duzenin ayni sonucu vermesi ve eksik gerceklestirimin ne zaman gorulmesi
import { AktarmaliTasiyici as SoyutTam, YarimTasiyici as SoyutYarim } from "./soyut/aktarmali.mjs";
import { AktarmaliTasiyici as ArayuzTam, YarimTasiyici as ArayuzYarim } from "./arayuz/aktarmali.mjs";
import { toplamUcret, sonDurum } from "./arayuz/ortak.mjs";
import { eksikler, UCRETLENDIRILEBILIR, IZLENEBILIR } from "./arayuz/sozlesme.mjs";

const GONDERILER = [{ kod: "TR1", agirlikGram: 4000 }, { kod: "TR2", agirlikGram: 900 }];
const KODLAR = ["TR1", "TR2200", "TR30"];

const s = new SoyutTam();
const a = new ArayuzTam();
console.log(`soyut  toplam = ${s.toplamUcret(GONDERILER)}  son durum = ${s.sonDurum(KODLAR).kod}`);
console.log(`arayuz toplam = ${toplamUcret(a, GONDERILER)}  son durum = ${sonDurum(a, KODLAR).kod}`);

const soyutFilo = [new SoyutTam(), new SoyutTam(), new SoyutYarim()];
let islenen = 0;
let hata = "";
try {
  for (const t of soyutFilo) { t.toplamUcret(GONDERILER); islenen += 1; }
} catch (e) { hata = e.message; }
console.log(`soyut  hatadan once islenen = ${islenen}/3  hata: ${hata}`);

const arayuzFilo = [new ArayuzTam(), new ArayuzTam(), new ArayuzYarim()];
const eksik = arayuzFilo.map((t) => eksikler(t, UCRETLENDIRILEBILIR, IZLENEBILIR));
const uygun = arayuzFilo.filter((_, i) => eksik[i].length === 0);
for (const t of uygun) toplamUcret(t, GONDERILER);
console.log(`arayuz kullanimdan once reddedilen = ${eksik.filter((e) => e.length > 0).length}/3` +
  `  eksik yontem: ${eksik.flat().join(",")}`);
console.log(`arayuz calisma zamani hatasi = 0  islenen = ${uygun.length}/3`);
node kosum.mjs
soyut  toplam = 18000  son durum = TR2200
arayuz toplam = 18000  son durum = TR2200
soyut  hatadan once islenen = 2/3  hata: birimUcret gerceklestirilmedi: TR1
arayuz kullanimdan once reddedilen = 1/3  eksik yontem: birimUcret
arayuz calisma zamani hatasi = 0  islenen = 2/3

İki düzen aynı toplamı ve aynı son durumu verdi. Eksik gerçekleştirimde ayrıldılar: soyut sınıf düzeninde hata ancak üçüncü taşıyıcı işlenmeye başladığında çıktı ve o noktada iki taşıyıcı zaten işlenmişti. Arayüz düzeninde eksiklik hiçbir iş yapılmadan önce ada kadar bildirildi.

Bu fark aracın kendisinden değil, denetimin nerede yapıldığından gelir. Soyut sınıfın fırlatan yöntemi bir çağrı bekler; sözleşme listesi ise çağrı gerektirmez. Statik tipleme kullanılan bir ortamda aynı denetim derleme zamanına iner ve koşumda hiç görünmez — TypeScript kursunda kurulan yapısal tipleme tam olarak bunu yapar.

Seçim Ölçütü

Soyut sınıf, ortak gerçekleştirim gerçekten paylaşılıyorsa ve bir tipin tek bir aileye ait olduğu kesinse tercih edilir; kalıtım hem sözleşmeyi hem kodu tek adımda taşır. Arayüz, bir tipin birden çok yetenek taşıması bekleniyorsa tercih edilir; ölçülen 3’e karşı 0 satırlık fark ve bunun taşıyıcı sayısıyla çarpılması bu seçimin bedelidir.

İki araç birbirini dışlamaz. Yaygın düzen, sözleşmeyi arayüzle bildirmek ve ortak gerçekleştirimi bağımsız işlevlerde ya da tek bir yardımcı tipte toplamaktır; böylece paylaşım kalıtım hattını tüketmez. Bu düzenin adı bileşimdir ve kalıtımla arasındaki seçim sonraki dersin konusudur.

Özet

  • Arayüz yalnız sözleşme bildirir, soyut sınıf sözleşmeyle birlikte ortak gerçekleştirimi de taşır; bir tip birçok arayüz gerçekleştirebilir ama tek bir sınıftan türeyebilir.
  • İki soyut sınıftan birden türeme denemesi çalışma zamanına bile ulaşmadan SyntaxError ile durdu; tek kalıtım dilin dilbilgisine yazılmış bir kısıttır.
  • Kısıtın çıkış yolu ortak kodun kopyalanmasıdır: soyut sınıf düzeninde 3 satır tekrar edildi, arayüz düzeninde 0.
  • Tekrar sabit değildir; iki yeteneğe birden ihtiyaç duyan her taşıyıcı aynı üç satırı çoğaltır ve kural değiştiğinde dokunulan dosya sayısı taşıyıcı sayısı kadar olur.
  • İki düzen aynı toplamı ve aynı son durumu üretti; ayrıldıkları yer eksik gerçekleştirimin görülme anıydı: soyut sınıfta iki taşıyıcı işlendikten sonra, arayüzde hiç iş yapılmadan.
  • İki soyut sınıfı zincirlemek kısıtı aşar ama izlenen her taşıyıcıyı ücretlendirilebilir yaparak sözleşmesi tutmayan bir alt tip üretir.

Sonraki Adım

Bu derste ortak davranış iki yolla paylaşıldı: kalıtımla ve taşıyıcıyı argüman alan işlevlerle. İkincisi kalıtım hattını tüketmediği için iki yeteneği bir arada taşıyabildi. Aynı tercihin bir başka sonucu daha vardır ve o, üst sınıfın kendi içinde bir değişiklik yaptığında ortaya çıkar. Sonraki ders bunu ölçer: bir üst sınıfta bir yöntemin gerçekleştirimi değiştiğinde alt sınıfın davranışının sessizce değişmesi bir koşumla gösterilir, sonra aynı değişiklik bileşimli sürümde denenir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat