İçeriğe geç
JavaScript

JavaScript Design Patterns: Singleton, Observer, Factory

Tarık Tunç
JavaScript Design Patterns: Singleton, Observer, Factory

JavaScript Design Patterns: Singleton, Observer, Factory

Tasarım desenleri (design patterns), yazılım geliştirmede tekrar eden sorunlara kanıtlanmış çözümler sunar. JavaScript'in esnek yapısı sayesinde klasik tasarım desenleri farklı ve yaratıcı şekillerde uygulanabilir. Bu yazıda en çok kullanılan üç deseni — Singleton, Observer ve Factory — detaylı örneklerle inceliyoruz.

1. Singleton Pattern

Singleton, bir sınıftan yalnızca bir örnek (instance) oluşturulmasını garanti eden desendir. Konfigürasyon yöneticisi, veritabanı bağlantısı veya cache gibi tek örnek gerektiren senaryolarda kullanılır.

// ES6 Class ile Singleton
class Yapilandirma {
  static #ornek = null;
  #ayarlar = {};

  constructor() {
    if (Yapilandirma.#ornek) {
      return Yapilandirma.#ornek;
    }

    this.#ayarlar = {
      apiUrl: "https://api.example.com",
      tema: "koyu",
      dil: "tr",
      debug: false
    };

    Yapilandirma.#ornek = this;
  }

  getir(anahtar) {
    return this.#ayarlar[anahtar];
  }

  ayarla(anahtar, deger) {
    this.#ayarlar[anahtar] = deger;
  }

  tumunu() {
    return { ...this.#ayarlar };
  }

  static ornekGetir() {
    if (!Yapilandirma.#ornek) {
      new Yapilandirma();
    }
    return Yapilandirma.#ornek;
  }
}

// Her zaman aynı örneği döner
const config1 = new Yapilandirma();
const config2 = new Yapilandirma();
console.log(config1 === config2); // true

config1.ayarla("tema", "açık");
console.log(config2.getir("tema")); // "açık"

// Modül Singleton (en basit yol)
// cache.js
const onbellek = new Map();

export function kaydet(anahtar, deger, ttl = 60000) {
  onbellek.set(anahtar, { deger, sonlanma: Date.now() + ttl });
}

export function getir(anahtar) {
  const kayit = onbellek.get(anahtar);
  if (!kayit) return null;
  if (Date.now() > kayit.sonlanma) {
    onbellek.delete(anahtar);
    return null;
  }
  return kayit.deger;
}
// ES modülleri zaten singleton gibi davranır — tek kez değerlendirilir

2. Observer Pattern

Observer (Gözlemci) deseni, bir nesnenin durumu değiştiğinde bağımlı nesneleri otomatik olarak bilgilendiren bir yayın-abonelik mekanizmasıdır.

// Genel EventEmitter implementasyonu
class EventEmitter {
  #olaylar = new Map();

  on(olay, dinleyici) {
    if (!this.#olaylar.has(olay)) {
      this.#olaylar.set(olay, new Set());
    }
    this.#olaylar.get(olay).add(dinleyici);

    // Abonelik iptal fonksiyonu döndür
    return () => this.off(olay, dinleyici);
  }

  once(olay, dinleyici) {
    const tekSeferlik = (...args) => {
      dinleyici(...args);
      this.off(olay, tekSeferlik);
    };
    return this.on(olay, tekSeferlik);
  }

  off(olay, dinleyici) {
    this.#olaylar.get(olay)?.delete(dinleyici);
  }

  emit(olay, ...args) {
    this.#olaylar.get(olay)?.forEach(fn => {
      try { fn(...args); } catch (e) { console.error(`Olay hatası [${olay}]:`, e); }
    });
  }
}

// Kullanım: Sepet sistemi
class Sepet extends EventEmitter {
  #urunler = [];

  ekle(urun) {
    this.#urunler.push(urun);
    this.emit("urunEklendi", urun, this.#urunler.length);
  }

  cikar(urunId) {
    const index = this.#urunler.findIndex(u => u.id === urunId);
    if (index > -1) {
      const [silinen] = this.#urunler.splice(index, 1);
      this.emit("urunCikarildi", silinen, this.#urunler.length);
    }
  }

  get toplam() {
    return this.#urunler.reduce((t, u) => t + u.fiyat, 0);
  }
}

const sepet = new Sepet();

// Dinleyiciler
sepet.on("urunEklendi", (urun, adet) => {
  console.log(`${urun.ad} eklendi. Sepette ${adet} ürün var.`);
});

sepet.on("urunEklendi", (urun) => {
  document.querySelector("#sepet-sayac").textContent = urun.ad;
});

const iptal = sepet.on("urunCikarildi", (urun) => {
  console.log(`${urun.ad} çıkarıldı`);
});

sepet.ekle({ id: 1, ad: "Laptop", fiyat: 25000 });
iptal(); // Abonelik iptal
İpucu: Observer pattern'inde dinleyici sızıntılarına dikkat edin. Artık ihtiyaç duyulmayan abonelikleri iptal edin. Özellikle SPA uygulamalarında component unmount edildiğinde dinleyicileri temizlemek kritik önem taşır.

3. Factory Pattern

Factory (Fabrika) deseni, nesne oluşturma mantığını soyutlayarak istemci kodunun somut sınıflara bağımlılığını azaltır.

// Simple Factory
class Bildirim {
  constructor(mesaj, tip) {
    this.mesaj = mesaj;
    this.tip = tip;
    this.tarih = new Date();
  }

  goster() {
    throw new Error("Alt sınıf implementasyonu gerekli");
  }
}

class EmailBildirim extends Bildirim {
  constructor(mesaj, alici) {
    super(mesaj, "email");
    this.alici = alici;
  }

  goster() {
    return `📧 ${this.alici}: ${this.mesaj}`;
  }
}

class SmsBildirim extends Bildirim {
  constructor(mesaj, telefon) {
    super(mesaj, "sms");
    this.telefon = telefon;
  }

  goster() {
    return `📱 ${this.telefon}: ${this.mesaj}`;
  }
}

class PushBildirim extends Bildirim {
  constructor(mesaj, baslik) {
    super(mesaj, "push");
    this.baslik = baslik;
  }

  goster() {
    return `🔔 ${this.baslik}: ${this.mesaj}`;
  }
}

// Factory fonksiyonu
function bildirimOlustur(tip, mesaj, piyon) {
  const fabrikalar = {
    email: () => new EmailBildirim(mesaj, piyon),
    sms: () => new SmsBildirim(mesaj, piyon),
    push: () => new PushBildirim(mesaj, piyon)
  };

  const fabrika = fabrikalar[tip];
  if (!fabrika) throw new Error(`Bilinmeyen bildirim tipi: ${tip}`);
  return fabrika();
}

// Kullanım
const bildirimler = [
  bildirimOlustur("email", "Hoş geldiniz!", "tarik@example.com"),
  bildirimOlustur("sms", "Kodunuz: 1234", "+905551234567"),
  bildirimOlustur("push", "Yeni güncelleme mevcut", "Uygulama")
];

bildirimler.forEach(b => console.log(b.goster()));

// Abstract Factory
class UIFabrika {
  butonOlustur() { throw new Error("Implement edilmeli"); }
  inputOlustur() { throw new Error("Implement edilmeli"); }
  kartOlustur() { throw new Error("Implement edilmeli"); }
}

class MaterialUIFabrika extends UIFabrika {
  butonOlustur(metin) { return { tip: "material", bileşen: "Button", metin }; }
  inputOlustur(yer) { return { tip: "material", bileşen: "TextField", yer }; }
}

class BootstrapFabrika extends UIFabrika {
  butonOlustur(metin) { return { tip: "bootstrap", bileşen: "btn", metin }; }
  inputOlustur(yer) { return { tip: "bootstrap", bileşen: "form-control", yer }; }
}
Uyarı: Tasarım desenlerini aşırı kullanmaktan kaçının (over-engineering). Basit bir if/else veya nesne literal'i yeterliyken Factory pattern uygulamak gereksiz karmaşıklık yaratır. Desen, gerçek bir sorunu çözdüğünde kullanılmalıdır.
Bilgi: JavaScript'te ES modülleri doğal olarak Singleton davranışı sergiler, closure'lar doğal olarak encapsulation sağlar ve first-class fonksiyonlar birçok deseni gereksiz kılar. Desenleri dilin doğasına uygun şekilde uygulayın.

Sonuç

Tasarım desenleri, kodun okunabilirliğini, bakımını ve genişletilebilirliğini artıran kanıtlanmış çözümlerdir. Singleton ile tekil kaynakları yönetme, Observer ile gevşek bağlı iletişim ve Factory ile esnek nesne oluşturma, JavaScript projelerinde en sık karşılaşacağınız üç desendir. Bu desenleri ihtiyaca göre uygulayarak daha modüler ve test edilebilir kod yazabilirsiniz.

Kaynaklar

İlgili Yazılar