← Kursa Dön
📄 Text · 30 min

Kalıtım ve Polimorfizm

Neden Bu Konu Önemli?

Bir hayvanat bahçesi yönetim sistemi yazıyorsun. Kediler, köpekler, papağanlar — hepsinin ortak özellikleri var (isim, yaş, beslenme) ama farklı davranışları da var (kedi miyavlar, köpek havlar, papağan konuşur). Her hayvan için sıfırdan ayrı sınıf yazmak yerine, ortak özellikleri bir "Hayvan" sınıfında tanımlayıp, her hayvan türünü ondan türetebilirsin.

İşte kalıtım (inheritance) budur: bir sınıf, başka bir sınıfın özellik ve metotlarını miras alır. Polimorfizm ise aynı metot adının farklı sınıflarda farklı davranması demektir — sesCikar() metodu kedide "Miyav!", köpekte "Hav hav!" döner.

Ama kalıtım bir gümüş kurşun değil. Bu derste hem kalıtımın gücünü, hem sınırlarını, hem de modern JavaScript'te tercih edilen composition (bileşim) yaklaşımını öğreneceğiz.


extends ile Kalıtım

ES6'da kalıtım extends keyword'ü ile yapılır. Alt sınıf (child/subclass), üst sınıfın (parent/superclass) tüm metot ve özelliklerini otomatik olarak alır.

// Üst sınıf (parent)
class Hayvan {
  constructor(isim, yas) {
    this.isim = isim;
    this.yas = yas;
    this.saglikDurumu = "sağlıklı";
  }
  
  bilgi() {
    return `${this.isim}, ${this.yas} yaşında, ${this.saglikDurumu}`;
  }
  
  sesCikar() {
    return "...";
  }
  
  beslen() {
    return `${this.isim} besleniyor`;
  }
}

// Alt sınıf (child) — Hayvan'dan kalıtır
class Kedi extends Hayvan {
  constructor(isim, yas, cins) {
    super(isim, yas); // Üst sınıfın constructor'ını çağır
    this.cins = cins;
  }
  
  // Method overriding — üst sınıfın metodunu geçersiz kıl
  sesCikar() {
    return "Miyav! 🐱";
  }
  
  // Alt sınıfa özel metot
  tirmala() {
    return `${this.isim} mobilyayı tırmaladı!`;
  }
}

class Kopek extends Hayvan {
  constructor(isim, yas, cins) {
    super(isim, yas);
    this.cins = cins;
  }
  
  sesCikar() {
    return "Hav hav! 🐕";
  }
  
  getir(nesne) {
    return `${this.isim} ${nesne}'i getirdi!`;
  }
}

// Kullanım
const tekir = new Kedi("Tekir", 3, "Tekir");
const karabas = new Kopek("Karabaş", 5, "Golden");

console.log(tekir.bilgi());     // "Tekir, 3 yaşında, sağlıklı" — Hayvan'dan
console.log(tekir.sesCikar());  // "Miyav! 🐱" — Kedi'de override
console.log(tekir.tirmala());   // "Tekir mobilyayı tırmaladı!" — Kedi'ye özel

console.log(karabas.beslen());    // "Karabaş besleniyor" — Hayvan'dan
console.log(karabas.sesCikar());  // "Hav hav! 🐕" — Kopek'te override
console.log(karabas.getir("top")); // "Karabaş topu getirdi!"

super Keyword'ü

super iki şekilde kullanılır:

1. Constructor'da: super()

Alt sınıfın constructor'ında this'i kullanmadan önce super() çağrılmalıdır. Bu, üst sınıfın constructor'ını çalıştırır.

class Sekil {
  constructor(renk) {
    this.renk = renk;
  }
}

class Daire extends Sekil {
  constructor(renk, yaricap) {
    // ❌ super() çağrılmadan this kullanılamaz!
    // this.yaricap = yaricap; // ReferenceError!
    
    super(renk); // ✅ Önce super() çağır
    this.yaricap = yaricap; // Artık this kullanılabilir
  }
}

// Constructor tanımlamazsan, otomatik olarak şu eklenir:
// constructor(...args) { super(...args); }

⚠️ Dikkat: Alt sınıfta constructor tanımlıyorsan super() zorunludur. Unutursan ReferenceError alırsın. Constructor tanımlamazsan sorun yok — otomatik eklenir.

2. Metotlarda: super.metotAdi()

