Singleton – Wzorzec Projektowy

Singleton jest wzorcem projektowym o którym napisano już bardzo dużo. Głównie można na internecie przeczytać o tym, że Singletonu to najlepiej unikać. Z mojego doświadczenia Singleton jest również częstym pytaniem podczas rozmowy na początkującego programisty.

James ma polską fabrykę, która zajmuje się produkcją części oraz składaniem z tych części hulajnóg.

public class Kierownica
{

}

public class Naklejka
{

}

public class Hamulce
{

}

public class Kolka
{

}
public class HalaZCzesci
{
	public Kierownica StworzKierownice()
	{
		var kierownica = new Kierownica();
		return kierownica;
	}

	public Naklejka StworzNaklejke()
	{
		var naklejka = new Naklejka();
		return naklejka;
	}

	public Hamulce StworzHamulce()
	{
		var hamulce = new Hamulce();
		return hamulce;
	}

	public Kolka StworzKolka()
	{
		var kolka = new Kolka();
		return kolka;
	}
}
var czesci = new HalaZCzesci();
var kierownica = czesci.StworzKierownice();
var naklejka = czesci.StworzNaklejke();
var hamulce = czesci.StworzHamulce();
var kolka = czesci.StworzKolka();

var skladanieHulajnogi = new HalaSkladajaca();
skladanieHulajnogi.Zloz(kierownica, naklejka, hamulce, kolka);
public class HalaSkladajaca
{
	public Hulajnoga Zloz(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka)
	{
		var hulajnoga = new Hulajnoga(kierownica, naklejka, hamulce, kolka);
		return hulajnoga;
	}
}

James w pewnym momencie stwierdził, że chciałby mieć dokładne raporty z każdej operacji, która miały miejsce u niego w fabryce. Wszystkie raporty mają być zapisane w jednym pliku w tym samym miejscu na dysku.

Aby sprostać wymaganiom James została zaimplementowana klasa Raport.

public class Raport
{
	private StreamWriter _writer;
	public Raport(string path)
	{
		_writer = new StreamWriter(path, true);
	}

	public void DodajDoRaportu(string informacje)
	{
		_writer.WriteLine(informacje);
	}

	public void ZamknijRaport()
	{
		_writer.Close();
	}
}

Dane do raportu będę dodawane w konstruktorach nowo tworzonych elementów hulajnogi. Za każdym razem razem kiedy będzie wywołany konstruktor od kierownicy, naklejki, hamulców, kółka oraz hulajnogi będą dodawane nowe informacje do raportu. Rozwiążmy ten problem najprościej jak się da.

public class Kierownica
{
	public Kierownica()
	{
		Raport raport = new Raport(@"C:\log.txt");
		raport.DodajDoRaportu("Stworzono kierownice");
		raport.ZamknijRaport();
	}
}
public class Hulajnoga
{
	...

	public Hulajnoga(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka, Raport raport)
	{
		_kierownica = kierownica;
		_naklejka = naklejka;
		_hamulce = hamulce;
		_kolka = kolka;

		Raport raport = new Raport(@"C:\log.txt");
		raport.DodajDoRaportu("Stworzono hulajoge");
		raport.ZamknijRaport();
	}
}

Rozwiązanie to jest kiepskiej jakości. Dlaczego?

Zmieńmy trochę aktualne rozwiązanie. Stwórzmy obiekt Raport w najwyższej warstwie w programu, a potem będziemy przekazywać referencje do obiektów niżej.

Raport raport = new Raport(@"C:\log.txt");
var czesci = new HalaZCzesci(raport);
var kierownica = czesci.StworzKierownice();
var naklejka = czesci.StworzNaklejke();
var hamulce = czesci.StworzHamulce();
var kolka = czesci.StworzKolka();

var skladanieHulajnogi = new HalaSkladajaca(raport);
skladanieHulajnogi.Zloz(kierownica, naklejka, hamulce, kolka);

raport.ZamknijRaport();
public class HalaZCzesci
{
	private Raport _raport;

	public HalaZCzesci(Raport raport)
	{
		_raport = raport;
	}

	public Kierownica StworzKierownice()
	{
		var kierownica = new Kierownica(_raport);
		return kierownica;
	}
}
public class Kierownica
{
	public Kierownica(Raport raport)
	{
		raport.DodajDoRaportu("Stworzono kierownice");
	}
}
public class HalaSkladajaca
{
	private Raport _raport;

	public HalaSkladajaca(Raport raport)
	{
		_raport = raport;
	}

	public Hulajnoga Zloz(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka)
	{
		var hulajnoga = new Hulajnoga(kierownica, naklejka, hamulce, kolka, _raport);
		return hulajnoga;
	}
}
public class Hulajnoga
{
	...

	public Hulajnoga(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka, Raport raport)
	{
		_kierownica = kierownica;
		_naklejka = naklejka;
		_hamulce = hamulce;
		_kolka = kolka;
		raport.DodajDoRaportu("Zlozono hulajoge");
	}
}

Rozwiązanie lepsze, ale spójrz proszę ile pracy nas to kosztowało. Wpierw tworzę Raport potem przekazuję go do klasy HalaZCzesciami. Potem z HaliZCzesciami przekazuję obiekt Raport do kierownicy, hamulców itd. Muszę pamiętać też, aby przekazać Raport  do HaliSkladajacej, a z Hali składającej do konstruktora Hulajnogi. Dużo pracy. Czy można prościej?

