/ design-pattern

Design Patterns (Tasarım Kalıpları) - Singleton Pattern

Yazılım geliştirme süreçlerinde karşımıza çıkan sorunlara tekrar edilebilir çözümler sunan desenlerdir. Bir başka deyişle, bir probleme çözüm yaklaşımı için genel şablonlardır. Programlama dilinden bağımsız olma sebebi de budur. Bir yazılım hangi dille geliştiriliyor olursa olsun, karşılaşabileceğiniz sorunların bir çoğuna tasarım kalıpları bir çözüm sunar ve sorunların nasıl daha kolay çözebileceğini gösterir.

Tasarım kalıpları hayatımıza nesneye dayalı programlama (object-oriented programming) ile girdi diyebiliriz. Sınıf(class) ve nesnelerin(object) oluşum, ilklendirme, birleşirme ve iletişim kurması üzerine geliştirilmişlerdir. Daha geliştirilebilir ve kaliteli bir kod yazmamıza olanak sağlamakla kalmaz; aynı zamanda diğer yazılımcılarla okunabilirlik düzeyinde iyi bir iletişim kurmamızı sağlar.

3 ana başlık altında topluyoruz:

  • Yaratımsal tasarım kalıpları (creational design patterns)
  • Yapısal tasarım kalıpları (structural design patterns)
  • Davranışsal tasarım kalıpları (behavioral design patterns)

Bu yazımda yaratımsal tasarım kalıplarından "singleton" tasarım kalıbını inceleyeğiz. Örneklerde Java ve C# dilini kullanacağız.

Singleton Design Pattern

Bu kalıbı eğer bir sınıfın sadece bir nesnesi olmasını istiyorsak kullanıyoruz. Bu demektir ki; sınıf kendi hayat döngüsünden sorumludur. Yani yapıcı metodunun (constructor) private olması sayesinde, kendi içerisinde sınıf ilklendirmesi yapılır. Başka bir sınıf içerisinde new Singleton() ile nesne oluşturulamaz.

Yukarıdaki UML sınıf diagramında gördüğümüz gibi constructor ve singleton nesnesi private olarak belirtilmiş. Bu nesneyi sınıf dışarısından çağırabilmek için ise getInstance() metodu public olmak durumunda.

Bu kalıbın yukarıdaki tanımlamamıza uygun olabilmesi için aynı zamanda da thread-safe olması gerekmektedir. Yani çok çekirdekli işlemcilerde, birden fazla threadin aynı anda singleton nesnesi oluşturabilmesini istemiyoruz. Bu kalıbın doğası gereği, sadece bir nesne olması ve tüm mantığın bu nesne üzerinden dönmesi gerekmektedir. Peki bu konuyu biraz daha açalım ve kısaca bu problem nasıl oluşuyor, problemi nasıl giderebiliyoruz görelim.

java-volatile-1

Threadler daha hızlı çalışabilmek için değişkenleri/verileri öncelikle CPU cache içerisine alır ve o şekilde işlerler. Eğer iki thread de aynı anda hafızadan cpu cache içerisine veriyi alırsa ve thread 1 değişken üzerinde işlem yaparsa, bundan thread 2'nin haberi olmadığı bir durumla karşılaşabiliyoruz. Çünkü diğer thread 2 hala cache içerisindeki değerle çalışıyordur ya da thread 1 değişikliğini hafızaya geri yazmamıştır. Bunu gidermek için hem Java'da hem de C#'da volatile değişkenler kullanıyoruz. Bu kullanım, değer değişikliğinin anında ana hafızaya da yazılmasını ve o değişkenin her seferinde önce hafızadan alınmasını garanti ediyor. Ama bu tek başına yeterli olmaz. Çünkü iki thread de değişkenin değerini aynı anda değiştiriyor olabilir ve birinin yaptığı değişiklik ezilebilir. Dolayısıyla thread safe yapabilmek için, aynı zamanda de bu bloğa erişimi kısıtlamamız ve bir zamanda sadece bir threadin değişkeni manipüle edebilmesini, değiştirebilmesini sağlamalıyız. Bunun için ise java'da syncronized, C#'da ise lock mantığını kullanıyoruz.

Yukarıdaki bilgilere dayanarak bir singleton tasarım kalıbı örneği yazalım.


// SingletonClass.java

public class SingletonClass {

	private static volatile SingletonClass instance = null;

	private SingletonClass () {
		// private constructor. Sadece SingletonClass içerisinde kullanılabilir.
		if (instance != null){
			throw new RuntimeException("Use getInstance() method to create.");
		}
	}

	public static SingletonClass getInstance() {

		// eğer instance null değilse thread-safe olmasına bakmaksızın instance döndürülür.
		if (instance == null) {

			// eğer null ise artık bu bloğa eş zamanlı olarak birden fazla threadin
			// kilitlenmesine, erişmesine izin vermiyoruz.
			syncronized (SingletonClass.class){

				// yukarıdaki nedenden dolayıdır ki, bir kez daha instance'ın
				// null olma durumunu konrol ediyoruz
				if (instance == null){
					instance = new SingletonClass();	
				}
				
			}
		}

		return instance;
	}
}

Aynı sınıfın C# kullanarak yapılan implementasyonu ise;


// SingletonClass.cs

 public sealed class SingletonClass
{
    private static volatile SingletonClass instance;
    private static object _lockerObject = new object();
    private SingletonClass() { 
        // private constructor. Sadece SingletonClass içerisinde kullanılabilir.
    }

    public static SingletonClass getInstance()
    {
        // eğer instance null değilse thread-safe olmasına bakmaksızın instance döndürülür.
        if (instance == null)
        {
            // eğer null ise artık bu bloğa eş zamanlı olarak birden fazla threadin
            // kilitlenmesine, erişmesine izin vermiyoruz.
            lock (_lockerObject)
            {
                // yukarıdaki nedenden dolayıdır ki, bir kez daha instance'ın
                // null olma durumunu konrol ediyoruz
                if (instance == null)
                {
                    instance = new ApplicationState();
                }
            }
        }

        return instance;
    }
}

Sonuç Olarak

Bir nesneyi tekrar ve tekrar oluşturup kullanmamıza gerek olmaz. Örneğin; db connection gibi, logger vb. gibi. Tüm istemcilerin aynı instance üzerinde işlem yapmasını istediğimiz durumlarda singleton pattern bizim yardımımıza koşar. Düşününce aynı işlemi static sınıflar kullanarak da yapabiliriz ama bu bizim en basitiyle performans kaybımıza yol açar. Çünkü, program belleğe yüklendiği anda bellekte bu tip sınıflar oluşturulur. Aynı zamanda da OOP (nesneye dayalı programlama) ilkelerine de pek uyuşmaz. Bu yüzden single pattern kullanarak tüm uygulama üzerinden bir kere oluşturulup defalarca kullanılan bir nesne tanımlayabiliriz.

Bir sonraki yazımda yine bir creational pattern olan BuilderPattern'den bahsetmeye çalışacağım.