Override ettiğin bir metodun üst sınıf versiyonunu çağırmak için:

class Hayvan {
  constructor(isim) {
    this.isim = isim;
  }
  
  bilgi() {
    return `${this.isim}`;
  }
  
  toString() {
    return `[Hayvan: ${this.isim}]`;
  }
}

class Kedi extends Hayvan {
  constructor(isim, cins) {
    super(isim);
    this.cins = cins;
  }
  
  // super ile üst sınıfın bilgi()'sini çağır + genişlet
  bilgi() {
    const temelBilgi = super.bilgi(); // Hayvan.bilgi() çağrılır
    return `${temelBilgi} (${this.cins} cinsi kedi)`;
  }
  
  toString() {
    return `[Kedi: ${this.isim}, ${this.cins}]`;
  }
}

const tekir = new Kedi("Pamuk", "British Shorthair");
console.log(tekir.bilgi());
// "Pamuk (British Shorthair cinsi kedi)"

Method Overriding (Metot Geçersiz Kılma)

Override, alt sınıfın üst sınıftan gelen bir metodu kendi versiyonuyla değiştirmesidir. Bu, polimorfizmin temelidir.

class Odeme {
  constructor(miktar) {
    this.miktar = miktar;
    this.tarih = new Date();
    this.durum = "bekliyor";
  }
  
  islemYap() {
    throw new Error("Bu metot alt sınıfta override edilmelidir!");
    // "Soyut metot" benzeri davranış
  }
  
  makbuzOlustur() {
    return `Tutar: ${this.miktar} TL — Durum: ${this.durum}`;
  }
}

class KrediKartiOdeme extends Odeme {
  constructor(miktar, kartNo) {
    super(miktar);
    this.kartNo = kartNo;
  }
  
  islemYap() {
    // Kredi kartı özel işlem mantığı
    console.log(`💳 Kart ${this.kartNo.slice(-4)} ile ödeme...`);
    this.durum = "onaylandi";
    return true;
  }
}

class HavaleOdeme extends Odeme {
  constructor(miktar, iban) {
    super(miktar);
    this.iban = iban;
  }
  
  islemYap() {
    // Havale özel işlem mantığı
    console.log(`🏦 IBAN'a havale: ${this.iban}...`);
    this.durum = "isleniyor"; // Havale anında onaylanmaz
    return true;
  }
}

class KripoOdeme extends Odeme {
  constructor(miktar, cuzdanAdresi) {
    super(miktar);
    this.cuzdanAdresi = cuzdanAdresi;
  }
  
  islemYap() {
    console.log(`₿ Kripto transfer: ${this.cuzdanAdresi.slice(0, 10)}...`);
    this.durum = "onaylandi";
    return true;
  }
}

// Polimorfizm: Aynı arayüz, farklı davranış
function odemeleriIsle(odemeler) {
  for (const odeme of odemeler) {
    odeme.islemYap(); // Her ödeme türü kendi implementasyonunu çalıştırır
    console.log(odeme.makbuzOlustur());
    console.log("---");
  }
}

// Farklı ödeme türleri, aynı fonksiyonla işlenir
odemeleriIsle([
  new KrediKartiOdeme(150, "4532-XXXX-XXXX-1234"),
  new HavaleOdeme(500, "TR33 0006 1005 1978 6457 8413 26"),
  new KripoOdeme(0.05, "0x742d35Cc6634C0532925a3b844Bc9e7595f"),
]);

instanceof Kontrolü

const odeme = new KrediKartiOdeme(100, "4532-XXX");

console.log(odeme instanceof KrediKartiOdeme); // true
console.log(odeme instanceof Odeme);            // true
console.log(odeme instanceof Object);           // true

// Tür kontrolü ile farklı davranış
function odemeyiGoruntule(odeme) {
  if (odeme instanceof KrediKartiOdeme) {
    console.log(`Kart: ****${odeme.kartNo.slice(-4)}`);
  } else if (odeme instanceof HavaleOdeme) {
    console.log(`IBAN: ${odeme.iban}`);
  }
}

Mixins — Çoklu Kalıtım Alternatifi

JavaScript tekli kalıtımı destekler — bir sınıf sadece bir sınıftan extends edebilir. Ama bazen birden fazla kaynaktan özellik almak istersin. Mixins bu ihtiyacı karşılar.

