Yazılım dünyasında bazı yapılar sadece bir kez oluşturulmak istenir. Örneğin:
1. Bir uygulama boyunca tek bir log dosyasıyla çalışmak,
2. Tek bir veritabanı bağlantısı kullanmak,
3. Ayarlar veya konfigürasyonlar gibi nesnelerin yalnızca bir örneğe sahip olması.
Singleton Design Pattern ihtiyaçtan ötürü oluşturulmuş bir yazılım tasarım desenidir. Bir sınıfın sadece bir örneği (instance) olmalı ve bu örneğe tüm sistemden erişilebilmelidir.
Eager Singleton (İlk Nesil Singleton)
İlk Singleton uygulamaları oldukça basitti. Uygulama başlatılırken nesne oluşturulur. Sonrasında nesneye ulaşılması istenildiği zaman oluşturulan nesne döndürülürdü.
public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return instance; } }
Sorun Ne?
Eğer bu nesne hiç kullanılmayacaksa bile uygulama başladığında bellekte yer kaplar. Özellikle büyük kaynak tüketen nesneler için bu ciddi bir israf olabilir.
Lazy Singleton (Tembel Ama Akıllı)
Bu sorunu çözmek için, “İhtiyaç duyulana kadar oluşturma!” yaklaşımı geliştirildi:
public class LazySingleton { private static LazySingleton instance; private LazySingleton() {} public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
Peki Bunun Sorunu Ne?
Bu yapı çoklu thread ortamında hataya sebep olabilir. İki farklı thread aynı anda getInstance() metodunu kullanırsa, iki farklı nesne oluşabilir. Çünkü iki thread, daha nesne oluşturulmamışken nesne oluşturabilir. Singleton mantığı bozulur.
Mutex (Synchronized Singleton)
mutex gibi bir senkronizasyon mekanizmasıyla bu durum çözülebilir.
public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton() {} public static synchronized ThreadSafeSingleton getInstance() { if (instance == null) { instance = new ThreadSafeSingleton(); } return instance; } }
Bu da Sorunlu…
Evet thread-safe, ama her çağrıda kilitleniyor. Performansı etkiler. Tek bir örnek zaten oluşturulduktan sonra kilide gerek yok ama burada her defasında kilitleniyor.
Double-Checked Locking Pattern
Performans kaybını önlemek için çift kontrol (double-checked locking) getirildi:
public class DCLSingleton { private static volatile DCLSingleton instance; private DCLSingleton() {} public static DCLSingleton getInstance() { if (instance == null) { synchronized (DCLSingleton.class) { if (instance == null) { instance = new DCLSingleton(); } } } return instance; } }
Bu yapı sayesinde:
- Nesne yalnızca gerekirse oluşturulur.
- İlk kontrolle gereksiz kilit önlenir.
- İkinci kontrolle thread-safe olur.
Bu durum bile aslında programlama dillerinin yapısı yüzünden ilk geliştirildiği zamanda istenilen sonucu vermemiştir. Bu durum programlama dillerinin eski yapısından kaynaklanır. İstenilen sonuçların alınması için volatile anahtar kelimesi kullanılmaya başlanmıştır. Durumun detayları bu yazı için konu dışına çıkmaktadır.
Peki O Zaman Neden Singleton Anti-Pattern Oldu?
Zamanında çok sevilen Singleton, artık çoğu durumda antipattern (yani kötü örnek) olarak görülüyor. İşte sebepleri:
1. Global State'e sebep olur. Test edilmesi zorlaşır.
2. Bağımlılığı gizler, dependency injection’a aykırıdır.
3. Kodun başka yerlerinde etkisini kontrol etmek zorlaşır (örneğin: sessizce değişen bir ayar).
4. Gerçek bir “tekil nesne” gerektiren durumlar çok nadirdir.
5. Paralel programlama ile başı derttedir.
6. Gereksiz karmaşıklık ekler.
Özetle
Singleton, küçük bir problemi çözmek için doğdu ama zamanla daha büyük problemlere neden olmaya başladı. Bugün modern yazılım geliştirmede:
- Eğer illa tek bir nesne gerekiyorsa, Dependency Injection + Scope kullanmak tercih ediliyor.
- Singleton’dan ziyade stateless servisler daha ön planda.