İçeriğe geç

Builder Design Pattern: Karmaşık Nesneleri Adım Adım İnşa Etmek

T
Tolgahan
· · 18 dk okuma · 179 görüntülenme

Builder Design Pattern: Karmaşık Nesneleri Adım Adım İnşa Etmek

Bir hamburger siparişi verdiğinizi düşünün. Kasiyere şöyle diyorsunuz: "Çift köfte olsun, cheddar peyniri ekleyin, turşu olmasın, sos olarak mayo ve barbekü istiyorum, ekmeği tam buğday olsun." Kasiyerin karşısında bir menü yok — sizin adım adım tarif ettiğiniz spesifikasyonlarla hamburgeri oluşturuyor. İşte Builder Design Pattern tam olarak bu. Karmaşık bir nesneyi, hangi parçaları istediğinizi tek tek belirterek adım adım inşa etmeniz.

Peki neden böyle bir şeye ihtiyaç duyalım? Çünkü yazılım geliştirirken sürekli çok parametreli nesneler oluşturuyoruz. Bir kullanıcı profili düşünün: isim, soyisim, e-posta, telefon, adres, doğum tarihi, profil fotoğrafı, biyografi, tercih edilen dil, bildirim ayarları... Bu nesneyi oluşturmak için ya parametreleri tek tek constructor'a (yapıcı metot) geçirirsiniz — ki bu bir kabus — ya da setter'lar kullanırsınız — ki bu da nesnenin tutarlılığını bozar. Builder Pattern, bu iki kötü seçenek arasında üçüncü ve doğru bir yol sunar.

Bu yazıda Builder Pattern'ı en ince detayına kadar öğreneceksiniz: hangi problemi çözer, nasıl implemente edilir, gerçek projelerde nasıl kullanılır, hangi hatalardan kaçınmanız gerekir. Yazıyı bitirdiğinizde bu pattern'ı ne zaman kullanacağınızı ve ne zaman kullanmamanız gerektiğini kesin olarak bileceksiniz.


Problem: Telescoping Constructor Antipattern

Builder Pattern'ı anlamak için önce çözdüğü problemi görmemiz lazım. Diyelim ki bir Pizza sınıfı yazıyorsunuz:

class Pizza {
    private String size;       // Zorunlu
    private String crust;      // Zorunlu
    private boolean cheese;
    private boolean pepperoni;
    private boolean mushroom;
    private boolean olive;
    private boolean onion;
    private boolean corn;

    // 2 parametreli constructor
    Pizza(String size, String crust) {
        this(size, crust, false, false, false, false, false, false);
    }

    // 3 parametreli constructor
    Pizza(String size, String crust, boolean cheese) {
        this(size, crust, cheese, false, false, false, false, false);
    }

    // 4 parametreli constructor
    Pizza(String size, String crust, boolean cheese, boolean pepperoni) {
        this(size, crust, cheese, pepperoni, false, false, false, false);
    }

    // ... ve tam versiyonu
    Pizza(String size, String crust, boolean cheese, boolean pepperoni,
          boolean mushroom, boolean olive, boolean onion, boolean corn) {
        this.size = size;
        this.crust = crust;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.mushroom = mushroom;
        this.olive = olive;
        this.onion = onion;
        this.corn = corn;
    }
}

Buna "Telescoping Constructor" (iç içe geçen yapıcı metotlar) antipattern denir. Her yeni parametre için bir constructor daha ekliyorsunuz. Kullanım tarafında ise durum tam bir felaket:

// Hangi true hangi malzeme? Kim bilir!
Pizza pizza = new Pizza("Large", "Thin", true, false, true, false, true, false);

Bu kodu okuyan biri, beşinci true'nun ne anlama geldiğini anlamak için sınıfın kaynak koduna gitmek zorunda. Parametrelerin sırası yanlış girildiyse derleyici (compiler) hata vermez çünkü hepsi aynı tipte — boolean. Bu tür hatalar production'da ortaya çıkar ve debug etmesi acı vericidir.

⚠️ Dikkat: Aynı tipte birden fazla parametre alan constructor'lar, parametrelerin karışması riskini taşır. boolean, boolean, boolean gördüğünüzde bir alarm zili çalmalı — bu kodun bakımı ve okunması zordur.

İkinci bir yaklaşım setter'lar kullanmak:

Pizza pizza = new Pizza();
pizza.setSize("Large");
pizza.setCrust("Thin");
pizza.setCheese(true);
pizza.setMushroom(true);
pizza.setOnion(true);

