Singleton często nazywany jest antywzorcem. Pytanie brzmi, czy słusznie? Tego oraz tego jak wykorzystać Singletona w swojej aplikacji dowiesz się z tego artykułu. Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Struktura
Na sam początek przedstawię Ci strukturę wzorca projektowego Singleton w postaci diagramu klas, która do szczególnie skomplikowanych nie należy. Już sam diagram powinien powiedzieć Ci wiele o charakterystyce oraz potencjalnych zastosowaniach tego wzorca.
Klasa Singleton posiada prywatną statyczną property typu Singleton, która przechowuje instancję Singletona. Oprócz tego klasę wyróżnia prywatny konstruktor oraz publiczna statyczna metoda getInstance()
. Jak sama nazwa wskazuje, służy on do otrzymywania instancji. Oprócz tego powinny oczywiście istnieć metody odpowiadające celowi istnienia danego Singletona.
class Singleton {
private static _instance: Singleton | null = null;
// Prevent creating new instances.
private constructor() {}
public static getInstance(): Singleton {
if( this._instance === null ) {
this._instance = new Singleton();
}
return this._instance;
}
}
Mając już diagram i przykładowy kod czas przejść do charakterystyki tego wzorca. Główną cechą charakterystyczną Singletona jest limitowana liczba obiektów klasy, jaką możemy stworzyć. Ściślej mówiąc, ta liczba to jeden. Dokładnie tyle instancji tej klasy może maksymalnie istnieć w systemie. Co więcej, tak jak już wspomniałem wcześniej, konstruktor posiada prywatny modyfikator dostępu, co oznacza, że nie ma możliwości stworzenia instancji klasy spoza niej samej. Jedynym sposobem na pozyskanie obiektu, jest skorzystanie z metody getInstance()
. Poniższy nie zadziała i przedstawia niewłaściwy sposób inicjalizacji Singletona.
const singleton = new Singleton(); // Do not do that!
W czystym JavaScripcie problem ten można rozwiązać poprzez rzucenie Errora w konstruktorze, w momencie gdy instancja istnieje. TypeScript nie pozwoli na zbudować pliku z takim fragmentem kodu.
Zastosowanie
Oczywistym zastosowaniem, jakie się nasuwa, są miejsca, gdzie może lub też powinna istnieć tylko jedna instancja danej klasy. Przykładem mogą być globalne configi lub też połączenia z bazą danych.
Innym, bardzo ciekawym przykładem zastosowania może być formatter dat w JavaScript/TypeScript. Nie tak dawno temu, kilkukrotnie natknąłem się na informację, że formatowanie daty i czasu za pomocą Intl.DateTimeFormat
nie pozostaje całkowicie obojętne dla wydajności aplikacji. Jednocześnie nie istnieje powód, dla którego tworzenie kilku instancji byłoby konieczne. W systemie zwykle obowiązuje tylko jeden format daty i czasu (a przynajmniej jeden dla danego użytkownika).
class DateTimeFormatter {
private static _instance: DateTimeFormatter | null = null;
private _intl: Intl.DateTimeFormat;
// Prevent creating new instances.
private constructor() {
console.log( 'Intl.DateTimeFormat instance created.' );
this._intl = new Intl.DateTimeFormat( 'en-US' );
}
public static getInstance(): DateTimeFormatter {
if( this._instance === null ) {
this._instance = new DateTimeFormatter();
}
return this._instance;
}
public format( date: Date ): string {
return this._intl.format( date );
}
}
const formatter: DateTimeFormatter = DateTimeFormatter.getInstance();
const formattedDates: string[] = [];
formattedDates.push( formatter.format( new Date( '1990-01-01' ) ) );
formattedDates.push( formatter.format( new Date( '2018-01-04' ) ) );
formattedDates.push( formatter.format( new Date( '2003-02-12' ) ) );
console.log( formattedDates );
Logi z powyższej aplikacji wyglądają następująco.
[LOG]: Intl.DateTimeFormat instance created. [LOG]: [ "1/1/1990", "1/4/2018", "2/12/2003" ]
Jak widać, została stworzona tylko jedna instancja Intl.DateTimeFormat
, co z pewnością przełoży się na wydajność. Zachęcam do uruchomienia samodzielnie powyżeszgo kodu, na przykład przez TypeScript Playground.
(anty)wzorzec?
Wokół tego wzorca narosły pewne kontrowersje. Przez część programistów nazywany jest wręcz antywzorcem.
Pierwszym z problemów, jakie przysparza Singleton, jest to, że w oczywisty sposób łamie on Zasadę Pojedynczej Odpowiedzialności (SRP — Single Responsibility Principle). Na przykładzie wcześniejszego formattera, oprócz formatowania pilnuje on również czy istnieje tylko jedna instancja klasy w systemie. To są oczywiście dwie osobne odpowiedzialności.
Nieumiejętnie zaimplementowany Singleton może powodować stworzenie w systemie tzw. god class/god object (klas/obiektów boskich). Obiekty tego typu wiedzą o wiele za wiele o innych elementach systemu. Przykładem może być obiekt klasy System, który agreguje wszystkie pozostałe obiekty z aplikacji.
Problem z Singletonami może również pojawić się w aplikacjach wielowątkowych. W tej sytuacji należy również zadbać o synchronizację, aby wyeliminować ryzyko stworzenia kilku instancji Singletona.
Przez niektórych Singleton określany jest jako „obiektowy zamiennik zmiennej globalnej”, a wykorzystanie zmiennych globalnych zdecydowanie nie należy do best practices.
Podsumowanie
Mimo że wokół wzorca Singleton narosły pewne kontrowersje, to uważam, że absolutnie nie należy go skreślać. Jeżeli do rozwiązania problemu wzorzec projektowy Singleton jest odpowiedni, to warto rozważyć jego użycie. Jak zwykle zachęcam do zapoznania się ze źródłami i materiałami dodatkowymi oraz do pozostawienia komentarza pod artykułem!
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić klikając w TEN link. Kupując z tego linku, wspierasz rozwój bloga.
Źródła i materiały dodatkowe
- Design Patterns: Elements of Reusable Object-Oriented Software – Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1994)
- Using ESLint to improve your app’s performance
- Formatting dates in JavaScript with Intl.DateTimeFormat
- Luxon – Performance of toLocaleString is slow, due to lack of caching
- Intl Format Cache
- On design patterns: When should I use the singleton?
- Why are global variables considered bad practice?
- Refactoring Guru – Singleton
- Wzorzec projektowy Singleton
- God object
- How do you refactor a God class?
- Singleton – 7 błędów, które popełniasz wykorzystując singleton
- Singleton inaczej
- Singleton Design Pattern
- What the Singleton Pattern Costs You
- Why Singletons are Evil
- Uncle Bob – SingletonVsJustCreateOne
Według mnie wzorzec Singleton (jak i wiele innych wzorców Gangu Czworga) powstały w odpowiedzi nie na ogólne problemy programistyczne, ale na problemy wynikające z ograniczeń Javy.
W TS/JS jest mała potrzeba na implementowanie Singletona, bo ten ekosystem ma już swoją odpowiedź na problem, jaki rozwiązuje singleton – na poziomie samego języka:
Bardzo wartościowy wpis. Ze swojej strony dodam, że warto pamiętać, że singleton nie powinien być stanowy bo może sporo namieszać przy testach 🙂