Singleton jako rozwiązanie problemu

Singleton wymusza dzięki składni języka możliwość stworzenia tylko jednej instancji obiektu danej klasy.

public class Raport
{
	private StreamWriter _writer;
	private static Raport _raport;
	private Raport()
	{           
		_writer = new StreamWriter(@"C:\log.txt", true);
	}

	public static Raport GetInstance()
	{
		if (_raport == null)
			_raport = new Raport();

		return _raport;             
	}

	public void DodajDoRaportu(string informacje)
	{
		_writer.WriteLine(informacje);
	}

	public void ZamknijRaport()
	{
		_writer.Close();
	}
}

W tym momencie nie można po prostu napisać w programie konstruktora klasy Raport, ponieważ konstruktor jest prywatny. Aby dostać instancję klasy należy wywołać metodę statyczną GetInstance.

var raport = Raport.GetInstance();

Metoda GetInstance  sprawdza, czy pole _raport jest nullem. Czyli innymi słowy sprawdzane jest czy obiekt klasy Raport  był już zainicjowany. Jeżeli nie był, to tworzony jest pierwszy i ostatni raz  w czasie działania tego programu. Następnie instancja jest przypisana do pola _raport . Każde kolejne wywołanie metody GetInstance zwróci już istniejącą już instancję klasy Raport.

Dzięki takiemu rozwiązaniu możemy użyć takiego rozwiązania zapisywania do pliku informacji:

public Kierownica()
{
	var raport = Raport.GetInstance();
	raport.DodajDoRaportu("Stworzono kierownice");
}

Wywoływana jest metoda GetInstance, która zwraca instancję klasy Raport, a następnie wykonujemy na niej operację.

Dzięki temu nasz program może wyglądać teraz tak:

var czesci = new HalaZCzesci();
var kierownica = czesci.StworzKierownice();
var naklejka = czesci.StworzNaklejke();
var hamulce = czesci.StworzHamulce();
var kolka = czesci.StworzKolka();

var skladanieHulajnogi = new HalaSkladajaca();
skladanieHulajnogi.Zloz(kierownica, naklejka, hamulce, kolka);

var raport = Raport.GetInstance();
raport.ZamknijRaport();
public class HalaZCzesci
{
	public Naklejka StworzNaklejke()
	{
		var naklejka = new Naklejka();
		return naklejka;
	}
	
	...
}
public class Naklejka
{
	public Naklejka()
	{
		var raport = Raport.GetInstance();
		raport.DodajDoRaportu("Stworzono naklejke");
	}
}
public class HalaSkladajaca
{
	public Hulajnoga Zloz(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka)
	{
		var hulajnoga = new Hulajnoga(kierownica, naklejka, hamulce, kolka);
		return hulajnoga;
	}
}
public class Hulajnoga
{
	...

	public Hulajnoga(Kierownica kierownica, Naklejka naklejka, Hamulce hamulce, Kolka kolka)
	{
		_kierownica = kierownica;
		_naklejka = naklejka;
		_hamulce = hamulce;
		_kolka = kolka;

		var raport = Raport.GetInstance();
		raport.DodajDoRaportu("Zlozono hulajoge");
	}
}

Singleton jest wzorcem kontrowersyjnym, który może budzić różnego rodzaju pytania.

Kiedy używać Singletonu?

Wiele przeczytałem na Internecie o tym jaki ten wzorzec projektowy jest zły i niedobry, bo:

  • Łamie Zasadę Jednej Odpowiedzialności,
  • Powoduje, że kod jest trudny do testowania,
  • Jest jak zmienna globalna, klasa statyczna, co uchodzi ogólnie za złą praktykę,
  • Występuje tak zwany tight coupling.

Skoro taki wzorzec projektowy powstał musi mieć gdzieś swoje zastosowanie. Więc przyjrzyjmy się jakie założenia musi spełniać kandydat na Singletona:

  • Kontroluje dostęp do współdzielonego w programie zasobu (np. plik)
  • Dostęp do zasobu jest potrzebny w różnych miejscach w systemie (potrzebny jest w hali z częściami, oraz w miejscu gdzie składane są hulajnogi)
  • Może istnieć tylko jedne obiekt

Jaka jest różnica pomiędzy Singletonem, a klasą statyczną?

  • Singleton może implementować interfejs, a klasa statyczna nie. Dzięki temu Singleton może być przekazany potem jako parametr i traktować go jako zwykłą klasę (oszczędzamy czas na wywołanie metody GetInstance)
  • Singleton można dziedziczyć
  • Singleton działa na zasadzie lazy loading, a klasa statyczna jest tworzona zaraz przy kompilacji programu
  • Singleton ma konstruktor, klasa statyczna nie
  • Singleton może mieć tylko jedną instancję, dzięki czemu można łatwiej kontrolować instancję, klasa statyczna nie ma żadnego stanu

 

Singleton – przydatne linki

Różnica pomiędzy klasą statyczną, a Singletonem, nr 1

Różnica pomiędzy klasą statyczną, a Singletonem, nr 2

Różnica pomiędzy klasą statyczną, a Singletonem, nr 3

Kiedy używać Singletonu?

Leave a Comment

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *