Singleton Pattern

Nesne yaratma süreçleri ile ilgilenen son kalıbımız olan singleton pattern bu yazımızın konusu olacaktır. Singleton tasarım kalıbı en sık kullanılan kalıplardandır. Özellikle spring gibi dependency injection yönetimi yapan frameworklerin popülerliği arttıkça, farkında olmadan da kullandığımız bu kalıbın temelinde, bir sınıfa ait birden fazla nesne yaratılamaması prensibi yatmaktadır. Bu cümle size “O sınıfa ait tanımlı bir nesne varsa, önce onu yok edip sonra yenisini yaratabiliriz” hissiyatı verebilir. Lakin buradaki durum, genel çözüm ve kullanıma baktığımızda “O sınıfa ait bir nesne yaratıp, onu programın hayat döngüsü boyunca kullanmak” şeklinde vuku bulur.

Uygulama yazarken sıkça kullandığımız araçlardan bir tanesi konfigürasyon dosyalarıdır. Mesela veri tabanımıza erişim bilgilerini bir dosyaya yazar ve o dosyadan okuruz. Dosyadan gelen bilgiler sayesinde veri tabanı bağlantımızı kurarız. Bunun gibi farklı konfigürasyonlarımız da bulunabilir. Bu konfigürasyonlar durumdan duruma değişmeyen, t anında uygulamanın bütün noktalarını ilgilendiren değerler olabilir. Fiziksel bir örnek vermek bu durumu daha net anlamamıza yardımcı olacaktır.

Bir binanın güvenliğinden sorumlu olduğunuzu düşünün. Prosedürler gereği binada kimin olduğunu her an bilme ihtiyacı olduğunu varsayalım. Güvenlik ekibinin 10 kişilik bir ekip olduğunu, bina kapasitesinin ise 400 kişi olduğunu düşünün. Bir problem anında bu 10 kişilik ekibin binadaki bütün insanları tahliye ettiğini düşünelim. Peki ya bu panik anında kimin içeride kimin dışarıda olduğunu nasıl anlarız? Daha da önemlisi 10 kişilik güvenlik ekibinin tamamı aynı anda bu bilgiye erişebilir mi? Elbette güvenlik personelinin ayrı ayrı insanları sayması akıllıca olmayacaktır. Haliyle insanları, çeşitli cihazlar sayesinde, içeri girerken ve dışarı çıkarken tespit edip, kişilerin tespit edilmiş durumunu (içeride veya dışarıda) bir merkezi sistemde saklamak akıllıca olacaktır. Daha da önemlisi merkezi sisteme bağlanan client cihazları güvenlik ekibine dağıtırsak, tüm ekip aynı anda aynı bilgiye sahip olur. Bu durumda anlaşılır ki, “Kişi listesinin tek bir yerde olması” önem teşkil eden konu başlığıdır.

Yukarıdaki örneği biraz daha java ile ifade etmeye yaklaşırsak, birden fazla instance oluşturulamayan bir sınıf ile durumu yönetmeye ihtiyacımız vardır. O halde bazı önlemlere ihtiyacımız olacaktır. Bilindiği gibi javada nesne yaratma işlemleri yapıcı-constructor methodlar üzerinden gerçekleşmektedir. Hiçbir yapıcı method tanımı olmasa bile java bize varsayılan olarak bir tane vermektedir. Yani biz her daim nesne oluşturmak adına bir yönteme sahibiz. Bu kalıp ile bu durumun önüne geçmek istiyoruz. Zira singleton tasarım kalıbında olmasını istediğimiz sınıfın constructor methodu her yerden çağırılamamalıdır. Bir methodun çağırılabileceği yerleri kısıtlamak için erişim düzenleyiciler üzerinden fikir yürütebiliriz. Bilindiği gibi private anahtar sözcüğü erişim düzenleyiciler arasında en kısıtlayıcı olandır.

private : En sıkı erişim düzenleyicisidir. Önüne konulduğu java elemanını sadece o sınıf içerisinden erişilebilir hale getirir. 

Bu bilgiye istinaden constructor seviyesinde private çalışmamız gerektiği çıkarımını yapabiliyoruz. Fakat constructor private ise, o sınıftan nasıl nesne yaratacağız? Bu noktada static yapılar‘dan da faydalanmamız gerekir.

Static üyeleri kullanmak için o sınıftan bir instance yaratmaya ihtiyaç yoktur. Bu da static üyelerin, o sınıfa ait bütün instancelarda değerinin veya işleyişinin aynı kalması anlamına gelmektedir.

Genel isimlendirme anlayışına uygun olarak singleton pattern kullanan sınıflarda o sınıfın örneğini getInstance() methodu döner. Bizde buna uygun olarak sınıfımızı kodlayacağız. Yukarıdaki bilgilerle birlikte singleton pattern ile uyumlu örneğimizi paylaşıyorum.

import java.util.HashMap;

public class Config {

	private HashMap<String, String> config;
	
	private static Config instance = new Config();

	private Config() {
		
		config = new HashMap<String, String>();
		config.put("status", "0");
		config.put("maxThreadCount", "10");
		config.put("instance", "app_server");
		config.put("port", "1881");
		
	}
	
	public static Config getInstance() {
		return instance;
	}

	public HashMap<String, String> getConfig() {
		return config;
	}

	public void setConfig(HashMap<String, String> config) {
		this.config = config;
	}
	
}

Sınıf içerisinde dikkatinizi çekmesi gereken noktalara tekrar göz atalım. Öncelikli olarak constructor method private ile tanımlanmış. Bu da bu yapıcı fonksiyonun sınıf dışından çağırılamayacağı anlamına geliyor. Ancak sınıfın bir örneğine (O sınıfa ait bir instance) ihtiyacımız var. Bu ihtiyaç ise private ve static bir saha olarak ile giderilmiş durumda.