// Mixin: Bir fonksiyon olarak tanımlanan davranış paketi
const Serileştirilebilir = (Base) =>
  class extends Base {
    toJSON() {
      const sonuc = {};
      for (const key of Object.keys(this)) {
        sonuc[key] = this[key];
      }
      return sonuc;
    }
    
    static fromJSON(json) {
      const data = typeof json === "string" ? JSON.parse(json) : json;
      return Object.assign(new this(), data);
    }
  };

const OlayGonderilebilir = (Base) =>
  class extends Base {
    #olaylar = new Map();
    
    on(olay, dinleyici) {
      if (!this.#olaylar.has(olay)) {
        this.#olaylar.set(olay, []);
      }
      this.#olaylar.get(olay).push(dinleyici);
      return this;
    }
    
    emit(olay, ...args) {
      const dinleyiciler = this.#olaylar.get(olay) || [];
      dinleyiciler.forEach((fn) => fn(...args));
      return this;
    }
  };

const Dogrulanabilir = (Base) =>
  class extends Base {
    dogrula() {
      const hatalar = [];
      // Basit doğrulama kuralları
      for (const [key, value] of Object.entries(this)) {
        if (value === null || value === undefined) {
          hatalar.push(`${key} boş olamaz`);
        }
      }
      return hatalar;
    }
    
    get gecerliMi() {
      return this.dogrula().length === 0;
    }
  };

// Mixinleri birleştir — "class pipeline"
class Kullanici extends Serileştirilebilir(OlayGonderilebilir(Dogrulanabilir(class {}))) {
  constructor(ad, email) {
    super();
    this.ad = ad;
    this.email = email;
  }
}

const kullanici = new Kullanici("Ahmet", "ahmet@test.com");

// Serileştirilebilir
console.log(JSON.stringify(kullanici));
// {"ad":"Ahmet","email":"ahmet@test.com"}

// OlayGonderilebilir
kullanici.on("kaydet", () => console.log("Kaydedildi!"));
kullanici.emit("kaydet");
// "Kaydedildi!"

// Dogrulanabilir
console.log(kullanici.gecerliMi); // true

💡 İpucu: Mixin pattern'ı güçlüdür ama karmaşıklaşabilir. 3-4'ten fazla mixin birleştirmek kodu anlaşılmaz hale getirebilir. Alternatif olarak composition düşünün.


Composition vs Inheritance

Yazılım dünyasında ünlü bir söz var: "Favor composition over inheritance" (Kalıtım yerine bileşimi tercih et). Bu neden söyleniyor?

Kalıtımın Sorunları

// ❌ Kalıtım zinciri sorunları

// Başlangıçta makul görünüyor:
class Hayvan { /* yemek, uyumak */ }
class Kuş extends Hayvan { /* uçmak */ }
class Ördek extends Kuş { /* yüzmek */ }

// Ama sorunlar başlıyor:
// - Penguen kuş ama UÇAMAZ — Kuş'tan extends edemez
// - Yarasa uçar ama kuş DEĞİL — Kuş'tan extends edemez
// - Deve kuşu koşar, uçamaz, kuştur — ne yapacağız?

// "Diamond problem" — çoklu kalıtım olsaydı:
// class Amfibi extends SudaYaşayan, KaradaYaşayan { /* çakışma! */ }

Composition Yaklaşımı

Composition'da davranışları ayrı nesneler olarak tanımlayıp, ihtiyaç duyan sınıflara enjekte edersin:

// Davranışlar bağımsız nesneler/fonksiyonlar olarak
const yüzücü = (nesne) => ({
  yüz() {
    return `${nesne.isim} yüzüyor 🏊`;
  },
});

const uçucu = (nesne) => ({
  uç() {
    return `${nesne.isim} uçuyor 🦅`;
  },
});

const koşucu = (nesne) => ({
  koş() {
    return `${nesne.isim} koşuyor 🏃`;
  },
});

const konuşucu = (nesne) => ({
  konuş(mesaj) {
    return `${nesne.isim}: "${mesaj}" 🗣️`;
  },
});

// Factory fonksiyonları — davranışları birleştir
function ördekOlustur(isim) {
  const nesne = { isim, tur: "ördek" };
  return Object.assign(
    nesne,
    yüzücü(nesne),
    uçucu(nesne),
    koşucu(nesne)
  );
}

function penguenOlustur(isim) {
  const nesne = { isim, tur: "penguen" };
  return Object.assign(
    nesne,
    yüzücü(nesne),
    koşucu(nesne)
    // uçucu YOK — penguen uçamaz!
  );
}

