Mert Özen Her Satırda Daha İleri
Frontend JavaScript İleri — #03

JavaScript OOP Desenleri: Inheritance mı Composition mı? Factory Function ve Mixin

Mert Özen 3 Kas 2026 9 dk 4 görüntülenme
JavaScript OOP Desenleri: Inheritance mı Composition mı? Factory Function ve Mixin

Inheritance'ın bedelini, composition ile davranış birleştirmeyi, factory function ve mixin'leri, closure ile #private arasındaki seçimi, JavaScript'e uyan SOLID ilkelerini ve ne zaman class yazmak gerektiğini konuşuyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bir sayaç üreten küçük bir fonksiyon yazdık. Sayacın değerini dışarıdan 100'e çekmeye çalışıyoruz. Bu kod konsola ne yazdırır?

function createCounter() {
  let count = 0;
  return {
    increment() {
      count += 1;
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
counter.count = 100;

console.log(counter.increment());

Okumaya devam etmeden önce bir tahmin yap. 101 mi, 2 mi, yoksa bir hata mı? Cevabı, veriyi gizlemenin yollarını konuşurken açıklayacağız.

Inheritance'ın Cazibesi ve Bedeli

Son iki yazıda object'lerin nasıl bağlandığını ve nasıl korunduğunu gördük. Şimdi asıl soruya geliyoruz: bir programın object'lerini nasıl tasarlamalısın? Çoğumuz nesne yönelimli programlamaya inheritance'la başlarız ve ilk tasarım hep çok mantıklı görünür:

class Animal {
  eat() { return "eating"; }
}
class Bird extends Animal {
  fly() { return "flying"; }
}
class Penguin extends Bird {
  fly() { throw new Error("Penguins can't fly"); }
}

const pingu = new Penguin();
console.log(pingu.eat()); // "eating"
console.log(pingu instanceof Bird); // true
// pingu.fly() → Error: Penguins can't fly

Sorun burada başlıyor. Penguen bir kuş, ama kuşun en temel davranışını reddediyor. Bird bekleyen her kod, elinde bir Penguin olduğunda patlayabilir. Yarın yüzebilen bir kuş, ertesi gün uçabilen bir memeli (yarasa) eklemen gerektiğinde ağaç iyice karışır. Inheritance, tasarımın en başında "kim kimin alt türü" sorusunu cevaplamanı ister ve gerçek hayat o cevabı sık sık değiştirir.

Bir de kırılgan taban class'ı (fragile base class) sorunu var: üst class'taki küçük bir değişiklik, onu miras alan ve senin hiç bakmadığın onlarca alt class'ı bozabilir. Erlang'ın yaratıcısı Joe Armstrong bunu unutulmaz bir benzetmeyle anlatmıştı: muz istedin, ama elinde muzu tutan bir goril ve bütün orman geldi. Inheritance'la bir davranışı alırken o davranışın bağlı olduğu her şeyi de alırsın.

Composition: Davranışları Parça Parça Birleştirmek

Composition'ın sorusu farklıdır. "Bu object nedir?" yerine "bu object ne yapabilir?" diye sorar. Inheritance bir soy ağacıysa, composition bir Lego kutusudur: her parça tek bir iş yapar, sen ihtiyacın olanları birleştirirsin.

const canEat = (state) => ({
  eat() { return `${state.name} is eating`; }
});
const canFly = (state) => ({
  fly() { return `${state.name} is flying`; }
});
const canSwim = (state) => ({
  swim() { return `${state.name} is swimming`; }
});

function createPenguin(name) {
  const state = { name };
  return { ...canEat(state), ...canSwim(state) };
}

function createDuck(name) {
  const state = { name };
  return { ...canEat(state), ...canFly(state), ...canSwim(state) };
}

const penguin = createPenguin("Pingu");
const duck = createDuck("Donald");

console.log(penguin.swim()); // "Pingu is swimming"
console.log(duck.fly()); // "Donald is flying"
console.log("fly" in penguin); // false

Penguenin artık reddetmesi gereken bir fly method'u yok, çünkü ona hiç verilmedi. Yarasa eklemek istediğinde ağacı yeniden düzenlemezsin; canEat ve canFly'ı birleştirirsin. Her yetenek küçük ve tek başına anlaşılır. "Composition'ı inheritance'a tercih et" tavsiyesinin özü budur: inheritance'ı yasaklamak değil, varsayılan seçim yapmamak.

Factory Function: new Olmadan Object Üretmek

Yukarıdaki createPenguin ve en baştaki createCounter birer factory function. Factory, object döndüren sıradan bir fonksiyondur. new gerekmez, this gerekmez; dolayısıyla daha önce gördüğümüz this kaybı sorunları da ortadan kalkar. Method'u bir callback olarak başka bir yere verdiğinde bozulmaz, çünkü veriye this üzerinden değil closure üzerinden ulaşır:

function createTimer(label) {
  let seconds = 0;
  return {
    tick() {
      seconds += 1;
      console.log(`${label}: ${seconds} s`);
    }
  };
}

const timer = createTimer("Tea");
const tick = timer.tick;
tick(); // "Tea: 1 s"

const another = createTimer("Coffee");
console.log(timer.tick === another.tick); // false

Son satır bedeli gösteriyor. Her factory çağrısı method'ların yeni kopyalarını üretir. Önceki yazılarda gördüğümüz prototype paylaşımı burada yok. Birkaç yüz object için bu fark önemsizdir; aynı anda yüz binlerce object üretiyorsan bellek ve hız açısından class ve prototype öne geçer. Factory'nin bir avantajı daha var: duruma göre farklı tipte object'ler döndürebilir ya da önbellekten var olan bir object'i verebilir. Constructor'lar her zaman yeni bir object'le başlar.

Mixin: Class'lara Yetenek Eklemek

Class kullanıyorsan ve tek bir zincirle sınırlı kalmak istemiyorsan mixin'lere başvurursun. En sade hâli, bir method object'ini prototype'a kopyalamaktır: Object.assign(User.prototype, serializableMixin). Daha güçlü bir yol, bir class alıp onu genişleten bir class döndüren fonksiyon yazmaktır:

const Serializable = (Base) => class extends Base {
  serialize() {
    return JSON.stringify(this);
  }
};

const Timestamped = (Base) => class extends Base {
  constructor(...args) {
    super(...args);
    this.createdAt = new Date().toISOString();
  }
};

class Note {
  constructor(text) {
    this.text = text;
  }
}

class SmartNote extends Serializable(Timestamped(Note)) {}

const note = new SmartNote("Buy milk");
console.log(note.serialize());
// {"text":"Buy milk","createdAt":"..."}
console.log(note instanceof Note); // true

Bu yapıda SmartNote'un zinciri Serializable'ın ürettiği class'tan, Timestamped'in ürettiği class'tan geçip Note'a varır. Her mixin bir halka ekler ve super normal şekilde çalışır. createdAt çalıştırdığın anın tarihini taşıyacağı için yorumdaki değeri kısalttık.

Mixin'lerin riski isim çakışmasıdır. İki mixin aynı isimde bir method tanımlarsa zincirde en dışta olan kazanır ve bunu kimse sana söylemez. Mixin sayısı arttıkça "bu method nereden geliyor?" sorusu da zorlaşır. Az sayıda, iyi isimlendirilmiş ve birbirinden bağımsız mixin kullan.

Encapsulation: Closure mı #private mı?

Baştaki soruya dönelim. Çıktı 2. count değişkeni createCounter'ın scope'unda yaşıyor ve dışarıdan ona ulaşmanın hiçbir yolu yok. counter.count = 100 satırı o değişkene dokunmadı; object'in üzerinde count adında yeni ve tamamen alakasız bir property oluşturdu. increment ise closure sayesinde hâlâ gerçek count'u görüyor, onu 1'den 2'ye çıkardı.

Class'larda aynı korumayı ES2022 ile gelen private alanlar sağlar. Daha önce gördüğümüz # syntax'ıyla:

class BankAccount {
  #balance = 0;

  deposit(amount) {
    if (amount <= 0) throw new RangeError("Amount must be positive");
    this.#balance += amount;
    return this.#balance;
  }

  static isAccount(value) {
    return #balance in value;
  }
}

const account = new BankAccount();
account.deposit(250);
account.balance = 1_000_000;

console.log(account.deposit(50)); // 300
console.log(BankAccount.isAccount(account)); // true
console.log(BankAccount.isAccount({ balance: 1 })); // false

Class dışında account.#balance yazmak bir SyntaxError'dır; kod çalışmaya bile başlamaz. Küçük bir not: Chrome DevTools, hata ayıklamayı kolaylaştırmak için konsolda private alanları okumana izin verir. Konsolda çalışıyor diye gerçek kodda da çalışacağını sanma. #balance in value ise, yine ES2022 ile gelen, bir object'in gerçekten bu class'tan üretilip üretilmediğini kontrol etmenin güvenilir yoludur.

İkisi arasında nasıl seçim yaparsın? Closure her object için ayrı fonksiyon kopyası ister ama son derece basittir. #private alanlar prototype method'larıyla çalışır, yani bellek dostudur. Alt class'lar da private alanlara erişemez. Alt çizgiyle başlayan isimler (_balance) ise yalnızca bir nezaket kuralıdır, hiçbir şeyi gizlemez.

SOLID'den JavaScript'e Taşınabilen Birkaç İlke

SOLID ilkeleri class ağırlıklı dillerde doğdu, ama bazıları JavaScript'te de işe yarar. Tek sorumluluk ilkesi: bir module ya da object'in değişmek için tek bir sebebi olsun. Kullanıcıyı doğrulayan, veritabanına yazan ve e-posta gönderen tek bir object, üç farklı sebeple değişir. Açık-kapalı ilkesi: yeni davranış eklemek için var olan kodu değiştirmek yerine genişletebilmelisin; JavaScript'te bu genelde parametre olarak verilen bir fonksiyon ya da bir strateji object'i demektir. Liskov ilkesini zaten penguende gördük: bir alt tür, üst türün yerine kullanıldığında hiçbir şeyi bozmamalı.

En pratik olanı bağımlılıkların tersine çevrilmesi. Bir object ihtiyaç duyduğu şeyi kendisi oluşturmak yerine dışarıdan almalı:

function createNotifier({ send }) {
  return {
    welcome(user) {
      return send(user.email, `Welcome, ${user.name}!`);
    }
  };
}

const consoleNotifier = createNotifier({
  send: (to, text) => console.log(`[${to}] ${text}`)
});

consoleNotifier.welcome({ name: "Alan", email: "alan@example.com" });
// "[alan@example.com] Welcome, Alan!"

createNotifier, mesajın nasıl gönderildiğini bilmiyor ve umursamıyor. Bugün konsola yazıyor, yarın aynı object'e fetch ile bir sunucuya istek atan bir send verirsin; welcome'ın tek satırı bile değişmez.

Ne Zaman Class, Ne Zaman Düz Fonksiyon?

Kesin bir kural yok ama işe yarayan bir pusula var. Aynı şekle sahip çok sayıda object üretiyorsan, instanceof ile tip kontrolüne ihtiyacın varsa, Error gibi yerleşik bir türü genişletiyorsan ya da ileride göreceğimiz Web Components'ta olduğu gibi platform senden bir class istiyorsa class kullan. Elinde az sayıda object varsa, davranışları serbestçe karıştırmak istiyorsan ya da this ile uğraşmak istemiyorsan factory ve composition daha rahattır.

Ve çoğu zaman ikisine de gerek yoktur. Bir veriyi alıp yeni bir veri döndüren işler için düz fonksiyonlar en iyisidir: calculateTotal(cart), formatPrice(amount). Her şeyi bir object'in method'u yapmak zorunda değilsin. Veriyi düz object'lerde tut, davranışı küçük fonksiyonlarda; object'i gerçekten bir durumu ve o durumu koruyan kuralları bir arada tutman gerektiğinde kur.

Sık Düşülen Tuzaklar

Yalnızca kod paylaşmak için inheritance kullanmak. İki class'ın ortak üç satırı var diye birini diğerinden türetme; o üç satırı bir fonksiyona taşı. Inheritance "bir tür ilişkisi" kurar, kod paylaşımı bunun yan etkisidir.

Spread ile composition yaparken getter'ları kaybetmek. Spread, getter'ı çağırır ve sonucunu düz bir değer olarak kopyalar; canlı bağlantı kopar:

const source = {
  first: "Ada",
  last: "Lovelace",
  get full() { return `${this.first} ${this.last}`; }
};

const copy = { ...source };
copy.first = "Grace";
console.log(copy.full); // "Ada Lovelace"

Getter'ları korumak istiyorsan bir önceki yazıdaki descriptor araçlarını kullan: Object.defineProperties({}, Object.getOwnPropertyDescriptors(source)).

Class method'unu callback olarak verip this'i kaybetmek. button.addEventListener("click", account.deposit) yazarsan method çalışır ama this artık account değildir; private alana erişmeye çalıştığı anda TypeError alırsın. Arrow function ile sar ya da bind kullan. Son olarak, desenlerin kendisini hedef hâline getirmek: üç satırlık bir iş için mixin zinciri kurmak, çözdüğünden fazla sorun yaratır.

Küçük Bir Deneme

★☆☆ Çalıştırmadan önce tahmin et: const a = createCounter(); const b = createCounter(); a.increment(); a.increment(); console.log(a.increment(), b.increment()); İki sayaç aynı count'u mu paylaşıyor?

★★☆ canEat, canFly ve canSwim'e bir de canWalk ekle ve bunlarla yürüyüp uçabilen ama yüzemeyen bir createBat factory'si yaz. Sonra her yeteneğin aynı state object'ini paylaştığını kanıtlamak için state'e bir energy property'si ekle ve her eylem enerjiyi bir azaltsın.

★★★ Serializable mixin'ine benzer bir Observable mixin'i yaz: on(eventName, fn) ile listener eklesin, emit(eventName, data) ile çağırsın. Listener listesini bir #listeners private alanında tut. SmartNote'a ekleyip her serialize çağrısında bir "saved" event'i yayınla.

Bu yazı boyunca object'lerin nasıl kurulduğuna odaklandık ve kodumuz hep yukarıdan aşağıya, sırayla çalıştı. Bir sonraki yazıda sıranın gerçekten nasıl belirlendiğine iniyoruz: event loop'un task ve microtask queue'larına, ekranın ne zaman çizildiğine ve setTimeout'un neden bazen beklediğinden geç çalıştığına.