private static Config instance = new Config();

Bu sahanın static olması onu özellikli bir noktaya koyar. Bunu java uygulamamız ayağa kalkarken 1 kere çalışıp işini gerçekleştiren bir satır olarak düşünebilirsiniz. Dolayısı ile bu saha uygulamanın açılışı esnasında, yapıcı methodu çağırarak temsili olarak oluşturduğumuz config sahasını initialize eder. Dikkat edilirse sahanın tipi, içinde bulunduğu sınıfın aynısıdır.

Son olarak getInstance() methodumuza bakalım. Bu method public ve statictir. Method public olmasaydı bu örneğimiz anlamsız olurdu. Zira biz nesneye ulaşmayı değil, onu tekrar tekrar yaratmayı engellemek istiyoruz.

public static Config getInstance() {
	return instance;
}

Bir test sınıfı vasıtasıyla örneğimizi çalıştırıp çıktılarına bakalım. Örnek içerisinde birbirinden ayrı gibi gözüken 2 nesnenin birbirlerini nasıl güncellediğini gözlemleyeceğiz. Ayrıca örnek içerisinde kalıba uygun olarak hiçbir yapıcı fonksiyon çağrısı yapmadığımızı göreceksiniz.

import java.util.Map;

public class Test {

	public static void main(String[] args) {
		
		//Doğrudan kullanım
		Config config1 = Config.getInstance();
		Map<String, String> configs1 = config1.getConfig();
		System.out.println("configs1 -> " + configs1.toString());
		
		//Başka bir kullanım 
		Config config2 = Config.getInstance();
		Map<String, String> configs2 = config1.getConfig();
		System.out.println("configs2 -> " + configs2.toString());
		
		//Nesnedeki verinin değiştirilmesi
		configs2.put("port","1938");
		System.out.println("configs2 -> " + configs2.toString());
		
		//Başka nesne vasıtasıyla değiştirilen verinin yayılması 
		System.out.println("configs1 -> " + configs1.toString());
	}
}
Çıktı:
configs1 -> {maxThreadCount=10, instance=app_server, port=1881, status=0}
configs2 -> {maxThreadCount=10, instance=app_server, port=1881, status=0}
configs2 -> {maxThreadCount=10, instance=app_server, port=1938, status=0}
configs1 -> {maxThreadCount=10, instance=app_server, port=1938, status=0}

Singleton pattern ile ilgili olarak değinebileceğimiz bir başka konu ise bu kalıbı lazy olarak yönetmektir. Eğer lazyness kavramı ile daha evvel karşılaşmadıysanız, e-ticaret veya haber sitelerinde biraz gezmeniz size fikir verecektir. Bu kavram “İhtiyaç oldukça yükle” anlayışına dayalı olarak hareket etmeyi temsil eder. Bazı e-ticaret sitelerinde sayfada aşağı indikçe yeni ürünler yüklendiğini görürüz. Böylece kesintisiz bir sayfa deneyimi sunulurken, performans noktasında da tüm kayıtların bir anda çekilmemesi sayesinde verimli bir çıktı alırız. Singleton tasarım kalıbında da aynı disiplini izlemek mümkündür. Ulaşmak istediğimiz instance, biz ihtiyaç duymadıkça oluşmadan bekleyebilir. Az önceki örnekte statik saha ile kodun programın çalışmasıyla tetiklendiğini söylemiştik. Şimdi ise bunu lazy hale getirelim.

import java.util.HashMap;

public class Config {

	private HashMap<String, String> config;
	
	private static Config instance;

	private Config() {
		
		config = new HashMap<String, String>();
		config.put("status", "0");
		config.put("maxThreadCount", "10");
		config.put("instance", "app_server");
		config.put("port", "1881");
		
	}
	
	public static Config getInstance() {
		
		if (instance ==null) {
			instance = new Config();
		}
		
		return instance;
	}

	public HashMap<String, String> getConfig() {
		return config;
	}

	public void setConfig(HashMap<String, String> config) {
		this.config = config;
	}
	
}

Yapılan değişiklik getInstance() methodu ve instance sahasının olduğu satırdadır. Buraya dikkatli baktığımızda görürüz ki instance sahasına doğrudan bir değer ataması yapılmamıştır. Bu da başlangıçta sahanın null olarak oluşması anlamına gelir. Fakat getInstance() methodunun içerisinde bu durum denetlenmiştir. Test kodumuz çalıştırıldığında da birebir aynı çıktıyı alırız. Bu tarz bir yaklaşım, özellikle uygulamanızın ayağa kalkması esnasında süreleri kısaltmak ve bellek yönetimini iyileştirmek için kullanılabilir.

Bir sınıfa ait tek bir instance olduğunu garanti etmek için kullanılan tasarım kalıbımız olan singleton patterni bu yazımızda paylaştık. İhtiyaç duyulan nesnenin tek adımda yaratılıp sonrasında yenisi ile değiştirilemediğini 2 ayrı örnekle görmüş olduk. Bu kalıbımızın bazı olumsuzlukları da mevcuttur. Örneğin unit test yazma noktasında sınıfın mocklanması mümkün olmadığından, alternatif testlerinizde sorun yaşatabilir. Thread güvenliği noktasında ise aynı anda akan iki isteğin birbirini manipüle edebilmesi olasılığı çok yüksektir. Ancak bütün bunlara rağmen bellek yönetimi noktasında çok ciddi katkılar sağlayabilen bu kalıp, en popüler creational kalıplardandır.

Leave a Reply

Your email address will not be published. Required fields are marked *