Builder Design Pattern: Karmaşık Nesneleri Adım Adım İnşa Etmek
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, booleangö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:
Bir
Buildernesnesi yaratırsınızHangi özellikleri istediğinizi method call'larla belirtirsiniz
Her method call, Builder'ın kendisini döndürür (method chaining / zincirleme metot çağrısı)
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ıyorsunuzImmutability (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şturulduLombok'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ı engelliyorImmutability: Oluşturulan
SelectQuerynesnesi 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
dataclassveyaattrskü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ı
finalyapın, setter yazmayın, constructor'ıprivatetutun. 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
unmodifiablesarmalayı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'inResponseEntity, OkHttp'ninRequest.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.
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
Spring Boot 3'te Exception Handling: Kapsamlı Rehber
Spring Boot 3 ile gelen RFC 7807 ProblemDetail desteği, @ControllerAdvice ile global exception handling ve validation er...
Java'da Stream API: Pratik Kullanım Rehberi
Java Stream API ile filter, map, reduce, collect, flatMap, groupingBy operasyonlarını pratik örneklerle öğrenin. Paralle...
Docker ile Spring Boot Uygulaması Deploy Etmek
Spring Boot uygulamalarını Docker ile containerize etmenin adım adım rehberi. Multi-stage build, docker-compose, health...