← Kursa Dön
📄 Text · 30 min

JavaScript Design Patterns

Neden Bu Konu Önemli?

Yazılım geliştirirken belirli sorunlar tekrar tekrar karşına çıkar: bir nesnenin uygulama genelinde tek instance olması, nesnelerin birbirini haberdar etmesi, nesne oluşturma sürecinin soyutlanması... Bu sorunları her seferinde sıfırdan çözmek yerine, yılların deneyimiyle kanıtlanmış kalıplara (patterns) başvurabilirsin.

Design patterns, tekrarlayan tasarım sorunlarına denenmiş ve test edilmiş çözümlerdir. Bir inşaat mühendisinin depreme dayanıklı bina yapma tekniklerini bilmesi gibi, bir yazılımcının da design pattern'ları bilmesi gerekir.

Bu derste JavaScript'te en sık kullanılan 5 pattern'ı — Singleton, Observer, Factory, Module ve Strategy — öğreneceğiz. Her birini somut JavaScript örnekleriyle, gerçek dünya senaryolarıyla göreceğiz.


Singleton Pattern

Sorun: Bir sınıftan yalnızca bir instance olması gerekiyor. Veritabanı bağlantısı, konfigürasyon yöneticisi, logger — bu nesnelerden birden fazla olması sorun yaratır.

Çözüm: Sınıfın her zaman aynı instance'ı döndürmesini garanti et.

Analoji

Bir ülkenin cumhurbaşkanlığı makamı gibi düşün. Makam bir tane. Kim seçilirse seçilsin, makam aynı. İkinci bir cumhurbaşkanlığı makamı oluşturamazsın.

// Singleton — Class tabanlı
class Database {
  static #instance = null;
  
  #connection = null;
  #queryCount = 0;
  
  constructor(connectionString) {
    // İkinci kez new denirse hata ver veya mevcut instance'ı dön
    if (Database.#instance) {
      return Database.#instance;
    }
    
    this.connectionString = connectionString;
    Database.#instance = this;
  }
  
  static getInstance(connectionString) {
    if (!Database.#instance) {
      Database.#instance = new Database(connectionString);
    }
    return Database.#instance;
  }
  
  async connect() {
    if (this.#connection) return this.#connection;
    
    console.log(`🔌 Veritabanına bağlanılıyor: ${this.connectionString}`);
    // Simülasyon
    this.#connection = { connected: true, time: Date.now() };
    return this.#connection;
  }
  
  query(sql) {
    this.#queryCount++;
    console.log(`📊 Sorgu #${this.#queryCount}: ${sql}`);
    return []; // Simülasyon
  }
  
  get stats() {
    return { queryCount: this.#queryCount, connected: !!this.#connection };
  }
}

// Kullanım
const db1 = Database.getInstance("mongodb://localhost:27017");
const db2 = Database.getInstance("mongodb://BASKA-ADRES"); // Farklı parametre

console.log(db1 === db2); // true! Her zaman aynı instance
console.log(db2.connectionString); // "mongodb://localhost:27017" — ilk oluşturulan

db1.query("SELECT * FROM users");
db2.query("SELECT * FROM orders");

console.log(db1.stats); // { queryCount: 2, connected: false }
// db1 ve db2 aynı nesne — sorgu sayısı paylaşılıyor

Modern JavaScript'te Singleton

// ES Module ile doğal singleton — modüller sadece 1 kez çalışır!
// logger.js
class Logger {
  #logs = [];
  
  log(level, message) {
    const entry = {
      level,
      message,
      timestamp: new Date().toISOString(),
    };
    this.#logs.push(entry);
    console.log(`[${level.toUpperCase()}] ${message}`);
  }
  
  info(msg) { this.log("info", msg); }
  warn(msg) { this.log("warn", msg); }
  error(msg) { this.log("error", msg); }
  
  get history() { return [...this.#logs]; }
}

// Modül seviyesinde instance oluştur ve export et
export const logger = new Logger();
// Bu dosyayı kaç yerden import edersen et, hep aynı logger gelir

💡 İpucu: ES Module'lerin doğal singleton davranışı sayesinde, karmaşık Singleton pattern'ına çoğu zaman gerek kalmaz. Bir instance oluşturup export etmek yeterlidir.


Observer Pattern

Sorun: Bir nesne değiştiğinde, ona bağımlı olan tüm nesnelerin otomatik haberdar edilmesi gerekiyor. Her bağımlı nesneyi elle güncellemek sürdürülemez.

Çözüm: "Yayıncı-abone" (publisher-subscriber) modeli. Nesneler olaylara abone olur, olay gerçekleştiğinde otomatik haberdar edilir.

Analoji

YouTube kanal aboneliği gibi düşün. Bir kanala abone olduğunda, yeni video geldiğinde bildirim alırsın. Kanala ne kadar kişi abone olursa olsun, kanal her birine bildirim gönderir. İstediğin zaman aboneliği iptal edebilirsin.

// EventEmitter — Observer pattern'ın JavaScript implementasyonu
class EventEmitter {
  #events = new Map();
  
  // Olaya abone ol
  on(event, listener) {
    if (!this.#events.has(event)) {
      this.#events.set(event, []);
    }
    this.#events.get(event).push(listener);
    return this; // Method chaining
  }
  
  // Tek seferlik abone ol
  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args);
      this.off(event, wrapper); // Çalıştıktan sonra kendini kaldır
    };
    return this.on(event, wrapper);
  }
  
  // Aboneliği kaldır
  off(event, listener) {
    const listeners = this.#events.get(event);
    if (listeners) {
      this.#events.set(
        event,
        listeners.filter((fn) => fn !== listener)
      );
    }
    return this;
  }
  
