Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.
Co to singleton?
Singleton jest kreacyjnym wzorcem projektowym, który tworzy i zakłada istnienie tylko jednej instancji tworzonego obiektu na cały system. Stworzony przez singletona byt jest dostępny globalnie, co powoduje wiele problemów związanych z tym wzorcem. Sam singleton ma przypiętą łatkę antywzorca. Dzieje się tak, ponieważ łamie on zasady SOLID (z uwagi m.in. na realizację więcej niż jednego zadania jednocześnie), wprowadza globalny stan, a także jest trudny w testowaniu.
Kiedy używać singleton?
Pomimo wad tego wzorca, istnieje kilka sytuacji, kiedy można zastanowić się nad jego zastosowaniem. Pierwszą z nich jest dobrze każdemu znany przykład połączenia z bazą danych. Zazwyczaj chcemy, aby istniało tylko jedno połączenie z bazą, w tym samym czasie. Do kontroli tego czy istnieje tylko jedna instancja doskonale nada się singleton. Po drugie, kiedy potrzebujemy dokładnej kontroli dostępu do zmiennych globalnych.
Powinniśmy jednak wystrzegać się używania singletonów. Pomijając oczywiste rzeczy (SOLID, stan globalny), testowanie jednostkowe jest bardzo utrudnione, gdyż konstruktor klasy jest prywatny, co praktycznie uniemożliwia automatyczne mock-owanie. Zamiast posiadania globalnych singletonów, lepiej użyć wstrzykiwania zależności i ograniczyć ilość instancji na samym mechanizmie DI (dependency injection).
Jak zaimplementować singleton?
Implementacja tego wzorca sprowadza się do posiadania prywatnego konstruktora klasy, tak, aby możliwość utworzenia nowej instancji dostępna była jedynie z poziomu jej samej. Dodatkowo, należy zdefiniować statyczną metodę, która posłuży do tworzenia obiektu. Metoda ta, w swoim ciele powinna sprawdzać czy klasa została już zainicjalizowana i tworzyć nowy obiekt tylko, jeśli on jeszcze nie istnieje. Na sam koniec, statyczna funkcja kreująca musi zwrócić nowo utworzony, lub już istniejący, zapisany w statycznym polu klasy obiekt.
class DbConnection {
private static connection?: DbConnection;
private constructor() {
// real connection implementation
}
static create(): DbConnection {
if (!DbConnection.connection) {
DbConnection.connection = new DbConnection();
}
return DbConnection.connection;
}
}
Oczywiście, samo utworzenie połączenia raczej nic nam nie da. Musimy być w stanie coś z nim zrobić. W obrębie klasy połączenia możemy stworzyć zwykłe metody, np. do uruchamiania zapytań czy zamykania komunikacji.
class DbConnection {
private static connection?: DbConnection;
private constructor() {}
static create(): DbConnection {
if (!DbConnection.connection) {
DbConnection.connection = new DbConnection();
}
return DbConnection.connection;
}
run(sql: SQL) {
// implementation
}
close() {
DbConnection.connection = null;
}
}
Podsumowanie
- Singleton pozwala na posiadanie jednej instancji klasy w obrębie całego systemu.
- Jest to antywzorzec, z uwagi na fakt łamania SOLID-a, wprowadzenie globalnego stanu czy trudne w napisaniu testy jednostkowe.
- Zawsze warto dwukrotnie zastanowić się zanim zaimplementujemy coś w ten sposób.
Dodaj komentarz