Design Patterns (Tasarım Kalıpları)
Yazılım geliştirirken sürekli aynı tür problemlerle karşılaşırsın: "Bu nesneyi sadece bir tane oluşturmalıyım", "Nesne oluşturma mantığını gizlemeliyim", "Farklı algoritmaları değiştirilebilir yapmalıyım"... Bu problemler ilk senin karşına çıkmıyor — yıllardır binlerce geliştirici aynı sorunlarla uğraşmış ve kanıtlanmış çözüm şablonları geliştirmiş.
İşte design patterns (tasarım kalıpları) tam olarak bu: tekrar eden tasarım problemlerine yeniden kullanılabilir çözümler. Bu derste Java'da en çok kullanılan 6 pattern'i öğreneceğiz.
1. Design Patterns Nedir?
1994'te dört yazar ("Gang of Four" — GoF) Design Patterns: Elements of Reusable Object-Oriented Software kitabını yayınladı. Bu kitap 23 temel pattern tanımlar ve yazılım mühendisliğinin en etkili kaynaklarından biri olarak kabul edilir.
Pattern'ler üç kategoriye ayrılır:
| Kategori | Amaç | Örnekler |
|---|---|---|
| Creational (Yaratımsal) | Nesne oluşturma | Singleton, Factory, Builder |
| Structural (Yapısal) | Sınıflar arası ilişki | Adapter, Decorator, Facade |
| Behavioral (Davranışsal) | Nesneler arası iletişim | Observer, Strategy, Command |
🎯 Analoji — Mimari Planlar:
>
Design patterns'i bir mimari plan kataloğu gibi düşün. Bir ev yaparken "oturma odası-mutfak açık plan" ya da "L-tipi mutfak" gibi bilinen plan tipleri var. Her mimar bunları sıfırdan icat etmez — kanıtlanmış planları kendi ihtiyacına göre uyarlar. Design patterns da yazılımda aynı işi görür: problemi tanırsın, uygun pattern'i seçersin, kendi koduna uyarlarsın.
Önemli uyarı: Pattern'leri "her yerde kullanmalıyım" diye düşünme. Pattern, bir probleme çözüm getirir — problem yoksa pattern gereksiz karmaşıklık ekler. Önce problemi anla, sonra pattern'e başvur.
2. Singleton Pattern — Tek Nesne Garantisi
Problem
Bazı sınıflardan sadece bir tane nesne olmalı. Veritabanı bağlantı havuzu, konfigürasyon yöneticisi, logger gibi kaynaklar tüm uygulama boyunca tek bir instance üzerinden paylaşılmalı.
Çözüm
Constructor'ı private yap, nesneye static bir metod üzerinden eriş.
Temel Implementasyon
class DatabasePool {
private static DatabasePool instance;
private DatabasePool() {
// private constructor — dışarıdan new yapılamaz
System.out.println("Bağlantı havuzu oluşturuldu");
}
static DatabasePool getInstance() {
if (instance == null) {
instance = new DatabasePool();
}
return instance;
}
void query(String sql) {
System.out.println("Sorgu çalıştırılıyor: " + sql);
}
}
// Kullanım
DatabasePool pool1 = DatabasePool.getInstance();
DatabasePool pool2 = DatabasePool.getInstance();
System.out.println(pool1 == pool2); // true — aynı nesneThread-Safe Singleton
Yukarıdaki kod çoklu thread ortamında güvenli değil. İki thread aynı anda getInstance() çağırırsa iki nesne oluşabilir.
// Double-checked locking
class DatabasePool {
private static volatile DatabasePool instance;
private DatabasePool() { }
static DatabasePool getInstance() {
if (instance == null) { // 1. kontrol (hızlı)
synchronized (DatabasePool.class) {
if (instance == null) { // 2. kontrol (güvenli)
instance = new DatabasePool();
}
}
}
return instance;
}
}En İyi Yol — Enum Singleton
Java'da Singleton yapmanın en temiz ve güvenli yolu enum kullanmaktır. Thread-safe, serialization-safe ve reflection saldırılarına karşı korumalı:
enum AppConfig {
INSTANCE;
private final Map<String, String> settings = new HashMap<>();
public void set(String key, String value) {
settings.put(key, value);
}
public String get(String key) {
return settings.getOrDefault(key, "N/A");
}
}
// Kullanım
AppConfig.INSTANCE.set("db.url", "jdbc:mysql://localhost:3306/mydb");
String url = AppConfig.INSTANCE.get("db.url");Ne zaman kullanılır: Veritabanı bağlantı havuzu, konfigürasyon yöneticisi, cache, logger, thread pool gibi uygulama genelinde tek instance gereken durumlar.
⚠️ Dikkat: Singleton'ı aşırı kullanma. Global state oluşturur, test yazmayı zorlaştırır ve bağımlılıkları gizler. Spring gibi framework'lerde dependency injection ile yönetilen bean'ler zaten singleton'dır — elle Singleton pattern'i yazmana genelde gerek kalmaz.
3. Factory Method Pattern — Nesne Oluşturmayı Soyutlama
Problem
Nesne oluşturma mantığını client kodundan ayırmak istiyorsun. Belki farklı koşullara göre farklı alt sınıflar oluşturulacak, belki de oluşturma süreci karmaşık.
Çözüm
Nesne oluşturmayı ayrı bir metoda (veya sınıfa) taşı. Client hangi sınıfın oluşturulduğunu bilmez.
Simple Factory
// Ürün arayüzü
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
public void send(String message) {
System.out.println("E-posta gönderildi: " + message);
}
}
class SmsNotification implements Notification {
public void send(String message) {
System.out.println("SMS gönderildi: " + message);
}
}
class PushNotification implements Notification {
public void send(String message) {
System.out.println("Push bildirim gönderildi: " + message);
}
}
// Factory
class NotificationFactory {
static Notification create(String type) {
return switch (type.toLowerCase()) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
case "push" -> new PushNotification();
default -> throw new IllegalArgumentException("Bilinmeyen tip: " + type);
};
}
}
// Kullanım — client hangi sınıf oluşturulduğunu bilmez
Notification notif = NotificationFactory.create("email");
notif.send("Siparişiniz kargoya verildi");Abstract Factory — Fabrikaların Fabrikası
Birbirleriyle ilişkili nesne ailelerini oluşturmanız gerektiğinde:
// UI bileşen arayüzleri
interface Button { void render(); }
interface TextBox { void render(); }
// Windows ailesi
class WindowsButton implements Button {
public void render() { System.out.println("[Windows Button]"); }
}
class WindowsTextBox implements TextBox {
public void render() { System.out.println("[Windows TextBox]"); }
}
// Mac ailesi
class MacButton implements Button {
public void render() { System.out.println("[Mac Button]"); }
}
class MacTextBox implements TextBox {
public void render() { System.out.println("[Mac TextBox]"); }
}
// Abstract Factory
interface UIFactory {
Button createButton();
TextBox createTextBox();
}
class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextBox createTextBox() { return new WindowsTextBox(); }
}
class MacUIFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public TextBox createTextBox() { return new MacTextBox(); }
}
// Kullanım
UIFactory factory = System.getProperty("os.name").contains("Mac")
? new MacUIFactory()
: new WindowsUIFactory();
Button btn = factory.createButton();
TextBox txt = factory.createTextBox();
btn.render(); // Platforma göre doğru bileşen
txt.render();Ne zaman kullanılır: Nesne oluşturma mantığı karmaşıksa, koşullara göre farklı sınıflar oluşturulacaksa veya nesne ailelerini tutarlı şekilde üretmek gerekiyorsa.
4. Builder Pattern — Adım Adım Nesne İnşası
Problem
Bir sınıfın çok sayıda opsiyonel parametresi var. Constructor'a 10 parametre geçmek okunaksız ve hata yapmaya açık. Hangi parametre hangisiydi? null geçilen yerler neyi temsil ediyor?
Çözüm
Nesneyi adım adım, okunabilir bir şekilde oluştur. Her adım bir metod çağrısı, son adımda build() ile nesneyi al.
class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private final int timeout;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers;
this.body = builder.body;
this.timeout = builder.timeout;
}
@Override
public String toString() {
return method + " " + url + " (timeout=" + timeout + "ms)";
}
// Builder iç sınıf
static class Builder {
private final String url; // zorunlu
private String method = "GET"; // varsayılan
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeout = 5000;
Builder(String url) {
this.url = url;
}
Builder method(String method) {
this.method = method;
return this; // method chaining için this döndür
}
Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
Builder body(String body) {
this.body = body;
return this;
}
Builder timeout(int ms) {
this.timeout = ms;
return this;
}
HttpRequest build() {
return new HttpRequest(this);
}
}
}Kullanımı:
// Fluent API — okunabilir ve esnek
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"name\": \"Ali\"}")
.timeout(10000)
.build();
System.out.println(request);
// POST https://api.example.com/users (timeout=10000ms)
// Basit GET — opsiyonel parametreler varsayılan
HttpRequest simple = new HttpRequest.Builder("https://api.example.com/health")
.build();Bu pattern Java'da her yerde karşına çıkar: StringBuilder, Stream, HttpClient.newBuilder(), Lombok @Builder... Java kütüphanelerinin çoğu Builder pattern kullanır.
Ne zaman kullanılır: 4+ parametreli constructor'lar, opsiyonel parametreler, karmaşık nesne oluşturma süreçleri. Telescope constructor (her kombinasyon için ayrı constructor) anti-pattern'inden kaçınmak için.
5. Observer Pattern — Olay Bildirimi
Problem
Bir nesnenin durumu değiştiğinde, birden fazla nesnenin haberdar olması gerekiyor. Ama izleyen nesnelerin listesi dinamik — yeni izleyici eklenebilir, mevcut izleyici çıkarılabilir.
Çözüm
Publisher-Subscriber (yayıncı-abone) modeli. Yayıncı, durum değiştiğinde tüm abonelerini bilgilendirir.
import java.util.ArrayList;
import java.util.List;
// Observer arayüzü
interface EventListener {
void onEvent(String eventType, String data);
}
// Publisher (Subject)
class EventManager {
private final Map<String, List<EventListener>> listeners = new HashMap<>();
void subscribe(String eventType, EventListener listener) {
listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
}
void unsubscribe(String eventType, EventListener listener) {
var list = listeners.get(eventType);
if (list != null) list.remove(listener);
}
void notify(String eventType, String data) {
var list = listeners.getOrDefault(eventType, List.of());
for (EventListener listener : list) {
listener.onEvent(eventType, data);
}
}
}
// Somut observer'lar
class EmailAlert implements EventListener {
public void onEvent(String eventType, String data) {
System.out.println("📧 E-posta gönderildi: [" + eventType + "] " + data);
}
}
class LogWriter implements EventListener {
public void onEvent(String eventType, String data) {
System.out.println("📝 Log yazıldı: [" + eventType + "] " + data);
}
}
class SlackNotifier implements EventListener {
public void onEvent(String eventType, String data) {
System.out.println("💬 Slack mesajı: [" + eventType + "] " + data);
}
}Kullanımı:
// Sistemi kur
EventManager events = new EventManager();
events.subscribe("user.created", new EmailAlert());
events.subscribe("user.created", new SlackNotifier());
events.subscribe("error", new LogWriter());
events.subscribe("error", new EmailAlert());
// Olay tetikle
events.notify("user.created", "Ali hesap oluşturdu");
// 📧 E-posta gönderildi: [user.created] Ali hesap oluşturdu
// 💬 Slack mesajı: [user.created] Ali hesap oluşturdu
events.notify("error", "Veritabanı bağlantısı koptu");
// 📝 Log yazıldı: [error] Veritabanı bağlantısı koptu
// 📧 E-posta gönderildi: [error] Veritabanı bağlantısı koptuObserver pattern Java'nın her yerinde: Swing/JavaFX event handling, Spring's @EventListener, Java'nın PropertyChangeListener, reactive programming (RxJava, Project Reactor)... Hepsi bu pattern'in varyasyonları.
Ne zaman kullanılır: Bir nesnenin değişiminin birden fazla nesneyi etkilemesi gerektiğinde. Özellikle event-driven mimarilerde, UI framework'lerinde ve mesajlaşma sistemlerinde.
6. Strategy Pattern — Algoritma Değiştirme
Problem
Bir işlemi birden fazla farklı yolla yapabilirsin (farklı sıralama algoritmaları, farklı ödeme yöntemleri, farklı fiyatlandırma kuralları). Bu alternatifleri if-else zinciriyle yönetmek bakım kabusuna dönüşür.
Çözüm
Her algoritmayı ayrı bir sınıfa koy, ortak bir arayüz üzerinden kullan. Çalışma zamanında algoritmayı değiştir.
// Strateji arayüzü
interface PricingStrategy {
double calculatePrice(double basePrice, int quantity);
}
// Stratejiler
class RegularPricing implements PricingStrategy {
public double calculatePrice(double basePrice, int quantity) {
return basePrice * quantity;
}
}
class PremiumPricing implements PricingStrategy {
public double calculatePrice(double basePrice, int quantity) {
return basePrice * quantity * 0.9; // %10 indirim
}
}
class WholesalePricing implements PricingStrategy {
public double calculatePrice(double basePrice, int quantity) {
if (quantity >= 100) return basePrice * quantity * 0.7; // %30
if (quantity >= 50) return basePrice * quantity * 0.8; // %20
if (quantity >= 10) return basePrice * quantity * 0.9; // %10
return basePrice * quantity;
}
}
// Context — stratejiyi kullanan sınıf
class ShoppingCart {
private PricingStrategy strategy;
ShoppingCart(PricingStrategy strategy) {
this.strategy = strategy;
}
void setStrategy(PricingStrategy strategy) {
this.strategy = strategy; // Runtime'da değiştirilebilir
}
double checkout(double unitPrice, int quantity) {
return strategy.calculatePrice(unitPrice, quantity);
}
}Kullanımı:
ShoppingCart cart = new ShoppingCart(new RegularPricing());
System.out.println(cart.checkout(100, 5)); // 500.0
cart.setStrategy(new PremiumPricing());
System.out.println(cart.checkout(100, 5)); // 450.0
cart.setStrategy(new WholesalePricing());
System.out.println(cart.checkout(100, 50)); // 4000.0 (%20 indirim)Java 8+ ile lambda'lar sayesinde basit stratejileri daha kısa yazabilirsin:
// Functional interface olarak strateji
ShoppingCart cart = new ShoppingCart(
(price, qty) -> price * qty * 0.85 // %15 indirim
);Ne zaman kullanılır: Aynı işin birden fazla yolu olduğunda, if-else zinciri büyüdüğünde, algoritmayı runtime'da değiştirmek gerektiğinde. Sıralama, filtreleme, fiyatlandırma, doğrulama gibi senaryolar.
7. Adapter Pattern — Uyumsuz Arayüzleri Birleştirme
Problem
Var olan bir sınıf istediğin işi yapıyor ama arayüzü senin beklediğinden farklı. Sınıfı değiştiremezsin (third-party kütüphane) veya değiştirmek istemezsin.
Çözüm
Bir "adaptör" sınıf yaz. Eski arayüzü yeni arayüze çevirir.
// Mevcut sistem — JSON formatında çalışıyor
interface DataExporter {
String exportAsJson(List<String> data);
}
class JsonExporter implements DataExporter {
public String exportAsJson(List<String> data) {
return "[" + String.join(",", data.stream()
.map(s -> "\"" + s + "\"").toList()) + "]";
}
}
// Yeni gereksinim — XML formatında da dışa aktarım lazım
// Ama elimizde sadece eski bir XML kütüphanesi var:
class LegacyXmlWriter {
// Farklı arayüz — String array alıyor, void dönüyor
void writeXml(String[] items, StringBuilder output) {
output.append("<items>");
for (String item : items) {
output.append("<item>").append(item).append("</item>");
}
output.append("</items>");
}
}
// Adapter — eski XML writer'ı yeni arayüze uyarla
class XmlExporterAdapter implements DataExporter {
private final LegacyXmlWriter writer = new LegacyXmlWriter();
public String exportAsJson(List<String> data) {
// Aslında XML dönüyor ama arayüz uyumlu
// (gerçek projede method adı da değiştirilir)
StringBuilder sb = new StringBuilder();
writer.writeXml(data.toArray(new String[0]), sb);
return sb.toString();
}
}Daha temiz ve gerçekçi bir örnek:
// Hedef arayüz
interface MediaPlayer {
void play(String filename);
}
// Mevcut MP3 player
class Mp3Player implements MediaPlayer {
public void play(String filename) {
System.out.println("MP3 çalınıyor: " + filename);
}
}
// Uyumsuz third-party kütüphane
class VlcLibrary {
void playMedia(String path, String format) {
System.out.println("VLC ile " + format + " çalınıyor: " + path);
}
}
// Adapter
class VlcAdapter implements MediaPlayer {
private final VlcLibrary vlc = new VlcLibrary();
public void play(String filename) {
String format = filename.substring(filename.lastIndexOf('.') + 1);
vlc.playMedia(filename, format);
}
}
// Kullanım — client sadece MediaPlayer arayüzünü bilir
MediaPlayer player = filename.endsWith(".mp3")
? new Mp3Player()
: new VlcAdapter();
player.play("song.flac");Ne zaman kullanılır: Mevcut kodu değiştiremeden uyumsuz arayüzleri birleştirmek gerektiğinde. Third-party kütüphane entegrasyonu, legacy kod adaptasyonu gibi durumlarda.
8. Pattern'leri Birlikte Kullanma — Gerçek Dünya Örneği
Gerçek projelerde pattern'ler tek başına değil, birlikte kullanılır. İşte bir bildirim sistemi örneği:
// Strategy: farklı gönderim kanalları
interface NotificationChannel {
void send(String recipient, String message);
}
class EmailChannel implements NotificationChannel {
public void send(String to, String msg) {
System.out.println("📧 " + to + ": " + msg);
}
}
class SmsChannel implements NotificationChannel {
public void send(String to, String msg) {
System.out.println("📱 " + to + ": " + msg);
}
}
// Builder: bildirim oluşturma
class Notification {
private final String recipient;
private final String subject;
private final String body;
private final NotificationChannel channel;
private Notification(Builder b) {
this.recipient = b.recipient;
this.subject = b.subject;
this.body = b.body;
this.channel = b.channel;
}
void send() { channel.send(recipient, subject + ": " + body); }
static class Builder {
private String recipient, subject, body;
private NotificationChannel channel = new EmailChannel();
Builder to(String r) { recipient = r; return this; }
Builder subject(String s) { subject = s; return this; }
Builder body(String b) { body = b; return this; }
Builder via(NotificationChannel c) { channel = c; return this; }
Notification build() { return new Notification(this); }
}
}
// Singleton: bildirim servisi
class NotificationService {
private static final NotificationService INSTANCE = new NotificationService();
private NotificationService() {}
static NotificationService getInstance() { return INSTANCE; }
void dispatch(Notification notification) {
notification.send();
}
}Kullanım:
Notification alert = new Notification.Builder()
.to("ali@example.com")
.subject("Sipariş Onayı")
.body("Siparişiniz kargoya verildi")
.via(new SmsChannel())
.build();
NotificationService.getInstance().dispatch(alert);
// 📱 ali@example.com: Sipariş Onayı: Siparişiniz kargoya verildiBurada Builder (okunabilir nesne oluşturma) + Strategy (kanal seçimi) + Singleton (servis erişimi) birlikte çalışıyor.
9. Anti-Patterns — Kaçınılması Gerekenler
💡 İpucu: Pattern bilmek kadar ne zaman kullanmayacağını bilmek de önemli. İşte yaygın tuzaklar:
Over-engineering: 3 satırlık bir iş için Factory + Builder + Singleton + Observer kurmak. Basit tutabiliyorsan basit tut.
Pattern-itis: "Bu projede Strategy kullanmalıyım çünkü havalı" — problem yoksa pattern yok.
God Singleton: Her şeyi Singleton yapıp global state cehennemi oluşturmak. Test edilemez, bakımı zor kod demek.
Premature Abstraction: Henüz tek bir implementasyon varken interface + factory oluşturmak. İkinci implementasyona ihtiyaç duyduğunda abstract'la — YAGNI prensibi.
10. Hızlı Referans Tablosu
| Pattern | Problem | Çözüm | Java'da Nerede? |
|---|---|---|---|
| Singleton | Tek instance olmalı | Private constructor + static erişim | Runtime.getRuntime(), Spring beans |
| Factory | Nesne oluşturma karmaşık | Oluşturmayı ayrı sınıfa taşı | Calendar.getInstance(), List.of() |
| Builder | Çok parametreli constructor | Adım adım oluştur | StringBuilder, HttpClient.newBuilder() |
| Observer | Değişiklik bildirimi | Publisher-subscriber | Swing events, @EventListener |
| Strategy | Değiştirilebilir algoritma | Her algo ayrı sınıf | Comparator, Predicate |
| Adapter | Uyumsuz arayüz | Çevirici sınıf yaz | InputStreamReader, Arrays.asList() |
Özet
Design patterns, tekrar eden yazılım tasarım problemlerine kanıtlanmış çözümlerdir. Creational, Structural ve Behavioral olmak üzere üç kategoride sınıflandırılır.
Singleton tek instance garantisi verir; Java'da enum Singleton en güvenli yoldur. Aşırı kullanımdan kaçın — global state test edilebilirliği azaltır.
Factory nesne oluşturma mantığını client'tan ayırır. Abstract Factory ise birbiriyle ilişkili nesne ailelerini tutarlı şekilde üretir.
Builder çok parametreli nesneleri okunabilir, fluent API ile adım adım oluşturur. Java kütüphanelerinin büyük çoğunluğu bu pattern'i kullanır.
Observer bir nesnedeki değişikliği birden fazla dinleyiciye bildirir (event-driven mimari). Strategy aynı işin farklı algoritmalarını runtime'da değiştirilebilir yapar.
Pattern'leri problem olduğunda kullan, "havalı olduğu için" değil. Basitlik her zaman gereksiz soyutlamadan üstündür — önce problemi anla, sonra pattern seç.
AI Asistan
Sorularını yanıtlamaya hazır