  // Olayı tetikle — tüm aboneleri haberdar et
  emit(event, ...args) {
    const listeners = this.#events.get(event);
    if (listeners) {
      listeners.forEach((fn) => {
        try {
          fn(...args);
        } catch (err) {
          console.error(`Olay hatası [${event}]:`, err);
        }
      });
    }
    return this;
  }
  
  // Tüm abonelikleri kaldır
  removeAllListeners(event) {
    if (event) {
      this.#events.delete(event);
    } else {
      this.#events.clear();
    }
    return this;
  }
}

Gerçek Dünya: Alışveriş Sepeti

// Observer pattern ile reactive sepet
class AlisverisSepeti extends EventEmitter {
  #urunler = [];
  
  constructor() {
    super();
  }
  
  ekle(urun) {
    this.#urunler.push(urun);
    this.emit("urunEklendi", urun, this.ozet);
    this.emit("degisim", this.ozet);
  }
  
  cikar(urunId) {
    const index = this.#urunler.findIndex((u) => u.id === urunId);
    if (index !== -1) {
      const [cikarilanUrun] = this.#urunler.splice(index, 1);
      this.emit("urunCikarildi", cikarilanUrun, this.ozet);
      this.emit("degisim", this.ozet);
    }
  }
  
  get ozet() {
    return {
      urunSayisi: this.#urunler.length,
      toplam: this.#urunler.reduce((t, u) => t + u.fiyat, 0),
      urunler: [...this.#urunler],
    };
  }
}

// Kullanım — farklı bileşenler sepeti dinliyor
const sepet = new AlisverisSepeti();

// UI güncelleme
sepet.on("degisim", (ozet) => {
  console.log(`🛒 Sepet: ${ozet.urunSayisi} ürün, ${ozet.toplam} TL`);
});

// Analytics
sepet.on("urunEklendi", (urun) => {
  console.log(`📊 Analytics: "${urun.ad}" sepete eklendi`);
});

// Stok kontrolü
sepet.on("urunEklendi", (urun) => {
  console.log(`📦 Stok: "${urun.ad}" stoktan düşülüyor`);
});

// Promosyon kontrolü
sepet.on("degisim", (ozet) => {
  if (ozet.toplam > 200) {
    console.log("🎁 %10 indirim kazandınız!");
  }
});

// Ürün ekle
sepet.ekle({ id: 1, ad: "JavaScript Kitabı", fiyat: 149.90 });
// 🛒 Sepet: 1 ürün, 149.9 TL
// 📊 Analytics: "JavaScript Kitabı" sepete eklendi
// 📦 Stok: "JavaScript Kitabı" stoktan düşülüyor

sepet.ekle({ id: 2, ad: "Mekanik Klavye", fiyat: 89.90 });
// 🛒 Sepet: 2 ürün, 239.8 TL
// 📊 Analytics: "Mekanik Klavye" sepete eklendi
// 📦 Stok: "Mekanik Klavye" stoktan düşülüyor
// 🎁 %10 indirim kazandınız!

Factory Pattern

Sorun: Nesne oluşturma mantığı karmaşık veya koşullara bağlı. İstemci kodun (client) hangi sınıfın instance'ını oluşturacağını bilmemesi gerekiyor.

Çözüm: Nesne oluşturma sorumluluğunu bir "fabrika" fonksiyonuna veya sınıfına devret.

Analoji

Bir araba fabrikası düşün. "Bana bir sedan ver" diyorsun. Fabrikanın hangi parçaları kullandığı, montaj süreci — bunları bilmene gerek yok. Fabrika doğru arabayı üretip sana teslim ediyor.

// Bildirim sistemi — farklı kanallar için farklı nesneler
class EmailBildirim {
  constructor(alici, konu, icerik) {
    this.kanal = "email";
    this.alici = alici;
    this.konu = konu;
    this.icerik = icerik;
  }
  