Bu daha okunabilir, ama ciddi bir problemi var: nesneyi tutarsız (inconsistent) durumda bırakabilirsiniz. setSize() çağırmadan önce nesne eksiktir. Setter kullanan nesneler ayrıca değiştirilemez (immutable) olamaz — herhangi bir yerden herhangi bir zamanda setter çağrılıp nesne değiştirilebilir. Çok iş parçacıklı (multi-threaded) ortamlarda bu büyük bir sorun.


Builder Pattern Nedir?

🎯 Analoji: Bir ev inşa ettiğinizi düşünün. Müteahhite (Builder) "3 oda olsun, bahçe olsun, garaj olmasın, çatı kiremit olsun" diyorsunuz. Müteahhit bu adımları sırayla uyguluyor ve sonunda size tamamlanmış evi teslim ediyor. Siz inşaat detaylarıyla uğraşmıyorsunuz — ne istediğinizi söylüyorsunuz, Builder gerisini hallediyor.

Builder Pattern, Gang of Four (GoF) kitabındaki 23 klasik tasarım kalıbından (design pattern) biridir ve "Creational Patterns" (oluşturucu kalıplar) kategorisinde yer alır. Temel fikir şu: karmaşık bir nesnenin oluşturulma sürecini, nesnenin kendisinden ayır. Böylece aynı oluşturma süreci farklı temsiller (representations) üretebilir.

Pratikte Builder Pattern şu şekilde çalışır:

  1. Bir Builder nesnesi yaratırsınız

  2. Hangi özellikleri istediğinizi method call'larla belirtirsiniz

  3. Her method call, Builder'ın kendisini döndürür (method chaining / zincirleme metot çağrısı)

  4. Son olarak build() çağırarak tamamlanmış nesneyi alırsınız

Sonuç: okunabilir, güvenli, immutable nesneler.


Java'da Builder Pattern: Klasik Uygulama

Joshua Bloch'un Effective Java kitabında popülerleştirdiği yaklaşım, Java dünyasında standart haline geldi. Builder, oluşturulacak sınıfın içinde static inner class olarak tanımlanır:

class Pizza {
    // Tüm alanlar final — nesne immutable
    private final String size;
    private final String crust;
    private final boolean cheese;
    private final boolean pepperoni;
    private final boolean mushroom;
    private final boolean olive;
    private final boolean onion;
    private final boolean corn;

    // Private constructor — dışarıdan doğrudan oluşturulamaz
    private Pizza(Builder builder) {
        this.size = builder.size;
        this.crust = builder.crust;
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.mushroom = builder.mushroom;
        this.olive = builder.olive;
        this.onion = builder.onion;
        this.corn = builder.corn;
    }

    // Getter'lar (setter yok — immutable!)
    public String getSize() { return size; }
    public String getCrust() { return crust; }
    public boolean hasCheese() { return cheese; }
    public boolean hasPepperoni() { return pepperoni; }
    public boolean hasMushroom() { return mushroom; }
    public boolean hasOlive() { return olive; }
    public boolean hasOnion() { return onion; }
    public boolean hasCorn() { return corn; }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(size).append(" pizza, ").append(crust).append(" hamur");
        if (cheese) sb.append(", peynirli");
        if (pepperoni) sb.append(", pepperonili");
        if (mushroom) sb.append(", mantarli");
        if (olive) sb.append(", zeytinli");
        if (onion) sb.append(", soganli");
        if (corn) sb.append(", misirli");
        return sb.toString();
    }

    // Static inner Builder sınıfı
    static class Builder {
        // Zorunlu parametreler
        private final String size;
        private final String crust;

        // Opsiyonel parametreler — varsayılan değerlerle
        private boolean cheese = false;
        private boolean pepperoni = false;
        private boolean mushroom = false;
        private boolean olive = false;
        private boolean onion = false;
        private boolean corn = false;

        // Builder constructor'ı — sadece zorunlu parametreler
        Builder(String size, String crust) {
            this.size = size;
            this.crust = crust;
        }

        // Her metot Builder'ın kendisini döndürür (method chaining)
        Builder cheese(boolean value) {
            this.cheese = value;
            return this;
        }

        Builder pepperoni(boolean value) {
            this.pepperoni = value;
            return this;
        }

        Builder mushroom(boolean value) {
            this.mushroom = value;
            return this;
        }

        Builder olive(boolean value) {
            this.olive = value;
            return this;
        }

        Builder onion(boolean value) {
            this.onion = value;
            return this;
        }

        Builder corn(boolean value) {
            this.corn = value;
            return this;
        }

        // build() metodu — Pizza nesnesini oluştur ve döndür
        Pizza build() {
            return new Pizza(this);
        }
    }

    // Test edelim
    public static void main(String[] args) {
        Pizza pizza = new Pizza.Builder("Large", "Thin")
                .cheese(true)
                .mushroom(true)
                .onion(true)
                .build();

        System.out.println(pizza);
        // Çıktı: Large pizza, Thin hamur, peynirli, mantarli, soganli
    }
}