function papağanOlustur(isim) {
  const nesne = { isim, tur: "papağan" };
  return Object.assign(
    nesne,
    uçucu(nesne),
    konuşucu(nesne)
  );
}

// Kullanım
const donald = ördekOlustur("Donald");
console.log(donald.yüz());  // "Donald yüzüyor 🏊"
console.log(donald.uç());   // "Donald uçuyor 🦅"

const tux = penguenOlustur("Tux");
console.log(tux.yüz());     // "Tux yüzüyor 🏊"
console.log(tux.koş());     // "Tux koşuyor 🏃"
// tux.uç() → undefined — yok çünkü penguen uçamaz!

const polly = papağanOlustur("Polly");
console.log(polly.uç());        // "Polly uçuyor 🦅"
console.log(polly.konuş("Merhaba!")); // 'Polly: "Merhaba!" 🗣️'

Class ile Composition

// Strategy pattern ile class tabanlı composition
class Logger {
  log(mesaj) {
    console.log(`[LOG] ${mesaj}`);
  }
}

class Validator {
  dogrula(data, kurallar) {
    const hatalar = [];
    for (const [alan, kural] of Object.entries(kurallar)) {
      if (kural.zorunlu && !data[alan]) {
        hatalar.push(`${alan} zorunludur`);
      }
      if (kural.minUzunluk && data[alan]?.length < kural.minUzunluk) {
        hatalar.push(`${alan} en az ${kural.minUzunluk} karakter olmalı`);
      }
    }
    return hatalar;
  }
}

class ApiClient {
  async get(url) {
    const response = await fetch(url);
    return response.json();
  }
  
  async post(url, data) {
    const response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return response.json();
  }
}

// Composition: Bağımlılıkları enjekte et
class UserService {
  // Kalıtım YOK — composition ile davranış ekle
  #logger;
  #validator;
  #api;
  
  constructor(logger, validator, api) {
    this.#logger = logger;
    this.#validator = validator;
    this.#api = api;
  }
  
  async kullaniciOlustur(data) {
    this.#logger.log("Kullanıcı oluşturuluyor...");
    
    const hatalar = this.#validator.dogrula(data, {
      ad: { zorunlu: true, minUzunluk: 2 },
      email: { zorunlu: true },
    });
    
    if (hatalar.length > 0) {
      this.#logger.log(`Doğrulama hatası: ${hatalar.join(", ")}`);
      throw new Error(hatalar.join(", "));
    }
    
    const sonuc = await this.#api.post("/api/users", data);
    this.#logger.log(`Kullanıcı oluşturuldu: ${sonuc.id}`);
    return sonuc;
  }
}

// Bağımlılıkları oluştur ve enjekte et
const userService = new UserService(
  new Logger(),
  new Validator(),
  new ApiClient()
);

Ne Zaman Kalıtım, Ne Zaman Composition?

// ✅ Kalıtım uygun olduğunda — "is-a" (bir türüdür) ilişkisi
// Kedi BİR hayvan türüdür → class Kedi extends Hayvan ✅
// Daire BİR şekil türüdür → class Daire extends Sekil ✅
// AdminUser BİR kullanıcı türüdür → class Admin extends User ✅

// ✅ Composition uygun olduğunda — "has-a" (sahiptir) ilişkisi
// Araba BİR motor DEĞİL, motorA SAHİPTİR → composition ✅
// UserService BİR logger DEĞİL, logger KULLANIR → composition ✅
// Oyuncu BİR hareket yeteneği DEĞİL, hareket EDEBILIR → composition ✅

// ❌ Kalıtımı zorlamak — yanlış
// class Stack extends Array → Stack bir dizi DEĞİL
//   push, pop dışında shift, splice gibi metotlar da miras alınır
//   Bu istenmeyen davranış!
// ✅ Doğrusu: Stack içinde bir dizi KULLANIR (composition)

Yaygın Hatalar ve Tuzaklar

Hata 1: super() Unutmak

class Alt extends Ust {
  constructor(deger) {
    // ❌ super() çağırmadan this kullanma
    // this.deger = deger; // ReferenceError!
    
    // ✅ Önce super()
    super();
    this.deger = deger;
  }
}

Hata 2: Derin Kalıtım Zinciri

// ❌ 5+ seviye kalıtım — bakımı imkânsız
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
class E extends D {}
class F extends E {}
// Bir metot değiştiğinde tüm zincir etkilenir!