  gonder() {
    console.log(`📧 Email gönderildi: ${this.alici} — ${this.konu}`);
    return true;
  }
}

class SmsBildirim {
  constructor(telefon, mesaj) {
    this.kanal = "sms";
    this.telefon = telefon;
    this.mesaj = mesaj;
  }
  
  gonder() {
    console.log(`📱 SMS gönderildi: ${this.telefon} — ${this.mesaj}`);
    return true;
  }
}

class PushBildirim {
  constructor(cihazId, baslik, icerik) {
    this.kanal = "push";
    this.cihazId = cihazId;
    this.baslik = baslik;
    this.icerik = icerik;
  }
  
  gonder() {
    console.log(`🔔 Push gönderildi: ${this.cihazId} — ${this.baslik}`);
    return true;
  }
}

// Factory — doğru bildirim nesnesini oluşturur
class BildirimFactory {
  static olustur(tip, ...args) {
    switch (tip) {
      case "email":
        return new EmailBildirim(...args);
      case "sms":
        return new SmsBildirim(...args);
      case "push":
        return new PushBildirim(...args);
      default:
        throw new Error(`Bilinmeyen bildirim tipi: ${tip}`);
    }
  }
}

// Kullanım — istemci hangi sınıfın kullanıldığını bilmez
const bildirimler = [
  BildirimFactory.olustur("email", "ahmet@test.com", "Hoşgeldiniz", "Merhaba!"),
  BildirimFactory.olustur("sms", "+905551234567", "Doğrulama kodunuz: 1234"),
  BildirimFactory.olustur("push", "device-abc-123", "Yeni Sipariş", "Siparişiniz onaylandı"),
];

// Polimorfizm: hepsi gonder() metoduna sahip
bildirimler.forEach((bildirim) => bildirim.gonder());
// 📧 Email gönderildi: ahmet@test.com — Hoşgeldiniz
// 📱 SMS gönderildi: +905551234567 — Doğrulama kodunuz: 1234
// 🔔 Push gönderildi: device-abc-123 — Yeni Sipariş

Registry ile Genişletilebilir Factory

// Yeni tipler eklenebilir factory
class BildirimFactory {
  static #registry = new Map();
  
  // Yeni bildirim tipi kaydet
  static kaydet(tip, sinif) {
    BildirimFactory.#registry.set(tip, sinif);
  }
  