Bu kodu çalıştırdığınızda farkı hemen görürsünüz. Hangi malzemenin ne olduğu ismiyle belirtiliyor. Sıralama önemli değil — .onion(true).cheese(true) yazabilirsiniz, sonuç aynı. Nesne oluşturulduktan sonra değiştirilemez çünkü setter yok ve tüm alanlar final.

Bu Yaklaşımın Avantajları

  • Okunabilirlik: new Pizza("L", "T", true, false, true, false, true, false) yerine .cheese(true).mushroom(true) yazıyorsunuz

  • Immutability (değiştirilemezlik): Nesne bir kez oluşturulunca dondurulmuş — thread-safe

  • Zorunlu vs opsiyonel ayrımı: Constructor'da zorunlu, method chain'de opsiyonel

  • Validation (doğrulama): build() içinde tüm iş kurallarını kontrol edebilirsiniz


Validasyonlu Builder: İş Kurallarını Zorla

Builder Pattern'ın en güçlü yanlarından biri, build() metodu içinde doğrulama yapabilmenizdir. Böylece geçersiz durumda bir nesne asla oluşturulamaz. Gerçek dünyada bunu sürekli kullanırsınız:

class HttpRequest {
    private final String method;
    private final String url;
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutMs;

    private HttpRequest(Builder builder) {
        this.method = builder.method;
        this.url = builder.url;
        this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
    }

    public String getMethod() { return method; }
    public String getUrl() { return url; }
    public Map<String, String> getHeaders() { return headers; }
    public String getBody() { return body; }
    public int getTimeoutMs() { return timeoutMs; }

    @Override
    public String toString() {
        return method + " " + url + " (timeout: " + timeoutMs + "ms, headers: "
               + headers.size() + ", body: " + (body != null ? body.length() + " chars" : "none") + ")";
    }

    static class Builder {
        private String method = "GET";
        private String url;
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeoutMs = 5000; // varsayılan 5 saniye

        Builder(String url) {
            this.url = url;
        }

        Builder method(String method) {
            this.method = method;
            return this;
        }

        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.timeoutMs = ms;
            return this;
        }

        HttpRequest build() {
            // Validasyon — geçersiz nesne oluşturulamaz!
            if (url == null || url.isBlank()) {
                throw new IllegalStateException("URL boş olamaz");
            }
            if (!url.startsWith("http://") && !url.startsWith("https://")) {
                throw new IllegalStateException("URL http:// veya https:// ile başlamalı");
            }
            if (("POST".equals(method) || "PUT".equals(method)) && body == null) {
                throw new IllegalStateException(method + " isteği için body zorunludur");
            }
            if ("GET".equals(method) && body != null) {
                throw new IllegalStateException("GET isteğinde body olmamalı");
            }
            if (timeoutMs <= 0) {
                throw new IllegalStateException("Timeout pozitif olmalı");
            }
            return new HttpRequest(this);
        }
    }

    public static void main(String[] args) {
        // Geçerli bir POST isteği
        HttpRequest postRequest = new HttpRequest.Builder("https://api.example.com/users")
                .method("POST")
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer token123")
                .body("{\"name\": \"Tolgahan\", \"role\": \"developer\"}")
                .timeout(10000)
                .build();

        System.out.println(postRequest);
        // Çıktı: POST https://api.example.com/users (timeout: 10000ms, headers: 2, body: 42 chars)

        // Basit bir GET isteği — varsayılan değerler kullanılır
        HttpRequest getRequest = new HttpRequest.Builder("https://api.example.com/users/1")
                .header("Accept", "application/json")
                .build();

        System.out.println(getRequest);
        // Çıktı: GET https://api.example.com/users/1 (timeout: 5000ms, headers: 1, body: none)

        // Bu hata verir — POST için body zorunlu
        try {
            HttpRequest badRequest = new HttpRequest.Builder("https://api.example.com/users")
                    .method("POST")
                    .build(); // IllegalStateException!
        } catch (IllegalStateException e) {
            System.out.println("Hata yakalandı: " + e.getMessage());
            // Çıktı: Hata yakalandı: POST isteği için body zorunludur
        }
    }
}