// ✅ Sığ kalıtım + composition
// Maximum 2-3 seviye kalıtım
// Davranış ekleme için composition/mixin kullan

Hata 3: Üst Sınıfın Private Alanlarına Erişim

class Ust {
  #gizli = "gizli veri";
  
  gizliyiAl() {
    return this.#gizli;
  }
}

class Alt extends Ust {
  goster() {
    // ❌ Private alanlar kalıtılmaz!
    // return this.#gizli; // SyntaxError!
    
    // ✅ Public/protected metot üzerinden eriş
    return this.gizliyiAl(); // Üst sınıfın public metodu
  }
}

💡 İpucu: JavaScript'te protected (sadece alt sınıflardan erişilebilir) anahtar kelimesi yoktur. Convention olarak _ prefix'i kullanılır (_dahiliMetot) ama bu gerçek koruma sağlamaz. TypeScript'te protected keyword'ü vardır.


Gerçek Dünya Örneği: UI Component Hiyerarşisi

// Basit UI component sistemi — kalıtım + composition dengesi
class Component {
  #element;
  #children = [];
  
  constructor(tag = "div") {
    this.#element = document.createElement(tag);
  }
  
  get element() {
    return this.#element;
  }
  
  addClass(...siniflar) {
    this.#element.classList.add(...siniflar);
    return this;
  }
  
  setText(metin) {
    this.#element.textContent = metin;
    return this;
  }
  
  setHTML(html) {
    this.#element.innerHTML = html;
    return this;
  }
  
  append(child) {
    if (child instanceof Component) {
      this.#element.appendChild(child.element);
      this.#children.push(child);
    }
    return this;
  }
  
  on(olay, handler) {
    this.#element.addEventListener(olay, handler);
    return this;
  }
  
  render(hedef) {
    if (typeof hedef === "string") {
      document.querySelector(hedef).appendChild(this.#element);
    } else {
      hedef.appendChild(this.#element);
    }
    return this;
  }
}

// Alt sınıflar — spesifik component'ler
class Button extends Component {
  constructor(metin, tip = "primary") {
    super("button");
    this.setText(metin);
    this.addClass("btn", `btn-${tip}`);
  }
  
  disable() {
    this.element.disabled = true;
    this.addClass("disabled");
    return this;
  }
  
  enable() {
    this.element.disabled = false;
    this.element.classList.remove("disabled");
    return this;
  }
}

class Card extends Component {
  #header;
  #body;
  
  constructor(baslik) {
    super("div");
    this.addClass("card");
    
    this.#header = new Component("div").addClass("card-header").setText(baslik);
    this.#body = new Component("div").addClass("card-body");
    
    this.append(this.#header);
    this.append(this.#body);
  }
  
  addContent(component) {
    this.#body.append(component);
    return this;
  }
}

// Kullanım
const card = new Card("Kullanıcı Profili");

const kaydetBtn = new Button("Kaydet", "primary")
  .on("click", () => console.log("Kaydedildi!"));

const iptalBtn = new Button("İptal", "secondary")
  .on("click", () => console.log("İptal edildi!"));

card.addContent(kaydetBtn);
card.addContent(iptalBtn);

// card.render("#app");

Özet

Bu derste kalıtım, polimorfizm ve alternatif yaklaşımları öğrendik:

  • `extends` ile bir sınıf başka bir sınıfın özelliklerini miras alır. `super()` üst sınıfın constructor'ını, `super.metot()` üst sınıfın metodunu çağırır.

  • Method overriding alt sınıfın üst sınıf metodunu geçersiz kılmasıdır. Bu, polimorfizmin temelidir — aynı arayüz, farklı davranış.

  • Mixins çoklu kalıtım eksikliğini fonksiyonlarla kapatan bir pattern'dir. Sınıfa ek davranışlar eklemek için kullanılır.

  • Composition over inheritance: Kalıtım "is-a" (türüdür) ilişkisi için, composition "has-a" (sahiptir/kullanır) ilişkisi için tercih edilir.

  • Derin kalıtım zincirleri (5+ seviye) bakım kabusu yaratır — sığ kalıtım + composition dengesi ideal.

  • JavaScript'te protected yoktur; private alanlar (#) alt sınıflardan bile erişilemez.

Bir sonraki derste JavaScript Design Patterns'ı inceleyeceğiz.