  static olustur(tip, ...args) {
    const Sinif = BildirimFactory.#registry.get(tip);
    if (!Sinif) {
      throw new Error(`"${tip}" bildirim tipi kayıtlı değil`);
    }
    return new Sinif(...args);
  }
}

// Mevcut tipleri kaydet
BildirimFactory.kaydet("email", EmailBildirim);
BildirimFactory.kaydet("sms", SmsBildirim);
BildirimFactory.kaydet("push", PushBildirim);

// Sonradan yeni tip eklemek çok kolay!
class SlackBildirim {
  constructor(kanal, mesaj) {
    this.kanal = kanal;
    this.mesaj = mesaj;
  }
  gonder() {
    console.log(`💬 Slack: #${this.kanal} — ${this.mesaj}`);
    return true;
  }
}

BildirimFactory.kaydet("slack", SlackBildirim);

// Factory kodu değişmeden yeni tip kullanılabiliyor!
const slack = BildirimFactory.olustur("slack", "genel", "Deploy tamamlandı!");
slack.gonder(); // 💬 Slack: #genel — Deploy tamamlandı!

Module Pattern

Sorun: Kodun bölümlerini kapsüllemek — public ve private arayüzü net ayırmak.

Çözüm: Closure veya ES Module kullanarak private state oluştur, sadece public API'yi dışa aç.

IIFE ile Module Pattern (Klasik)

// IIFE (Immediately Invoked Function Expression) ile kapsülleme
const SepetModulu = (() => {
  // Private değişkenler — dışarıdan erişilemez
  let urunler = [];
  let indirimOrani = 0;
  
  // Private fonksiyonlar
  function fiyatHesapla() {
    const toplam = urunler.reduce((t, u) => t + u.fiyat * u.adet, 0);
    return toplam * (1 - indirimOrani);
  }
  
  function logYaz(mesaj) {
    console.log(`[Sepet] ${mesaj}`);
  }
  
  // Public API — return edilen nesne
  return {
    ekle(urun) {
      const mevcut = urunler.find((u) => u.id === urun.id);
      if (mevcut) {
        mevcut.adet += 1;
      } else {
        urunler.push({ ...urun, adet: 1 });
      }
      logYaz(`${urun.ad} eklendi`);
    },
    
    cikar(urunId) {
      urunler = urunler.filter((u) => u.id !== urunId);
    },
    
    indirimUygula(oran) {
      if (oran < 0 || oran > 1) {
        throw new RangeError("İndirim oranı 0-1 arasında olmalı");
      }
      indirimOrani = oran;
      logYaz(`%${(oran * 100).toFixed(0)} indirim uygulandı`);
    },
    
    get toplam() {
      return fiyatHesapla();
    },
    
    get urunSayisi() {
      return urunler.reduce((t, u) => t + u.adet, 0);
    },
    
    listele() {
      return urunler.map((u) => `${u.ad} x${u.adet}: ${u.fiyat * u.adet} TL`);
    },
  };
})();

// Kullanım
SepetModulu.ekle({ id: 1, ad: "Kitap", fiyat: 50 });
SepetModulu.ekle({ id: 2, ad: "Kalem", fiyat: 10 });
SepetModulu.indirimUygula(0.1); // %10 indirim

console.log(SepetModulu.toplam);     // 54 (60 * 0.9)
console.log(SepetModulu.listele());  // ["Kitap x1: 50 TL", "Kalem x1: 10 TL"]

// Private'lara erişim İMKÂNSIZ
// console.log(SepetModulu.urunler);     // undefined
// console.log(SepetModulu.fiyatHesapla); // undefined

ES Module ile Modern Module Pattern

// cart.js — ES Module doğal kapsülleme sağlar

// Private — export edilmemiş her şey private
let items = [];
let discount = 0;

function calculateTotal() {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return subtotal * (1 - discount);
}

// Public — sadece export edilenler dışarıdan erişilebilir
export function addItem(item) {
  const existing = items.find((i) => i.id === item.id);
  if (existing) {
    existing.qty += 1;
  } else {
    items.push({ ...item, qty: 1 });
  }
}

export function removeItem(id) {
  items = items.filter((i) => i.id !== id);
}

export function getTotal() {
  return calculateTotal();
}

export function setDiscount(rate) {
  discount = rate;
}

export function getItems() {
  return [...items]; // Kopyasını döndür — orijinali koru
}

Strategy Pattern

Sorun: Bir işlemin birden fazla yöntemi (algoritması) var ve çalışma zamanında hangisinin kullanılacağına karar vermek istiyorsun.

Çözüm: Her algoritmayı ayrı bir "strateji" olarak tanımla. Bağlamı (context) stratejiden ayır.

Analoji

Bir navigasyon uygulaması düşün. Hedefe "en kısa yol", "trafiksiz yol", "yürüyerek" gibi farklı stratejilerle gidebilirsin. Navigasyon uygulaması (context) aynı, ama rota hesaplama algoritması (strateji) farklı.

// Ödeme stratejileri
const odemeStratejileri = {
  krediKarti: {
    isim: "Kredi Kartı",
    komisyon: 0.02, // %2
    islemYap(miktar, detaylar) {
      const komisyon = miktar * this.komisyon;
      const toplam = miktar + komisyon;
      console.log(`💳 Kart: ****${detaylar.kartNo.slice(-4)}`);
      console.log(`   Tutar: ${miktar} TL + ${komisyon} TL komisyon = ${toplam} TL`);
      return { basarili: true, toplam };
    },
  },
  
  havale: {
    isim: "Havale/EFT",
    komisyon: 0,
    islemYap(miktar, detaylar) {
      console.log(`🏦 Havale: ${detaylar.iban}`);
      console.log(`   Tutar: ${miktar} TL (komisyon yok)`);
      return { basarili: true, toplam: miktar };
    },
  },
  
  kripto: {
    isim: "Kripto Para",
    komisyon: 0.01,
    islemYap(miktar, detaylar) {
      const komisyon = miktar * this.komisyon;
      const toplam = miktar + komisyon;
      console.log(`₿ Cüzdan: ${detaylar.adres.slice(0, 10)}...`);
      console.log(`   Tutar: ${miktar} TL + ${komisyon} TL ağ ücreti = ${toplam} TL`);
      return { basarili: true, toplam };
    },
  },
};

// Context — stratejiyi kullanan sınıf
class OdemeIslemi {
  #strateji;
  