Dikkat edin: build() metodu içinde tüm iş kurallarını kontrol ediyoruz. POST ve PUT isteklerinde body zorunlu, GET isteklerinde body olmamalı, URL geçerli bir formatta olmalı. Bu kontroller sayesinde geçersiz bir HttpRequest nesnesi oluşturmak fiziksel olarak imkansız. Hatalı kullanım derleme anında (compile time) değil ama en azından nesne oluşturulma anında — yani çok erken bir aşamada — yakalanır.

💡 İpucu: Collections.unmodifiableMap() ile header map'ini kopyalayıp değiştirilemez hale getirdik. Bu, "defensive copy" (savunmacı kopya) tekniğidir ve Builder'ın döndürdüğü nesnenin gerçekten immutable olmasını garanti eder. Orijinal map'e referans tutarsanız, dışarıdan değiştirilme riski doğar.


Gerçek Dünyada Builder: Nerede Karşılaşırsınız?

Builder Pattern teorik bir egzersiz değil — Java ekosisteminde her yerde karşınıza çıkar. Bu örnekleri tanımak, pattern'ı ne zaman uygulamanız gerektiğini anlamanıza yardımcı olacak.

StringBuilder — Java'nın Kendi Builder'ı

Java'daki en bilinen Builder aslında StringBuilder'dır:

String result = new StringBuilder()
        .append("Merhaba")
        .append(" ")
        .append("Dünya")
        .append("!")
        .toString(); // build() yerine toString()

System.out.println(result);
// Çıktı: Merhaba Dünya!

Spring Boot'ta Builder Kullanımı

Spring Boot dünyasında Builder Pattern her yerdedir. ResponseEntity, UriComponentsBuilder, MockMvcRequestBuilders — hepsi Builder kullanır:

// Spring ResponseEntity — bir Builder döndürür
ResponseEntity<String> response = ResponseEntity
        .status(HttpStatus.CREATED)
        .header("X-Custom-Header", "custom-value")
        .body("Kullanıcı başarıyla oluşturuldu");

// Spring UriComponentsBuilder — URL oluşturmak için Builder
String url = UriComponentsBuilder
        .fromHttpUrl("https://api.example.com")
        .path("/users")
        .queryParam("page", 1)
        .queryParam("size", 20)
        .queryParam("sort", "name,asc")
        .toUriString();

// url = "https://api.example.com/users?page=1&size=20&sort=name,asc"

Lombok @Builder — Sıfır Boilerplate

Gerçek projelerde her sınıf için elle Builder yazmak zahmetli olabilir. Lombok kütüphanesi @Builder anotasyonu ile bunu otomatikleştirir:

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Getter
@Builder
@ToString
class UserProfile {
    private final String firstName;
    private final String lastName;
    private final String email;
    @Builder.Default private final String language = "tr";
    @Builder.Default private final boolean emailNotifications = true;
    @Builder.Default private final boolean smsNotifications = false;
}

// Kullanımı — aynı fluent API
class Main {
    public static void main(String[] args) {
        UserProfile profile = UserProfile.builder()
                .firstName("Tolgahan")
                .lastName("Aydın")
                .email("tolgahan@example.com")
                .smsNotifications(true)
                .build();

        System.out.println(profile);
        // Çıktı: UserProfile(firstName=Tolgahan, lastName=Aydın,
        //         email=tolgahan@example.com, language=tr,
        //         emailNotifications=true, smsNotifications=true)
    }
}

Lombok, derleme zamanında Builder sınıfını otomatik olarak üretir. @Builder.Default ile varsayılan değer atayabilirsiniz. Sonuç olarak elinizde aynı güvenli, okunabilir, immutable Builder Pattern var — ama sıfır satır boilerplate kod yazmışsınız.


Builder vs Constructor vs Factory: Ne Zaman Hangisi?

Bu üçü sık karıştırılır. Hepsinin farklı güçlü yanları var:

Constructor (yapıcı metot) kullanın eğer:

  • Parametreler az (1-3 adet)

  • Tüm parametreler zorunlu

  • Parametre tipleri birbirinden farklı (karışma riski yok)

