← Kursa Dön
📄 Text · 20 min

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:

KategoriAmaçÖrnekler
Creational (Yaratımsal)Nesne oluşturmaSingleton, Factory, Builder
Structural (Yapısal)Sınıflar arası ilişkiAdapter, Decorator, Facade
Behavioral (Davranışsal)Nesneler arası iletişimObserver, 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ı nesne

Thread-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ı koptu

Observer 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 verildi

Burada 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

PatternProblemÇözümJava'da Nerede?
SingletonTek instance olmalıPrivate constructor + static erişimRuntime.getRuntime(), Spring beans
FactoryNesne oluşturma karmaşıkOluşturmayı ayrı sınıfa taşıCalendar.getInstance(), List.of()
BuilderÇok parametreli constructorAdım adım oluşturStringBuilder, HttpClient.newBuilder()
ObserverDeğişiklik bildirimiPublisher-subscriberSwing events, @EventListener
StrategyDeğiştirilebilir algoritmaHer algo ayrı sınıfComparator, Predicate
AdapterUyumsuz arayüzÇevirici sınıf yazInputStreamReader, 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ç.