  constructor(stratejiAdi) {
    this.stratejiDegistir(stratejiAdi);
  }
  
  stratejiDegistir(stratejiAdi) {
    const strateji = odemeStratejileri[stratejiAdi];
    if (!strateji) {
      throw new Error(`"${stratejiAdi}" ödeme yöntemi bulunamadı`);
    }
    this.#strateji = strateji;
  }
  
  odemeYap(miktar, detaylar) {
    console.log(`\n💰 Ödeme: ${miktar} TL — Yöntem: ${this.#strateji.isim}`);
    console.log("─".repeat(40));
    return this.#strateji.islemYap(miktar, detaylar);
  }
}

// Kullanım
const odeme = new OdemeIslemi("krediKarti");
odeme.odemeYap(500, { kartNo: "4532-XXXX-XXXX-1234" });

// Strateji değiştir — aynı context, farklı algoritma
odeme.stratejiDegistir("havale");
odeme.odemeYap(500, { iban: "TR33 0006 1005 1978 6457" });

Sıralama Stratejileri

// Farklı sıralama stratejileri
class SiralamaMotoru {
  #strateji;
  
  constructor(strateji) {
    this.#strateji = strateji;
  }
  
  set strateji(fn) {
    this.#strateji = fn;
  }
  
  sirala(veri) {
    // Orijinal veriyi değiştirmemek için kopya üzerinde çalış
    return [...veri].sort(this.#strateji);
  }
}

// Stratejiler — basit fonksiyonlar
const fiyataGoreArtan = (a, b) => a.fiyat - b.fiyat;
const fiyataGoreAzalan = (a, b) => b.fiyat - a.fiyat;
const ismeGore = (a, b) => a.isim.localeCompare(b.isim, "tr");
const puanaGore = (a, b) => b.puan - a.puan;
const tariheGore = (a, b) => new Date(b.tarih) - new Date(a.tarih);

// Kullanım
const urunler = [
  { isim: "Laptop", fiyat: 25000, puan: 4.5, tarih: "2024-01-15" },
  { isim: "Mouse", fiyat: 500, puan: 4.8, tarih: "2024-03-20" },
  { isim: "Klavye", fiyat: 1200, puan: 4.2, tarih: "2024-02-10" },
  { isim: "Monitör", fiyat: 8000, puan: 4.6, tarih: "2024-01-05" },
];

const motor = new SiralamaMotoru(fiyataGoreArtan);

console.log("Fiyata göre (artan):");
console.log(motor.sirala(urunler).map((u) => `${u.isim}: ${u.fiyat} TL`));

motor.strateji = puanaGore;
console.log("\nPuana göre (azalan):");
console.log(motor.sirala(urunler).map((u) => `${u.isim}: ⭐${u.puan}`));

Pattern'ları Birleştirmek

Gerçek uygulamalarda pattern'lar genellikle birlikte kullanılır:

// Mini uygulama: Görev Yönetici
// Singleton + Observer + Factory + Strategy

// Observer: EventEmitter (yukarda tanımlandı)
class EventBus extends EventEmitter {
  static #instance = null; // Singleton
  
