Singleton deseni ile bir sınıftan sadece tek bir nesne oluşmasını ve her yerde o nesnenin kullanmasını sağlayabiliriz. Singleton deseni uygulanmış bir sınıftan birden fazla nesne/örnek oluşturmak mümkün değildir. Bu sınıfların bellekte tek bir örneği(nesnesi) oluşturulur ve uygulamamız çalıştığı sürece o nesne bellekte muhafaza edilir.
Singleton Tasarım Desenini Ne Zaman Uygulamalıyız?
Bir sınıftan oluşturacağımız nesne;
- İçerisinde kullanıcıya özel veri tutmuyorsa,
- Uygulama içerisinde sürekli kullanılıyorsa,
O sınıfa Singleton tasarım desenini uygulayabiliriz. Ancak gerçekten bu desenin uygulanmasının gerekli olup/olmadığına aşağıdaki kriterlere bakılarak karar verilebilir.
- Sınıfın bellekte tek bir örneği oluşturulur ve tüm kullanıcılar o örneğe erişir.
Sınıftan sadece tek bir nesne yaratılacağı için o nesneye erişen tüm kullanıcılar aynı verilere ulaşacaktır. Bu yüzden nesne içerisinde kullanıcıya has bilgiler bulunmamalıdır.
- Oluşturulan nesne uygulamamız çalıştığı sürece bellekte muhafaza edilir.
Normalde bir sınıftan oluşturduğumuz her bir nesne sadece kullanıldığı sürece bellekte yer kaplar. O nesnelerin referansları yok olduğunda yani artık kullanılamayacakları zaman .NET’de Garbage Collector (Bkz: Garbage Collector) devreye girip, o nesneleri bellekten silmektedir. Ancak Singleton deseni uygulanmış sınıftan oluşturulan nesne kullanılmasa dahi bellekte tutulmaya, yer kaplamaya devam eder. Bu yüzden uygulama içerisinde nadir kullanılan sınıflara Singleton desenini uygulamak, sürekli kullanılmamalarına rağmen bellekte sürekli yer işgal etmelerine neden olacaktır.
Diyelim ki geliştirmekte olduğunuz bir web uygulamasında, içinde kullanıcıya özel bilgi barındırmayan ve sürekli kullanılması gereken bir sınıfınız var. Bu sınıfa Singleton deseni uygulanmadığı takdirde, web uygulamasını kullanan her bir kullanıcı için bellekte en az bir tane o sınıftan nesne oluşturulacaktır. Web uygulamanızı kullanan 1000 kullanıcı var ise, en az 1000 tane nesne oluşturulacak demektir. Ziyaretçi sayısı arttıkça bu durum sorun yaratmaya başlayacaktır. Ancak Singleton deseni uygulandığı takdirde; o sınıftan sadece tek bir nesne oluşturulacak ve tüm kullanıcıların oluşturulmuş olan o nesneyi kullanması sağlanacaktır.
Singleton Tasarım Desenini Nasıl Uygularım?
Basit bir örnek üzerinden Singleton tasarım deseninin nasıl uygulandığını inceleyelim.
Örnek Senaryo: Uygulamamızın çalışması esnasında ortaya çıkan hataları ve bir olayın olduğu zamanla ilgili verileri loglamak için kullanacağımız bir Logger sınıfımızın olduğunu düşünelim.
Amacımız Singleton tasarım deseninin uygulanışını öğrenmek olduğundan; kod kalabalığını arttırmamak ve konuyu daha anlaşılır bir halde aktarmak için; Logger sınıfını log mesajlarını konsola basacak şekilde basitçe hazırladım.
public class Logger { public void Info(string message) { Console.WriteLine($"Info - {message}"); } public void Error(string message) { Console.WriteLine($"Error - {message}"); } }
Logger sınıfını incelediğimiz zaman içerisinde kullanıcıya özel bir veri tutmadığını ve tek görevinin log mesajlarını konsola basmak olduğunu görmekteyiz. Ayrıca loglama, projemizin içerisinde yer alan tüm sınıflarda ihtiyaç duyduğumuz bir gereksinimdir. Her sınıf içerisinde Logger sınıfını new()‘leyip yeni bir nesne yaratmak yerine, ona Singleton desenini uygulamamız çok daha doğru bir yaklaşım olacaktır.
- Adım: Sınıfın new()’lenmesinin Önüne Geçmek
Amacımız Logger sınıfından tek bir nesne yaratmak ve her yerde o nesneyi kullanmak olduğundan; Logger sınıfının diğer sınıfların içerisinden new()‘lenmesinin önüne geçmemiz gerekiyor.
public class Logger { // Yapıcı metodu private olan sınıflar sadece kendi içerisinden new()'lenebilir. private Logger() { } public void Info(string message) { Console.WriteLine($"Info - {message}"); } public void Error(string message) { Console.WriteLine($"Error - {message}"); } }
NOT: Yapıcı metodu private yapılan bir sınıf, sadece kendi içerisinden new()‘lenebilir. Farklı bir sınıf içerisinde bu sınıfın new()‘lenmesi mümkün değildir.
Artık herhangi bir sınıf içerisinde, aşağıdaki şekilde Logger sınıfını new()‘lemek istediğimiz zaman, IDE uyarı verecek ve proje derlenmeyecektir.
Logger logger = new Logger();
2. Adım: Sınıftan Tek Bir Nesne Oluşmasını Sağlamak
Başka sınıflardan Logger nesnesi oluşturulmasını bir önceki adımda engellemiştik. Şimdi Logger sınıfı içerisinde, kendi nesnesini yaratacak bir metot hazırlayacağız.
public class Logger { private static Logger _logger; private Logger() { } public static Logger CreateAsSingleton() { if(_logger == null) { _logger = new Logger(); } return _logger; } public void Info(string message) { Console.WriteLine($"Info - {message}"); } public void Error(string message) { Console.WriteLine($"Error - {message}"); } }
CreateAsSingleton() isminde static bir metot oluşturduk. Metot static olduğu için Logger sınıfından nesne oluşturmadan doğrudan bu metodu çağırabilmekteyiz (Bknz: C# – Static Elemanlar ve Sınıflar). Bu metot ilk çağrıldığında _logger referansı null olacağı için; if bloğu içerisine girip Logger sınıfından yeni bir nesne yaratacak ve adresini _logger referansına atayacaktır. Sonraki CreateAsSingleton() metodunun çağrımlarında _logger referansı, Logger nesnesinin adresi içerdiğinden; CreateAsSingleton() metodu ilk çağrıldığında oluşturulan Logger nesnesini döndürecektir.
class Program { static void Main(string[] args) { // CreateAsSingleton() metodu ilk kez çağrıldığı için Logger sınıfından bir nesne yaratıp geriye döndürür. Logger logger1 = Logger.CreateAsSingleton(); // Daha önce yaratılan Logger nesnesini geri döndürür. Logger logger2 = Logger.CreateAsSingleton(); } }
3. Adım: Multi Thread Uygulamalarda Birden Fazla Nesne Oluşmasının Önüne Geçmek
Uygulamamızı ilk ayağa kaldırdığımızda aynı anda iki thread’in CreateAsSingleton() metodunu çağırdığını düşünelim. Eğer öncesinde CreateAsSingleton() hiç çağrılmamışsa, _logger referansı null olacağından; iki thread’de aynı anda if(_logger == null) bloğuna girip, iki farklı Logger nesnesi oluşturacaktır.
public class Logger { private static Logger _logger; private static object _lockObject = new object(); private Logger() { } public static Logger CreateAsSingleton() { lock (_lockObject) { if (_logger == null) { _logger = new Logger(); } } return _logger; } public void Info(string message) { Console.WriteLine($"Info - {message}"); } public void Error(string message) { Console.WriteLine($"Error - {message}"); } }
İki farklı nesne oluşmasını önlemek için Lock anahtar sözcüğünü kullanıyoruz. Lock sayesinde; aynı anda yalnızca bir thread’in kod bloğu içerisine girmesine izin veriyoruz. Böylece CreateAsSingleton() metodu aynı anda iki thread tarafından çağrılsa bile, Lock bloğunda threadlerden biri, diğer thread’in işi tamamlanıncaya kadar beklemeye alınacaktır. İlk thread’in işi tamamlandığında _logger referansına bir Logger nesnesinin adresi atanmış olacağından, ikinci thread if(_logger == null) kod bloğuna girmeyecek ve yeni bir nesne oluşumu önlenmiş olunacaktır.
Serdar YILMAZ