Bir Java uygulamasında yeni özellik geliştirmek genellikle kolaydır. Asıl zorluk, uygulama büyüdükçe mevcut kodu bozmadan yeni davranışlar ekleyebilmektir.
Örneğin bir e-ticaret uygulaması düşünelim. İlk sürüm yalnızca kredi kartıyla ödeme kabul ediyor. Birkaç ay sonra ekip dijital cüzdan, banka transferi ve farklı ödeme sağlayıcıları eklemek istiyor. Geliştirici her ödeme yöntemi için aynı sınıfın içine yeni if-else koşulları eklerse sistem zamanla bakım açısından zorlaşır.

Strategy tasarım deseni bu soruna bir çözüm sunar. Ancak tek çözüm o değildir. Basit bir switch ifadesi de bazı projelerde aynı işi daha az kodla halledebilir.
Tasarım desenleri (Design Patterns), yazılım geliştiricilerin tekrar eden tasarım problemlerini çözmek için kullandığı, deneyimle şekillenmiş çözüm yaklaşımlarıdır. Bir desen hazır kod sunmaz. Geliştiriciye sorumlulukları nasıl ayıracağını, bağımlılıkları nasıl yöneteceğini ve değişiklikleri sistemin hangi noktalarında karşılayacağını gösterir.
Bu rehberde Java’nın en önemli tasarım desenlerini gerçek kullanım senaryolarıyla inceleyeceğiz. Java 17 ve Java 21 özelliklerinin klasik desenleri nasıl etkilediğini, hangi desenin ne zaman avantaj kazandırdığını ve hangi koşullarda gereksiz karmaşıklık yarattığını da ele alacağız.
Tasarım Desenleri (Design Patterns) Nedir?
Tasarım deseni, belirli bir yazılım tasarım problemine tekrar uygulanabilen genel bir çözüm modelidir.
Örneğin:
- Bir nesneden yalnızca tek örnek üretmek istiyorsanız Singleton desenini değerlendirebilirsiniz.
- Nesne oluşturma mantığını iş kurallarından ayırmak istiyorsanız Factory yaklaşımı yardımcı olur.
- Çok sayıda yapılandırma parametresi içeren nesneler oluşturuyorsanız Builder kullanabilirsiniz.
- Çalışma anında farklı algoritmalar arasında geçiş yapmak istiyorsanız Strategy uygundur.
- Bir olay gerçekleştiğinde birden fazla bileşenin haberdar olmasını istiyorsanız Observer kullanabilirsiniz.
Burada önemli bir ayrım var: Tasarım deseni ile yazılım mimarisi aynı şey değildir.
Clean Architecture bir uygulamanın katmanlarını ve bağımlılık yönlerini ele alırken Strategy, belirli bir davranış ailesini nasıl yöneteceğinize odaklanır. Bu iki yaklaşım birbirini tamamlayabilir.
Tasarım Desenleri Neden Önemlidir?
İyi seçilmiş bir tasarım deseni geliştiriciye birkaç somut avantaj kazandırır.
Değişikliklerin etkisini sınırlar. Bir ödeme sağlayıcısını değiştirmek için sipariş yönetimi kodunu yeniden yazmak zorunda kalmazsınız.
Test yazmayı kolaylaştırır. Bağımlılıkları arayüzler üzerinden yönettiğinizde gerçek servisler yerine test nesneleri kullanabilirsiniz.
Takım içi iletişimi güçlendirir. Bir geliştiricinin “burada Strategy kullanıyoruz” demesi, uzun bir sınıf diyagramını açıklamaktan daha hızlı olabilir.
Tekrarlanan tasarım kararlarını azaltır. Ekip, iyi anladığı çözümleri benzer problemlere tutarlı biçimde uygular.
Bununla birlikte her tasarım deseni yeni sınıflar, arayüzler veya ek soyutlamalar getirebilir. Küçük bir problemi çözmek için gereğinden fazla katman oluşturmak kod kalitesini artırmaz.
Java Tasarım Desenleri Kaç Kategoriye Ayrılır?
Gang of Four (GoF) yaklaşımı, 23 klasik tasarım desenini üç ana kategori altında inceler.
| Kategori | Temel amaç | Örnekler |
|---|---|---|
| Creational (Yaratımsal) | Nesne oluşturma sürecini yönetmek | Singleton, Factory Method, Abstract Factory, Builder, Prototype |
| Structural (Yapısal) | Sınıfları ve nesneleri birlikte çalıştırmak | Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight |
| Behavioral (Davranışsal) | Nesneler arasındaki iletişimi ve davranışları düzenlemek | Strategy, Observer, Command, State, Iterator, Mediator, Chain of Responsibility, Template Method, Visitor, Memento, Interpreter |
Bu ayrım, hangi deseni seçeceğinizi doğrudan söylemez. Önce problemin hangi sorumlulukla ilgili olduğunu anlamanız gerekir.
Nesne oluşturma koduyla mı uğraşıyorsunuz? Birden fazla sistemi mi bütünleştiriyorsunuz? Yoksa çalışma anında değişen iş kurallarını mı yönetiyorsunuz?
Bu sorular desen seçimini daraltır.
1. Singleton Pattern: Tek Bir Nesneyle Çalışmak
Singleton, bir sınıftan yalnızca tek bir örnek oluşturmayı ve bu örneğe merkezi bir erişim noktası sunmayı amaçlar.
Bir uygulamanın belirli bir süreç içinde ortak kullanacağı konfigürasyon nesnesi veya durum tutmayan bir yardımcı servis bu yaklaşım için aday olabilir.
Ancak Singleton ile global değişken kullanımı arasındaki sınır oldukça incedir.
Java’da Singleton Nasıl Uygulanır?
Java’da Singleton geliştirmek için farklı yöntemler bulunur. Bunlardan biri enum kullanmaktır.
public enum ApplicationConfig {
INSTANCE;
private final String environment = "production";
public String getEnvironment() {
return environment;
}
}
Kullanım:
String environment =
ApplicationConfig.INSTANCE.getEnvironment();
System.out.println(environment);
Java, her enum sabiti için tek bir örnek oluşturur. Bu yaklaşım standart Java serileştirme davranışıyla uyumludur ve ek bir synchronized mekanizması gerektirmeden güvenli örnek oluşturma sağlar.
Bununla birlikte enum Singleton, nesnenin içindeki değiştirilebilir verileri kendiliğinden thread-safe hâle getirmez.
Singleton Kullanırken Nelere Dikkat Etmelisiniz?
Bir Singleton başka sınıflar tarafından doğrudan çağrılıyorsa bağımlılıklar kodun içine gömülür.
Örneğin:
public class OrderService {
public void createOrder() {
String env =
ApplicationConfig.INSTANCE.getEnvironment();
}
}
OrderService artık konfigürasyon kaynağını kendisi seçer. Test ortamında bu bağımlılığı değiştirmek güçleşebilir.
Dependency Injection kullanan projelerde geliştirici çoğu zaman Singleton yerine uygulama kapsamındaki tek bir servis örneğini tercih eder.
Spring Framework içindeki varsayılan singleton bean kapsamı buna örnektir. Spring, ilgili uygulama bağlamında bir bean tanımı için ortak bir örnek yönetir. Bu davranış, JVM genelinde tek örnek garantisi veren klasik Singleton ile aynı değildir.
Ne zaman kullanmalı? Nesnenin süreç genelinde tek bir örnek olarak var olması gerçekten bir tasarım gereksinimiyse ve global erişimin getirdiği bağımlılıkları kabul ediyorsanız kullanın.
2. Factory Pattern: Nesne Oluşturma Mantığını Ayırmak
Bir uygulama farklı bildirim kanalları üzerinden mesaj gönderebilir: e-posta, SMS veya anlık bildirim.
Her iş servisi hangi sınıfı oluşturacağını kendisi seçerse aynı karar mantığı uygulamanın farklı noktalarına dağılır.
Factory yaklaşımı nesne oluşturma kararını merkezi bir bileşene taşır.
Burada terminolojiye dikkat etmek gerekir. Gündelik Java geliştirmesinde Factory Pattern olarak anılan basit statik fabrika yaklaşımı, GoF kitabındaki kalıtım temelli Factory Method deseniyle birebir aynı değildir.
Java 21 ile Factory Örneği
Önce bildirim modellerini tanımlayalım:
sealed interface Notification
permits EmailNotification, SmsNotification {
}
record EmailNotification(String address)
implements Notification {
}
record SmsNotification(String phoneNumber)
implements Notification {
}
Ardından her kanal için farklı bir gönderici oluşturalım:
interface NotificationSender {
void send(String message);
}
final class EmailSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
final class SmsSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("SMS: " + message);
}
}
Factory sınıfı:
final class NotificationFactory {
private NotificationFactory() {
}
static NotificationSender create(
Notification notification) {
return switch (notification) {
case EmailNotification email ->
new EmailSender();
case SmsNotification sms ->
new SmsSender();
};
}
}
Kullanım:
Notification notification =
new EmailNotification("[email protected]");
NotificationSender sender =
NotificationFactory.create(notification);
sender.send("Siparişiniz onaylandı.");
Bu Tasarım Neden İşe Yarar?
NotificationFactory nesnenin somut türünü seçer. İstemci ise oluşturduğu nesneyle NotificationSender arayüzü üzerinden iletişim kurar.
Java 17 ile kalıcı hâle gelen sealed yapısı, hangi türlerin Notification arayüzünü uygulayabileceğini sınırlar.
Java 21 ile kalıcı hâle gelen switch pattern matching özelliği ise bu türler üzerinde kapsamlı bir eşleştirme yapar.
Yeni bir bildirim türünü permits kapsamına eklediğinizde derleyici, ilgili switch ifadesinin yeni türü de ele almasını isteyebilir.
Bu yaklaşımın bir sınırı var: Factory, yeni somut türler karşısında değişiklik gerektirir. Kapalı bir tür ailesi için bu davranış avantaj sağlar. Dinamik olarak genişleyen bir eklenti mimarisinde ise registry tabanlı bir fabrika veya Dependency Injection daha uygun olabilir.
Ne zaman kullanmalı? Nesne oluşturma kararı birden fazla yerde tekrar ediyorsa veya nesne oluştururken iş kurallarına bağlı seçim yapıyorsanız Factory yaklaşımını değerlendirin.
3. Builder Pattern: Karmaşık Nesneleri Adım Adım Oluşturmak
Bir API isteği düşünelim. Kullanıcı, rapor oluştururken tarih aralığı, format, dil, filtreler ve sıralama tercihi belirleyebiliyor.
Bütün parametreleri tek constructor üzerinden almak zamanla okunabilirliği azaltabilir.
Builder deseni, karmaşık bir nesnenin oluşturulma sürecini anlaşılır adımlara böler.
Java Builder Örneği
import java.time.LocalDate;
import java.util.Objects;
public final class ReportRequest {
private final LocalDate startDate;
private final LocalDate endDate;
private final String format;
private ReportRequest(Builder builder) {
this.startDate = builder.startDate;
this.endDate = builder.endDate;
this.format = builder.format;
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private LocalDate startDate;
private LocalDate endDate;
private String format = "PDF";
public Builder startDate(LocalDate value) {
this.startDate = value;
return this;
}
public Builder endDate(LocalDate value) {
this.endDate = value;
return this;
}
public Builder format(String value) {
this.format = value;
return this;
}
public ReportRequest build() {
Objects.requireNonNull(startDate);
Objects.requireNonNull(endDate);
Objects.requireNonNull(format);
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException(
"Başlangıç tarihi bitişten sonra olamaz."
);
}
return new ReportRequest(this);
}
}
}
Kullanım:
ReportRequest request = ReportRequest.builder()
.startDate(LocalDate.of(2026, 1, 1))
.endDate(LocalDate.of(2026, 1, 31))
.format("CSV")
.build();
Builder, nesne oluşmadan önce alanları toplar ve build() aşamasında doğrulama yapar.
Bu sayede geçersiz tarih aralığına sahip bir ReportRequest nesnesi üretmeyiz.
Java Record, Builder Deseninin Yerini Alır mı?
Java 17 ile record kullanarak veri taşıyan sınıfları çok daha kısa yazabiliriz.
import java.time.LocalDate;
import java.util.Objects;
public record ReportFilter(
LocalDate startDate,
LocalDate endDate,
String format) {
public ReportFilter {
Objects.requireNonNull(startDate);
Objects.requireNonNull(endDate);
Objects.requireNonNull(format);
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException(
"Geçersiz tarih aralığı"
);
}
}
}
Üç alanlı basit bir veri modeli için bu çözüm yeterli olabilir.
Ancak çok sayıda isteğe bağlı parametre, birbirine bağımlı yapılandırma seçeneği veya aşamalı oluşturma süreci varsa Builder hâlâ değer taşır.
Record bileşenlerinin sonradan yeniden atanamaması da tek başına derin immutability garantisi vermez. Örneğin bir record içinde değiştirilebilir liste tutarsanız dış kod bu listenin içeriğini değiştirebilir.
Ne zaman kullanmalı? Constructor parametreleri anlamını kaybediyorsa, çok sayıda isteğe bağlı alan varsa veya nesne oluştururken tutarlı bir doğrulama akışı gerekiyorsa Builder tercih edin.
4. Adapter Pattern: Uyumsuz Arayüzleri Birleştirmek
Yazılım projelerinde her servisin aynı arayüzü kullanmasını bekleyemeyiz.
Örneğin bir ödeme sistemi farklı bankaların servisleriyle iletişim kurabilir. Bankalardan biri makePayment() metodunu, diğeri executeTransaction() metodunu sunabilir.
Bu farklılıkları iş katmanına taşımak, ödeme kodunu dış servislerin ayrıntılarına bağımlı hâle getirir.
Adapter deseni, farklı arayüzleri ortak bir sözleşmeye dönüştürür.
Java Adapter Örneği
Uygulamamızın ortak sözleşmesi:
import java.math.BigDecimal;
interface PaymentGateway {
boolean pay(BigDecimal amount);
}
Harici bankanın sunduğu servis:
class LegacyBankClient {
public String executeTransaction(
BigDecimal amount) {
return "SUCCESS";
}
}
Adapter:
final class LegacyBankAdapter
implements PaymentGateway {
private final LegacyBankClient client;
LegacyBankAdapter(LegacyBankClient client) {
this.client = client;
}
@Override
public boolean pay(BigDecimal amount) {
String result =
client.executeTransaction(amount);
return "SUCCESS".equals(result);
}
}
Kullanım:
PaymentGateway gateway =
new LegacyBankAdapter(new LegacyBankClient());
boolean success =
gateway.pay(new BigDecimal("149.90"));
Artık sipariş servisi hangi bankanın hangi metodunu kullandığını bilmek zorunda kalmaz.
Adapter Deseninin Mimari Avantajı
Adapter yalnızca metot isimlerini değiştirmez. Dış sistemlerin veri modellerini ve hata sözleşmelerini de uygulamanın kendi modeline dönüştürebilir.
Örneğin gerçek bir banka entegrasyonunda HTTP durum kodlarını, hata mesajlarını ve işlem referanslarını ortak bir sonuç nesnesine dönüştürmek isteyebilirsiniz.
Ancak ödeme gibi kritik işlemlerde "SUCCESS" dışındaki her sonucu doğrudan başarısızlık olarak yorumlamak yeterli değildir. Zaman aşımı, belirsiz işlem sonucu ve kesin ret birbirinden farklı durumlardır.
Bu ayrım olmadan aynı ödemeyi tekrar denemek çift tahsilat riski doğurabilir.
Ne zaman kullanmalı? Üçüncü taraf SDK, eski sistem veya farklı veri sözleşmesi kullanan bir servisle entegrasyon geliştirirken Adapter deseni güçlü bir seçenektir.
5. Decorator Pattern: Mevcut Davranışa Yeni Sorumluluklar Eklemek
Bir dış API istemciniz olduğunu düşünün.
İlk sürüm yalnızca HTTP isteği gönderiyor. Daha sonra her isteğin süresini ölçmek, log üretmek veya metrik toplamak istiyorsunuz.
Bütün bu sorumlulukları doğrudan HTTP istemcisinin içine eklemek, sınıfın kapsamını genişletir.
Decorator deseni, mevcut nesnenin etrafına yeni bir nesne yerleştirerek davranış eklemenize olanak tanır.
Java Decorator Örneği
Ortak arayüz:
interface ApiClient {
String fetchData();
}
Temel istemci:
final class DefaultApiClient implements ApiClient {
@Override
public String fetchData() {
return "API response";
}
}
Zaman ölçen Decorator:
final class TimedApiClient implements ApiClient {
private final ApiClient delegate;
TimedApiClient(ApiClient delegate) {
this.delegate = delegate;
}
@Override
public String fetchData() {
long start = System.nanoTime();
try {
return delegate.fetchData();
} finally {
long elapsed = System.nanoTime() - start;
System.out.println(
"Duration(ns): " + elapsed
);
}
}
}
Kullanım:
ApiClient client =
new TimedApiClient(new DefaultApiClient());
System.out.println(client.fetchData());
TimedApiClient, gerçek istemcinin sorumluluğunu değiştirmez. Yalnızca işlem süresini ölçer.
Decorator zinciri kurarak farklı davranışları birleştirebilirsiniz. Örneğin ayrı bir Decorator metrik kaydı tutarken bir diğeri erişim kontrolü gerçekleştirebilir.
Decorator ile Inheritance Arasındaki Fark
Kalıtım, davranışı sınıf hiyerarşisi üzerinden genişletir. Decorator ise composition kullanır.
Bu fark, davranış kombinasyonları arttığında önem kazanır.
Her kombinasyon için alt sınıf üretmek yerine ihtiyacınız olan davranışları nesnelerin etrafına yerleştirebilirsiniz.
Yine de her çapraz kesen sorumluluk için elle Decorator yazmak zorunda değilsiniz. Spring AOP, interceptor veya filtre mekanizmaları bazı durumlarda daha uygun seçeneklerdir.
Ne zaman kullanmalı? Mevcut bir nesneye loglama, metrik, doğrulama veya önbellekleme gibi ek davranışlar kazandırmak istediğinizde değerlendirin.
6. Strategy Pattern: Değişebilen Algoritmaları Yönetmek
Strategy, gerçek projelerde en sık karşılaşılan davranışsal tasarım desenlerinden biridir.
Bir e-ticaret sisteminde farklı indirim hesaplama kuralları bulunduğunu düşünelim.
Standart müşteriler indirim almıyor. Premium müşteriler %10 indirim alıyor. Kurumsal müşteriler ise farklı bir fiyatlandırma kuralına tabi.
Bütün bu kuralları tek bir calculateDiscount() metodunda toplamak başlangıçta kolaydır. Ancak yeni kampanyalar geldikçe koşullar uzar.
Strategy, her algoritmayı ayrı bir davranış olarak ele alır.
Java Strategy Örneği
Önce ortak sözleşmeyi tanımlayalım:
import java.math.BigDecimal;
interface DiscountStrategy {
BigDecimal calculate(BigDecimal amount);
}
Standart müşteri stratejisi:
final class StandardDiscount
implements DiscountStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return BigDecimal.ZERO;
}
}
Premium müşteri stratejisi:
final class PremiumDiscount
implements DiscountStrategy {
private static final BigDecimal RATE =
new BigDecimal("0.10");
@Override
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(RATE);
}
}
Stratejiyi kullanan servis:
final class PricingService {
private final DiscountStrategy strategy;
PricingService(DiscountStrategy strategy) {
this.strategy = strategy;
}
BigDecimal calculateTotal(BigDecimal amount) {
if (amount.signum() < 0) {
throw new IllegalArgumentException(
"Tutar negatif olamaz"
);
}
BigDecimal discount =
strategy.calculate(amount);
return amount.subtract(discount);
}
}
Kullanım:
PricingService service =
new PricingService(new PremiumDiscount());
BigDecimal total = service.calculateTotal(
new BigDecimal("1000.00")
);
System.out.println(total); // 900.0000
Burada PricingService, indirimin nasıl hesaplandığını bilmez.
Yeni bir indirim politikası geldiğinde geliştirici mevcut servis mantığını değiştirmeden farklı bir DiscountStrategy uygulaması ekleyebilir.
Gerçek finansal uygulamalarda para birimini, yuvarlama kurallarını ve hesaplama hassasiyetini ayrıca tanımlamak gerekir. Örnekteki BigDecimal kullanımı yalnızca algoritma ayrımını göstermeyi amaçlar.
Strategy ile Open/Closed Principle İlişkisi
Open/Closed Principle, yazılım bileşenlerinin yeni davranışlarla genişleyebilmesini ve mevcut kodda gereksiz değişiklik ihtiyacının azalmasını hedefler.
Strategy bunu doğrudan destekler.
Fakat bütün koşullar altında mevcut kodu değiştirmeme garantisi vermez. Strateji seçimini yöneten bileşenin yeni türleri tanıması gerekebilir.
Her Strategy İçin Yeni Sınıf Gerekli mi?
Hayır.
Java’da tek soyut metot içeren arayüzleri lambda ifadeleriyle uygulayabilirsiniz.
DiscountStrategy campaignDiscount =
amount -> amount.multiply(
new BigDecimal("0.15")
);
Basit ve durum tutmayan bir algoritma için lambda yeterli olabilir.
Buna karşılık bir strateji kendi konfigürasyonunu, bağımlılıklarını ve kapsamlı testlerini taşıyorsa ayrı sınıf kullanmak daha okunabilir bir çözüm sunar.
Ne zaman kullanmalı? Aynı işlemin farklı algoritmalarla gerçekleşmesi gerekiyorsa ve bu algoritmaları çalışma anında değiştirmek istiyorsanız Strategy kullanın.
7. Observer Pattern: Olay Tabanlı İletişim Kurmak
Bir sipariş tamamlandığında sistem birden fazla işlem başlatabilir.
Örneğin:
- Müşteriye bildirim gönderir.
- Analitik sistemine olay iletir.
- Sadakat puanı hesaplar.
- Denetim kaydı üretir.
Sipariş servisi bütün bu bileşenleri doğrudan çağırırsa zamanla çok sayıda bağımlılık taşımaya başlar.
Observer deseni, bir olay gerçekleştiğinde ilgili dinleyicilerin haberdar olmasını sağlar.
Java Observer Örneği
Önce olayı tanımlayalım:
record OrderCreatedEvent(String orderId) {
}
Dinleyici sözleşmesi:
interface OrderEventListener {
void onOrderCreated(OrderCreatedEvent event);
}
Bildirim dinleyicisi:
final class EmailNotificationListener
implements OrderEventListener {
@Override
public void onOrderCreated(
OrderCreatedEvent event) {
System.out.println(
"Email notification: " + event.orderId()
);
}
}
Olayı yöneten sınıf:
import java.util.ArrayList;
import java.util.List;
final class OrderEventPublisher {
private final List<OrderEventListener> listeners =
new ArrayList<>();
public void subscribe(OrderEventListener listener) {
listeners.add(listener);
}
public void publish(OrderCreatedEvent event) {
for (OrderEventListener listener : listeners) {
listener.onOrderCreated(event);
}
}
}
Kullanım:
OrderEventPublisher publisher =
new OrderEventPublisher();
publisher.subscribe(
new EmailNotificationListener()
);
publisher.publish(
new OrderCreatedEvent("ORD-1001")
);
Publisher yalnızca dinleyici arayüzünü bilir. Bildirim gönderme ayrıntılarını bilmez.
Observer ile Event-Driven Architecture Aynı Şey mi?
Hayır.
Observer, aynı uygulama içinde çalışan nesneler arasında olay bildirimi için kullanılabilir.
Event-Driven Architecture ise farklı servisler, mesaj aracıları, kalıcı olay akışları ve dağıtık iletişim gibi daha geniş konuları ele alır.
Örneğin Apache Kafka üzerinden bir olay yayınlamak, klasik uygulama içi Observer kullanımından farklı operasyonel gereksinimler getirir.
Bu örnekte publish() metodu senkron çalışır. Dinleyicilerden biri hata fırlatırsa sonraki dinleyiciler olayı alamayabilir.
Gerçek bir uygulamada hata izolasyonu, tekrar deneme, olay sıralaması ve teslimat garantileri için ayrıca tasarım kararı vermelisiniz.
ArrayList tabanlı abonelik yönetimi de eşzamanlı ekleme ve bildirim işlemleri için uygun değildir. Birden fazla thread dinleyici listesini değiştiriyorsa thread-safe bir yapı veya açık bir eşzamanlılık politikası seçmelisiniz.
Ne zaman kullanmalı? Bir olayın birden fazla bağımsız bileşeni ilgilendirdiği ve yayıncı ile dinleyiciler arasında doğrudan bağımlılık kurmak istemediğiniz durumlarda tercih edin.
8. State Pattern: Nesnenin Durumuna Göre Davranışını Değiştirmek
Bir siparişin farklı yaşam döngüsü durumları vardır:
CREATED, PAID, SHIPPED, DELIVERED, CANCELLED
Her durum aynı işlemlere izin vermez.
Örneğin kargoya çıkan bir siparişi doğrudan iptal etmek istemeyebilirsiniz. Teslim edilen sipariş ise farklı bir iade sürecine geçebilir.
Karmaşık yaşam döngülerinde State deseni her durumun davranışını ayrı bir nesneyle temsil eder.
Ancak Java 21, basit ve sınırlı durum makineleri için daha kısa bir seçenek de sunar.
Java 21 ile Sealed State Modeli
sealed interface OrderState
permits Created, Paid, Shipped, Cancelled {
}
record Created() implements OrderState {
}
record Paid() implements OrderState {
}
record Shipped() implements OrderState {
}
record Cancelled() implements OrderState {
}
Durum geçişini modelleyelim:
final class OrderStateMachine {
static OrderState cancel(OrderState state) {
return switch (state) {
case Created c -> new Cancelled();
case Paid p -> new Cancelled();
case Shipped s ->
throw new IllegalStateException(
"Kargodaki sipariş iptal edilemez"
);
case Cancelled c ->
throw new IllegalStateException(
"Sipariş zaten iptal"
);
};
}
}
Bu kod Java 21’in sealed interface ve pattern matching özelliklerini kullanır.
Derleyici, tanımladığımız tür ailesi üzerinden switch kapsamını denetleyebilir.
Önemli bir mimari ayrım var: Bu örnek klasik nesne yönelimli State deseninin tam uygulaması değildir. Burada davranışları durum nesnelerine dağıtmak yerine merkezi bir durum geçiş fonksiyonunda topluyoruz.
Gerçek GoF State deseni, her durumun kendi geçiş ve davranış mantığını yönetmesine olanak tanır.
Hangi Yaklaşımı Seçmelisiniz?
Az sayıda durum ve birkaç geçiş içeren kapalı bir modelde sealed interface ile switch çoğu zaman yeterlidir.
Ancak her durum farklı iş servislerini çağırıyor, kendi kurallarını yönetiyor ve yeni davranışlar ekliyorsa klasik State yaklaşımı daha uygun olabilir.
Modern Java kullanmak, bütün tasarım desenlerini ortadan kaldırmaz. Bazı problemleri daha az soyutlamayla çözmemize yardımcı olur.
Java 17 ve Java 21 Tasarım Desenlerini Nasıl Etkiliyor?
Klasik tasarım desenleri, Java’nın bugünkü dil özelliklerinin büyük bir kısmından daha önce ortaya çıktı.
Bu nedenle eski kitaplarda gördüğünüz her sınıf hiyerarşisini güncel bir Java projesinde aynı şekilde uygulamak zorunda değilsiniz.
| Modern Java özelliği | Tasarıma etkisi |
|---|---|
| Record | Basit veri modellerindeki gereksiz kod miktarını azaltır |
| Sealed Class / Interface | Kapalı tür ailelerini açıkça tanımlar |
| Pattern Matching for Switch | Türe bağlı dallanma mantığını sadeleştirir |
| Lambda Expressions | Basit Strategy ve Command uygulamalarını kısaltır |
| Method References | Fonksiyonel davranışları daha kolay aktarır |
| Virtual Threads (Java 21) | Yoğun eşzamanlı I/O işlerinde thread yönetimini kolaylaştırır |
Virtual Threads bir tasarım deseni değildir. Java 21’in sunduğu bir eşzamanlılık özelliğidir. Ancak uygulamanın concurrency modelini tasarlarken verdiğiniz kararları etkileyebilir.
Aynı şekilde record da Builder deseninin birebir karşılığı değildir. Bir dil özelliğinin hangi problemi çözdüğünü anlamadan bir desenin yerine koymak doğru sonuç vermez.
Tasarım Desenleri ile SOLID Prensipleri Arasındaki İlişki
SOLID ve tasarım desenleri farklı konuları ele alır.
SOLID prensipleri sınıf ve modül tasarımına yönelik rehberlik sunar. Tasarım desenleri ise belirli problemlere çözüm yapıları önerir.
İkisi arasındaki ilişkiyi birkaç örnekle inceleyelim.
Single Responsibility Principle (SRP)
Bir sınıfın değişmesi için birden fazla bağımsız nedeni olmaması gerekir.
Decorator, bildirim gönderme ile performans ölçme sorumluluklarını farklı sınıflara ayırabilir.
Open/Closed Principle (OCP)
Strategy, mevcut algoritma kullanıcılarını değiştirmeden yeni algoritma uygulamaları eklemeyi kolaylaştırır.
Ancak desenin yanlış uygulanması hâlinde merkezi bir if-else zinciri aynı bağımlılık sorununu koruyabilir.
Liskov Substitution Principle (LSP)
Bir Strategy uygulaması, arayüzün belirlediği sözleşmeye uymalıdır.
Örneğin DiscountStrategy için negatif indirim değerleri yasaksa yeni bir stratejinin bu kuralı ihlal etmesi yerine geçebilirliği bozar.
Interface Segregation Principle (ISP)
Küçük, amaca odaklı arayüzler Adapter ve Strategy gibi desenlerle daha iyi çalışır.
Her davranışı kapsayan devasa bir servis arayüzü, uygulayan sınıflara gereksiz sorumluluk yükler.
Dependency Inversion Principle (DIP)
Üst seviye iş kurallarını somut teknoloji sınıflarına doğrudan bağımlı hâle getirmemelisiniz.
Örneğin PricingService, PremiumDiscount sınıfını doğrudan oluşturmak yerine DiscountStrategy arayüzünü kullanır.
Bu tercih, test edilebilirliği ve bağımlılıkların yönetimini iyileştirir.
Hangi Tasarım Deseni Ne Zaman Kullanılmalı?
Tasarım deseni seçerken sınıf isimlerinden önce değişiklik ihtiyacına odaklanın.
| Karşılaştığınız problem | Değerlendirilecek desen |
|---|---|
| Tek bir ortak nesne örneği gerekiyor | Singleton |
| Nesne oluşturma kuralları karmaşıklaşıyor | Factory / Factory Method |
| Nesne çok sayıda parametre içeriyor | Builder |
| Harici servis ortak arayüze uymuyor | Adapter |
| Mevcut davranışa ek sorumluluk gerekiyor | Decorator |
| Karmaşık alt sistem için basit giriş noktası gerekiyor | Facade |
| Algoritma çalışma anında değişiyor | Strategy |
| Olay birden fazla dinleyiciyi ilgilendiriyor | Observer |
| Nesne davranışı mevcut durumuna bağlı | State |
| İşlemleri kuyruklamak veya komut olarak saklamak gerekiyor | Command |
| İstek birden fazla işleyiciden geçiyor | Chain of Responsibility |
Bu tabloyu kesin seçim kuralları olarak değerlendirmeyin. Aynı problem farklı bağlamlarda farklı çözümler gerektirebilir.
Örneğin yalnızca iki seçenek içeren sabit bir algoritma için switch ifadesi yeterli olabilir. Yeni algoritmaların bağımsız ekipler tarafından geliştirildiği bir sistemde ise Strategy daha güçlü bir tercih hâline gelir.
Spring Boot Projelerinde Tasarım Desenleri
Spring Framework, birçok tasarım deseninin fikirlerini kendi altyapısında kullanır.
Bununla birlikte framework kullanmak, desenleri öğrenme ihtiyacını ortadan kaldırmaz.
Dependency Injection ve Strategy
Spring, aynı arayüzün farklı implementasyonlarını bean olarak yönetebilir.
Bir ödeme servisinde farklı PaymentStrategy sınıfları tanımlayıp uygun olanı çalışma anında seçebilirsiniz.
Ancak seçimin nerede gerçekleşeceğini açıkça tasarlamalısınız. Bir servisin Spring ApplicationContext üzerinden sürekli bean araması, gizli bağımlılıklar yaratabilir.
Constructor Injection çoğu durumda daha açık bir bağımlılık modeli sunar.
Spring Event Modeli ve Observer
Spring’in ApplicationEventPublisher mekanizması, uygulama içinde olay yayınlama yaklaşımını destekler.
@EventListener kullanan bileşenler ilgili olaylara tepki verebilir.
Fakat uygulama içi olay yayınlamak ile dağıtık mesajlaşma arasında teslimat ve dayanıklılık açısından önemli farklar bulunur.
Spring AOP ve Decorator
Spring AOP, belirli metot çağrılarına ek davranış bağlamayı kolaylaştırır.
Transaction yönetimi, metrik toplama ve bazı loglama senaryoları buna örnek olabilir.
Bu yapı klasik Decorator ile aynı uygulama tekniğini kullanmak zorunda değildir. Benzer sorumluluk ayrımı hedeflerine farklı mekanizmalarla ulaşabilir.
Spring’in sunduğu soyutlamalar, tasarım kararının neden gerekli olduğunu anlamadığınızda karmaşıklığı gizleyebilir. Bu nedenle framework mekanizmasını seçmeden önce çözmek istediğiniz bağımlılık problemini tanımlayın.
Tasarım Desenlerini Kullanırken Yapılan Yaygın Hatalar
Her Problem İçin Desen Aramak
Bir metot üç satırda anlaşılır biçimde çalışıyorsa onu beş sınıfa bölmek kod kalitesini otomatik olarak artırmaz.
Soyutlama maliyetini karşılayacak somut bir değişiklik ihtiyacı olmalı.
Gereksiz Singleton Kullanımı
Singleton, hızlı erişim sunduğu için cazip görünür. Ancak kontrolsüz global durum, özellikle paralel testlerde ve eşzamanlı işlemlerde sorun çıkarabilir.
Factory ile Dependency Injection’ı Karıştırmak
Factory nesne oluşturma sorumluluğunu ele alır.
Dependency Injection ise nesnenin ihtiyaç duyduğu bağımlılıkları dışarıdan almasına odaklanır.
Bir uygulama her iki yaklaşımı birlikte kullanabilir.
Observer ile Güvenilir Mesajlaşmayı Eşdeğer Görmek
Uygulama içindeki bir Observer mekanizması, olayları otomatik olarak kalıcı tutmaz.
Bir listener hata verdiğinde veya uygulama kapandığında ne olacağını ayrıca tasarlamanız gerekir.
Sırf Tasarım Deseni Kullanmak İçin Arayüz Açmak
Her somut sınıfın mutlaka bir arayüze ihtiyacı yoktur.
Arayüz, gerçek bir sözleşmeyi veya anlamlı bir değişkenlik noktasını temsil ediyorsa değer kazandırır.
Tek implementasyonu bulunan ve alternatif davranış ihtiyacı taşımayan bir sınıf için erken soyutlama gereksiz olabilir.
Tasarım Deseni Seçerken Sorulması Gereken Sorular
Kodun içine yeni bir desen yerleştirmeden önce şu sorulara yanıt verin:
- Hangi somut problemle karşı karşıyayım?
- Bu problem bugün gerçekten var mı, yoksa geleceğe ilişkin bir tahmin mi?
- Mevcut kod değişikliklere hangi noktada direnç gösteriyor?
- Daha basit bir Java dil özelliği aynı sorunu çözebilir mi?
- Yeni soyutlama birim testleri kolaylaştıracak mı?
- Bu tasarımı altı ay sonra okuyacak geliştirici niyetimi anlayabilecek mi?
- Deseni kaldırırsam kod daha basit ve anlaşılır hâle gelir mi?
Bu sorular özellikle refactoring sırasında önemlidir.
Refactoring sürecinde önce değişiklik maliyetinin yüksek olduğu bölgeyi bulun. Sonra yeni yapının gerçekten bu maliyeti azaltıp azaltmadığını testlerle doğrulayın.
Sık Sorulan Sorular
Java’da en çok kullanılan tasarım desenleri hangileridir?
Strategy, Factory, Builder, Adapter, Observer ve Decorator, Java backend uygulamalarında sık karşılaşılan desenler arasındadır. Ancak kullanım sıklığı framework’e, mimariye ve projenin ihtiyaçlarına göre değişir.
Java’da tasarım deseni kullanmak zorunlu mu?
Hayır. Tasarım desenleri dilin zorunlu bileşenleri değildir. Geliştirici, kodun bakımını ve genişletilmesini kolaylaştırdığı durumlarda bu çözümleri tercih eder.
Factory ile Builder arasındaki fark nedir?
Factory hangi nesnenin oluşturulacağını veya nesne oluşturma sürecinin hangi bileşen tarafından yönetileceğini ele alır. Builder ise özellikle çok adımlı ve çok parametreli nesne oluşturma süreçlerine odaklanır.
Strategy ile State arasındaki fark nedir?
Strategy, aynı amaca yönelik farklı algoritmalar arasında seçim yapmayı hedefler.
State ise bir nesnenin kendi yaşam döngüsündeki mevcut durumuna bağlı davranışları yönetir.
İki desen benzer sınıf yapıları kullanabilir; ancak tasarım niyetleri farklıdır.
Java Record, Builder kullanımını gereksiz kılar mı?
Hayır. Record, veri odaklı sınıflarda tekrarlayan kodu azaltır. Builder ise karmaşık oluşturma süreçlerini ve çok sayıdaki isteğe bağlı parametreyi yönetmeye devam edebilir.
Java 21 ile klasik tasarım desenleri hâlâ gerekli mi?
Evet. Java 21’in sealed türleri ve pattern matching özellikleri bazı çözümleri sadeleştirir. Ancak nesnelerin sorumluluklarını ayırmak, bağımlılıkları yönetmek ve değişebilir davranışları modellemek hâlâ tasarım kararları gerektirir.
Sonuç: İyi Tasarım, Doğru Soyutlamayı Seçmekle Başlar
Java tasarım desenleri, geliştiriciye her problem için uygulanacak sabit kurallar sunmaz. Tekrar eden tasarım sorunlarını çözmek için ortak bir düşünme biçimi kazandırır.
Singleton nesne yaşam döngüsüne, Factory nesne oluşturma kararlarına, Builder karmaşık yapılandırmalara odaklanır. Adapter ve Decorator bileşenlerin birlikte çalışma biçimini düzenler. Strategy, Observer ve State ise davranışların nasıl değişeceğini ve bileşenlerin nasıl iletişim kuracağını ele alır.
Java 17 ve Java 21, bu kararları uygularken daha güçlü dil araçları sunuyor. Ancak kısa kod her zaman iyi tasarım anlamına gelmez; daha fazla sınıf da otomatik olarak daha iyi mimari oluşturmaz.
Bir deseni seçmeden önce kodun hangi nedenle değiştiğini, hangi bağımlılıkların sorun çıkardığını ve ekleyeceğiniz soyutlamanın gerçek faydasını değerlendirin.
İyi bir tasarım deseni, kodu karmaşık göstermek için değil, değişikliği daha kolay yönetmek için vardır.
Kaynaklar ve İleri Okuma
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994.
- Joshua Bloch — Effective Java, Third Edition, Addison-Wesley, 2018.
- Robert C. Martin — Clean Architecture, Prentice Hall, 2017.
- Oracle — Java SE 21 Language Documentation: https://docs.oracle.com/en/java/javase/21/language/
- OpenJDK — JEP 395, Records: https://openjdk.org/jeps/395
- OpenJDK — JEP 409, Sealed Classes: https://openjdk.org/jeps/409
- OpenJDK — JEP 441, Pattern Matching for switch: https://openjdk.org/jeps/441
- Refactoring.Guru — Design Patterns in Java: https://refactoring.guru/design-patterns/java