Factory Method kullanın eğer:

  • Farklı alt tiplerin (subclass) hangisinin oluşturulacağına karar vermek gerekiyor

  • Oluşturma mantığı karmaşık ama parametre sayısı az

  • İsimlendirilmiş constructor'lara ihtiyaç var (createFromJson(), createEmpty())

Builder Pattern kullanın eğer:

  • Çok sayıda parametre var (4+)

  • Parametrelerin çoğu opsiyonel

  • Aynı tipte birden fazla parametre var (boolean, String, int)

  • Nesnenin immutable olması gerekiyor

  • Oluşturma sırasında validasyon gerekiyor

💡 İpucu: Bir kural olarak — eğer constructor'ınızda 4'ten fazla parametre varsa veya birden fazla boolean parametre varsa, Builder Pattern kullanmayı düşünün. Joshua Bloch, Effective Java'da bu eşiği açıkça belirtir.


Yaygın Hatalar: Herkesi Düşüren Tuzaklar

1. Builder'ı Mutable (Değiştirilebilir) Bırakmak

En sık yapılan hata, Builder ile oluşturulan nesnenin aslında immutable olmamasıdır:

// ❌ YANLIŞ — Setter var, immutable değil!
class User {
    private String name;
    private String email;

    // Builder var ama setter da var — anlamsız
    public void setName(String name) { this.name = name; }
    public void setEmail(String email) { this.email = email; }

    static class Builder {
        // ...
    }
}

Builder Pattern'ın temel amacı immutable nesne üretmektir. Setter eklediğiniz an bu amacı yok etmiş olursunuz. Alanları `final` yapın, setter yazmayın, constructor'ı `private` tutun.

2. build() İçinde Validasyon Yapmamak

Builder'ın en güçlü silahını kullanmamak:

// ❌ YANLIŞ — Validasyon yok, geçersiz nesne oluşturulabilir
Pizza build() {
    return new Pizza(this); // e-posta null olsa da kabul eder
}

// ✅ DOĞRU — build() içinde tüm kuralları kontrol et
Pizza build() {
    if (size == null) {
        throw new IllegalStateException("Size zorunludur");
    }
    if (crust == null) {
        throw new IllegalStateException("Crust zorunludur");
    }
    return new Pizza(this);
}

Validasyonu build() içinde yapın, setter metotlarında değil. Neden? Çünkü bazı validasyonlar birden fazla alana bağlıdır — "eğer A seçildiyse B de olmalı" gibi kurallar ancak tüm alanlar set edildikten sonra kontrol edilebilir.

3. Builder Nesnesini Yeniden Kullanmak

// ❌ TEHLİKELİ — Aynı Builder'ı tekrar kullanmak
Pizza.Builder builder = new Pizza.Builder("Medium", "Thick");
builder.cheese(true);

Pizza pizza1 = builder.build();       // peynirli pizza
builder.pepperoni(true);
Pizza pizza2 = builder.build();       // peynirli VE pepperonili pizza

// pizza1 hâlâ sadece peynirli — ama bu davranış kafa karıştırıcı
// ve bazı Builder implementasyonlarında pizza1 de etkilenebilir!

Builder'ı tek seferlik kullanın. Bir build() çağrısından sonra aynı Builder nesnesini değiştirip tekrar build() yapmak, beklenmedik davranışlara yol açabilir. Her yeni nesne için yeni bir Builder oluşturun.

4. Defensive Copy Yapmamak

// ❌ YANLIŞ — Dışarıdan gelen koleksiyonu doğrudan atamak
private HttpRequest(Builder builder) {
    this.headers = builder.headers; // Tehlikeli! Builder'daki map değişirse bu da değişir
}

// ✅ DOĞRU — Defensive copy yap
private HttpRequest(Builder builder) {
    this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
}

Koleksiyon (List, Map, Set) alanlarını Builder'dan doğrudan kopyalarsanız, Builder tarafındaki değişiklikler oluşturulmuş nesneyi de etkiler. Her zaman yeni bir kopya oluşturun ve mümkünse Collections.unmodifiable*() ile sarmalayın.


Best Practices: Profesyonellerin Bildiği İnce Noktalar

1. Zorunlu Parametreleri Builder Constructor'ına Alın

// Zorunlu parametreler Builder constructor'ında
// Opsiyonel parametreler method chain'de
Builder(String url) {          // URL zorunlu — constructor'da
    this.url = url;
}
Builder timeout(int ms) {     // Timeout opsiyonel — method'da
    this.timeoutMs = ms;
    return this;
}