  static getInstance() {
    if (!EventBus.#instance) {
      EventBus.#instance = new EventBus();
    }
    return EventBus.#instance;
  }
}

// Factory: Görev oluşturucu
class GorevFactory {
  static #sayac = 0;
  
  static olustur(tip, baslik, detaylar = {}) {
    const temel = {
      id: ++GorevFactory.#sayac,
      baslik,
      durum: "bekliyor",
      olusturma: new Date(),
    };
    
    switch (tip) {
      case "bug":
        return { ...temel, tip: "bug", oncelik: "yüksek", ...detaylar };
      case "feature":
        return { ...temel, tip: "feature", oncelik: "normal", ...detaylar };
      case "task":
        return { ...temel, tip: "task", oncelik: "düşük", ...detaylar };
      default:
        return { ...temel, tip: "genel", oncelik: "normal", ...detaylar };
    }
  }
}

// Strategy: Filtreleme stratejileri
const filtreStratejileri = {
  hepsi: () => true,
  bekleyen: (g) => g.durum === "bekliyor",
  tamamlanan: (g) => g.durum === "tamamlandı",
  yuksekOncelik: (g) => g.oncelik === "yüksek",
  buglar: (g) => g.tip === "bug",
};

// Ana uygulama
class GorevYonetici {
  #gorevler = [];
  #filtre = filtreStratejileri.hepsi;
  #bus = EventBus.getInstance();
  
  ekle(tip, baslik, detaylar) {
    const gorev = GorevFactory.olustur(tip, baslik, detaylar);
    this.#gorevler.push(gorev);
    this.#bus.emit("gorevEklendi", gorev);
    return gorev;
  }
  
  tamamla(id) {
    const gorev = this.#gorevler.find((g) => g.id === id);
    if (gorev) {
      gorev.durum = "tamamlandı";
      this.#bus.emit("gorevTamamlandi", gorev);
    }
  }
  
  filtreDegistir(stratejiAdi) {
    this.#filtre = filtreStratejileri[stratejiAdi] || filtreStratejileri.hepsi;
  }
  
  listele() {
    return this.#gorevler.filter(this.#filtre);
  }
}

// Kullanım
const bus = EventBus.getInstance();
const yonetici = new GorevYonetici();

// Observer'lar kaydol
bus.on("gorevEklendi", (g) => console.log(`➕ Yeni: [${g.tip}] ${g.baslik}`));
bus.on("gorevTamamlandi", (g) => console.log(`✅ Tamamlandı: ${g.baslik}`));

// Görevler ekle (Factory)
yonetici.ekle("bug", "Login sayfası çöküyor", { oncelik: "yüksek" });
yonetici.ekle("feature", "Karanlık mod ekle");
yonetici.ekle("task", "Dokümantasyon güncelle");

// Filtre değiştir (Strategy)
yonetici.filtreDegistir("yuksekOncelik");
console.log("\nYüksek öncelikli görevler:");
console.log(yonetici.listele());

yonetici.tamamla(1);
// ✅ Tamamlandı: Login sayfası çöküyor

Özet

Bu derste JavaScript'te en yaygın kullanılan design pattern'ları öğrendik:

  • Singleton: Bir sınıftan tek instance. Veritabanı, logger, konfigürasyon için. ES Module'ler doğal singleton sağlar.

  • Observer: Yayıncı-abone modeli. Olaylara abone ol, tetiklendiğinde haberdar ol. EventEmitter, DOM events, Redux hep bu pattern.

  • Factory: Nesne oluşturma mantığını soyutlar. İstemci hangi sınıfın kullanıldığını bilmez. Genişletilebilir registry ile yeni tipler kolayca eklenir.

  • Module: Closure veya ES Module ile kapsülleme. Private state oluştur, public API'yi dışa aç.

  • Strategy: Algoritmayı bağlamdan ayır. Çalışma zamanında strateji değiştir. Sıralama, ödeme, doğrulama gibi senaryolar.

Bu bölümle OOP ve Prototipler konusunu tamamladık! Bir sonraki bölümde TypeScript dünyasına giriş yapacağız.