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ıyorModern 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); // undefinedES 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.
AI Asistan
Sorularını yanıtlamaya hazır