Bu yaklaşım, zorunlu parametreler olmadan build() çağrılmasını fiziksel olarak engeller. Derleyici düzeyinde güvenlik sağlar.

2. Builder Metotlarını Anlamlı İsimlendirin

// ❌ Boolean parametreli — ne anlama geliyor?
.notifications(true)

// ✅ İsim her şeyi anlatıyor
.withEmailNotifications()
.withoutSmsNotifications()

Boolean parametre yerine withX() ve withoutX() metotları yazmak, kodu çok daha okunabilir kılar.

3. Generic Builder ile Kalıtım (Inheritance) Desteği

Eğer Builder'lı sınıflarınız kalıtım kullanıyorsa, "Curiously Recurring Generic Pattern" uygulayabilirsiniz:

abstract class AbstractBuilder<T extends AbstractBuilder<T>> {
    protected String name;

    @SuppressWarnings("unchecked")
    protected T self() {
        return (T) this;
    }

    public T name(String name) {
        this.name = name;
        return self();
    }
}

Bu teknik, alt sınıf Builder'larının üst sınıf metotlarını çağırdıktan sonra da method chaining'e devam edebilmesini sağlar.

4. toBuilder() Metodu ile Kopyalama

Bazı durumlarda mevcut bir nesneyi alıp, birkaç alanını değiştirip yeni bir nesne oluşturmak istersiniz. Bunun için toBuilder() metodu ekleyin:

class Pizza {
    // ... mevcut kod ...

    // Mevcut nesneden Builder oluştur
    Builder toBuilder() {
        return new Builder(this.size, this.crust)
                .cheese(this.cheese)
                .pepperoni(this.pepperoni)
                .mushroom(this.mushroom)
                .olive(this.olive)
                .onion(this.onion)
                .corn(this.corn);
    }
}

// Kullanımı
Pizza original = new Pizza.Builder("Large", "Thin").cheese(true).build();
Pizza modified = original.toBuilder().pepperoni(true).onion(true).build();
// original değişmedi — yeni bir nesne oluşturuldu

Lombok'ta bu özellik @Builder(toBuilder = true) ile otomatik gelir.


Bütünleşik Gerçek Dünya Örneği: Veritabanı Sorgu Builder'ı

Şimdi tüm öğrendiklerimizi birleştiren kapsamlı bir örnek yapalım. Bir SQL SELECT sorgusu oluşturan Builder yazacağız. Bu örnek; method chaining, validasyon, defensive copy, koleksiyon yönetimi ve toString() ile çıktı üretmeyi bir arada gösteriyor:

import java.util.*;
import java.util.stream.Collectors;

class SelectQuery {
    private final String table;
    private final List<String> columns;
    private final List<String> conditions;
    private final String orderBy;
    private final String orderDirection;
    private final Integer limit;
    private final Integer offset;

    private SelectQuery(Builder builder) {
        this.table = builder.table;
        this.columns = List.copyOf(builder.columns);       // Immutable kopya
        this.conditions = List.copyOf(builder.conditions);   // Immutable kopya
        this.orderBy = builder.orderBy;
        this.orderDirection = builder.orderDirection;
        this.limit = builder.limit;
        this.offset = builder.offset;
    }

    // SQL sorgusunu String olarak üret
    public String toSQL() {
        StringBuilder sql = new StringBuilder("SELECT ");

        // Kolonlar
        if (columns.isEmpty()) {
            sql.append("*");
        } else {
            sql.append(String.join(", ", columns));
        }

        // Tablo
        sql.append(" FROM ").append(table);

        // WHERE koşulları
        if (!conditions.isEmpty()) {
            sql.append(" WHERE ");
            sql.append(String.join(" AND ", conditions));
        }

        // ORDER BY
        if (orderBy != null) {
            sql.append(" ORDER BY ").append(orderBy);
            sql.append(" ").append(orderDirection);
        }

        // LIMIT ve OFFSET
        if (limit != null) {
            sql.append(" LIMIT ").append(limit);
        }
        if (offset != null) {
            sql.append(" OFFSET ").append(offset);
        }

        return sql.toString();
    }

    @Override
    public String toString() {
        return toSQL();
    }

    static class Builder {
        private final String table;
        private List<String> columns = new ArrayList<>();
        private List<String> conditions = new ArrayList<>();
        private String orderBy;
        private String orderDirection = "ASC";
        private Integer limit;
        private Integer offset;

        Builder(String table) {
            this.table = table;
        }

        Builder select(String... cols) {
            columns.addAll(Arrays.asList(cols));
            return this;
        }

        Builder where(String condition) {
            conditions.add(condition);
            return this;
        }

        Builder orderBy(String column) {
            this.orderBy = column;
            this.orderDirection = "ASC";
            return this;
        }

        Builder orderByDesc(String column) {
            this.orderBy = column;
            this.orderDirection = "DESC";
            return this;
        }

        Builder limit(int limit) {
            this.limit = limit;
            return this;
        }

        Builder offset(int offset) {
            this.offset = offset;
            return this;
        }

        SelectQuery build() {
            // Validasyonlar
            if (table == null || table.isBlank()) {
                throw new IllegalStateException("Tablo adı zorunludur");
            }
            if (offset != null && limit == null) {
                throw new IllegalStateException("OFFSET kullanmak için LIMIT belirtilmelidir");
            }
            if (limit != null && limit <= 0) {
                throw new IllegalStateException("LIMIT pozitif bir sayı olmalıdır");
            }
            return new SelectQuery(this);
        }
    }

    public static void main(String[] args) {
        // Basit sorgu
        SelectQuery simple = new SelectQuery.Builder("users")
                .build();
        System.out.println(simple);
        // Çıktı: SELECT * FROM users

        // Filtrelenmiş ve sıralı sorgu
        SelectQuery filtered = new SelectQuery.Builder("products")
                .select("id", "name", "price", "stock")
                .where("price > 100")
                .where("stock > 0")
                .where("category = 'electronics'")
                .orderByDesc("price")
                .limit(20)
                .offset(40)
                .build();
        System.out.println(filtered);
        // Çıktı: SELECT id, name, price, stock FROM products
        //        WHERE price > 100 AND stock > 0 AND category = 'electronics'
        //        ORDER BY price DESC LIMIT 20 OFFSET 40

        // Validasyon testi — OFFSET var, LIMIT yok
        try {
            SelectQuery bad = new SelectQuery.Builder("orders")
                    .offset(10)
                    .build();
        } catch (IllegalStateException e) {
            System.out.println("Hata: " + e.getMessage());
            // Çıktı: Hata: OFFSET kullanmak için LIMIT belirtilmelidir
        }
    }
}

Bu örnekte Builder Pattern'ın tüm güçlü yanlarını görüyorsunuz:

  • Okunabilirlik: Sorguyu oluştururken ne yaptığınız kristal netliğinde

  • Esneklik: Sadece ihtiyacınız olan parçaları ekliyorsunuz, geri kalanı varsayılan

  • Güvenlik: build() içindeki validasyon, geçersiz SQL oluşturulmasını engelliyor

  • Immutability: Oluşturulan SelectQuery nesnesi değiştirilemez — List.copyOf() ile koleksiyonlar da korunmuş

  • Method chaining: Zincirleme çağrılar kodu doğal dilde okumak gibi yapıyor


Builder Pattern Ne Zaman Kullanılmamalı?

Her pattern her yerde uygun değildir. Builder Pattern'ı kullanmayın eğer:

  • Sınıfınız 2-3 parametreli ise: Basit bir constructor yeterlidir. Builder eklemek gereksiz karmaşıklık yaratır.

  • Tüm alanlar zorunlu ve farklı tipte ise: Constructor gayet okunabilir olur, Builder ek yük olur.

  • Performans kritik bir hot path'te iseniz: Builder ekstra bir nesne oluşturur (Builder nesnesi kendisi). Nanosaniye düzeyinde optimizasyon gerekiyorsa bu overhead önemli olabilir — ama bu durum çok nadirdir.

  • Nesne zaten mutable olacaksa: Builder'ın temel amacı immutable nesne üretmektir. Eğer nesneniz setter'larla değiştirilecekse, Builder fazladan bir katman olur.


Python'da Builder Pattern

Builder Pattern dile bağımlı değildir. Python'da da aynı prensibi uygulayabilirsiniz, ama Python'un named parameters (isimli parametreler) özelliği sayesinde Builder'a Java kadar sık ihtiyaç duymazsınız:

class EmailMessage:
    """Immutable e-posta mesajı — Builder ile oluşturulur."""

    def __init__(self, builder):
        self.to = builder._to
        self.subject = builder._subject
        self.body = builder._body
        self.cc = tuple(builder._cc)          # Immutable kopya
        self.bcc = tuple(builder._bcc)        # Immutable kopya
        self.attachments = tuple(builder._attachments)
        self.is_html = builder._is_html

    def __str__(self):
        parts = [f"To: {self.to}", f"Subject: {self.subject}"]
        if self.cc:
            parts.append(f"CC: {', '.join(self.cc)}")
        if self.bcc:
            parts.append(f"BCC: {', '.join(self.bcc)}")
        parts.append(f"Body ({('HTML' if self.is_html else 'Text')}): {self.body[:50]}...")
        if self.attachments:
            parts.append(f"Attachments: {', '.join(self.attachments)}")
        return "\n".join(parts)

    class Builder:
        def __init__(self, to, subject):
            # Zorunlu alanlar
            self._to = to
            self._subject = subject
            # Opsiyonel alanlar
            self._body = ""
            self._cc = []
            self._bcc = []
            self._attachments = []
            self._is_html = False

        def body(self, body):
            self._body = body
            return self

        def html_body(self, body):
            self._body = body
            self._is_html = True
            return self

        def cc(self, *addresses):
            self._cc.extend(addresses)
            return self

        def bcc(self, *addresses):
            self._bcc.extend(addresses)
            return self

        def attach(self, *files):
            self._attachments.extend(files)
            return self

        def build(self):
            # Validasyon
            if not self._to or not self._to.strip():
                raise ValueError("Alıcı adresi boş olamaz")
            if not self._subject or not self._subject.strip():
                raise ValueError("Konu boş olamaz")
            if "@" not in self._to:
                raise ValueError("Geçersiz e-posta adresi")
            return EmailMessage(self)


# Kullanım
email = (EmailMessage.Builder("dev@example.com", "Haftalık Rapor")
         .html_body("<h1>Sprint Özeti</h1><p>Bu hafta 12 task tamamlandı.</p>")
         .cc("manager@example.com", "lead@example.com")
         .attach("rapor.pdf", "metrikler.xlsx")
         .build())

print(email)
# Çıktı:
# To: dev@example.com
# Subject: Haftalık Rapor
# CC: manager@example.com, lead@example.com
# Body (HTML): <h1>Sprint Özeti</h1><p>Bu hafta 12 task tamamlan...
# Attachments: rapor.pdf, metrikler.xlsx

💡 İpucu: Python'da dataclass veya attrs kütüphanesi ile keyword arguments kullanarak da benzer sonuçlar elde edebilirsiniz. Ama nesne oluşturma süreci karmaşıksa — örneğin validasyonlar, koşullu varsayılanlar, veya birbirine bağımlı alanlar varsa — Builder Pattern Python'da da değerlidir.


Sonuç

Builder Design Pattern, karmaşık nesneleri oluşturmanın en temiz, en güvenli ve en okunabilir yoludur. İşte bu yazıdan çıkarmanız gereken temel noktalar:

  • Telescoping Constructor antipattern'ından kaçının. 4+ parametreli constructor'lar okunabilirlik ve güvenlik kabusu yaratır. Builder, bu problemi kökünden çözer.

  • Builder ile immutable nesneler oluşturun. Alanları final yapın, setter yazmayın, constructor'ı private tutun. Bu, thread safety ve öngörülebilirlik sağlar.

  • Validasyonu `build()` içinde yapın. Geçersiz durumda nesne oluşturulmasını imkansız kılın. Hataları erken yakalayın.

  • Defensive copy unutmayın. Koleksiyon alanlarını Builder'dan kopyalarken yeni bir kopya oluşturun ve unmodifiable sarmalayıcı ile koruyun.

  • Her yerde Builder kullanmayın. 2-3 parametreli basit sınıflar için constructor yeterlidir. Builder, karmaşıklığı yönetmek için vardır — gereksiz yere eklemek kendi karmaşıklığını yaratır.

  • Gerçek dünyada her yerde göreceksiniz. StringBuilder, Spring'in ResponseEntity, OkHttp'nin Request.Builder'ı, Lombok'un @Builder'ı — pattern'ı tanıdığınızda bu kütüphaneleri çok daha doğal kullanacaksınız.

Builder Pattern'ı öğrenmek, sadece bir tasarım kalıbı ezberlemek değildir. Bu, "karmaşık nesne oluşturmayı nasıl güvenli, okunabilir ve bakımı kolay yapabilirim?" sorusuna verilen en iyi cevabı anlamaktır. Bir sonraki projenizde 4'ten fazla parametreli bir sınıf gördüğünüzde, artık ne yapmanız gerektiğini biliyorsunuz.

Paylaş:
Son güncelleme: Jul 19, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar