Singleton
Jedni go kochają, inni nienawidzą. Choć nie ma on żadnego związku ze szkocką whisky, Singleton jest jednym z bardziej kontrowersyjnych wzorców projektowych.
W dzisiejszym wpisie poznasz jego strukturę, podstawowe założenia, a także wady i zalety. Jeśli przychodzisz po gotowy kod i przykładową implementację znajdziesz je tutaj
Podstawowe założenia wzorca
Implementacja wzorca Singleton powinna zawierać w sobie dwa podstawowe założenia:
- Prywatny konstruktor, który zapobiega możliwości stworzenia instancji poprzez słowo kluczowe new
- Statyczna metoda, która będzie służyła do zwracania istniejącego obiektu. W przypadku wywołania jej po raz pierwszy uruchomi ona konstruktor, a on zajmie się procesem kreacji instancji
Istnieje także pogląd, że Singleton nie powinien podlegać modyfikacjom. Na przykład w języku Javascript można "zamrozić obiekt" za pomocą metody Object.freeze(). Naszym zdaniem niemutowalność nie jest kluczowa w tym wzorcu i sam Singleton w swoich założeniach rozwiązuje nieco inne problemy.
W dzisiejszym wpisie skupimy się na implementacji i opisie nieco bardziej popularnego wariantu lazy loading. Jeśli chcesz wyrobić sobie szerszą opinię na temat tego wzorca polecamy zapoznać się jeszcze z wariantem eager loading
Singleton jako ogólny sposób myślenia
Obecnie ten wzorzec jest tak popularny, że przywołuje się go nie tylko w kontekście programowania obiektowego. Dobrym przykładem jest choćby ES Module z JavaScript. Każdy plik jest osobnym modułem, który z automatu może mieć maksymalnie jedną instancję, czyli w praktyce jest też „tak jakby singletonem”
Biorąc to pod uwagę, z pewnością w trakcie swojej pracy wielokrotnie spotkasz się z takimi określeniami i innymi „singletono-podobnymi tworami”. Prawdopodobnie większość z nich zawiera w sobie ziarno prawdy, jednakże mówiąc o wzorcu projektowym raczej powinniśmy myśleć w kontekście programowania obiektowego
Rzućmy jeszcze okiem na graficzny schemat. Z łatwością można zauważyć, że jest on o wiele prostszy niż większość diagramów opisujących wzorce
W najbardziej minimalistycznej wersji potrzebujemy tylko statycznego pola przechowującego instancję oraz statycznej metody, która ten obiekt tworzy/zwraca
Implementacja krok po kroku
Kilka prostych kroków do implementacji Singletona:
- Utwórz prywatne i statyczne pole w klasie, które będzie trzymać instancję klasy
- Stwórz prywatny konstruktor, a w nim logikę odpowiedzialną za kreację instancji. Uniemożliwi to przypadkowe utworzenie przez klientów nowej instancji klasy Singletona.
- Dodaj publiczną i równocześnie statyczną metodę, która będzie zwracać już istniejący obiekt. Będzie ona także odpowiedzialna za utworzenie instancji przy pierwszym wywołaniu.
- Jeśli potrzebujesz dodatkowych funkcjonalności, zaimplementuj je w osobnych metodach (już niestatycznych)
Poniżej umieszczamy uproszczony przykład klasy przechowującej globalną konfigurację aplikacji
class ApplicationGlobalConfig { private static instance: ApplicationGlobalConfig | null = null private port: number; private url: string; // Private keyword prevents new instances from being created outside of the class private constructor() { // initializing config logic there this.port = 5000; this.url = 'https://example.com'; } public static getConfig(): ApplicationGlobalConfig { if (this.instance === null) { this.instance = new ApplicationGlobalConfig(); } return this.instance; } public updatePort(newPort: number): void { this.port = newPort; } public updateUrl(newUrl: string): void { this.url = newUrl; } }
Powyższy przykład stanowi szkielet implementacji tytułowego wzorca. Możesz go śmiało rozbudowywać na własne potrzeby, dodając kolejne pola i metody.
Problemy i kontrowersje
Słabe punkty Singletona:
- Złamanie zasady pojedynczej odpowiedzialności, gdyż rozwiązuje 2 problemy jednocześnie: problem globalnego dostępu oraz kontroli liczby instancji
- Utrudniona testowalność klasy Singleton, ze względu na prywatny konstruktor oraz metody statyczne
- Problemy w środowiskach wielowątkowych, gdyż musimy dodatkowo zadbać o obsługę wielu wątków i w dalszym ciągu tworzyć tylko jedną instancję Singletona
W wyniku powyższych cech oraz nadmiernego używania przez programistów Singleton jest często uważany za antywzorzec sprowadzający się do zamiennika dla zmiennych globalnych
Kiedy jednak warto po niego sięgnąć:
- Gdy chcemy mieć pewność, że istnieje wyłącznie jeden obiekt danej klasy dostępny globalnie
- W sytuacji, gdy potrzebujemy większej kontroli nad globalnymi zmiennymi
Podsumowanie
Mamy nadzieję, że nieco rozjaśniliśmy Ci, na czym polega koncepcja tego wzorca. Teraz możesz z powodzeniem stosować go w swoich projektach przy jednoczesnym zachowaniu umiaru.
Biorąc pod uwagę cechy Singletona, może to być dobry sposób na obsługę połączeń z bazami danych lub zewnętrznymi serwisami, a także na przechowywanie globalnej konfiguracji w projekcie. Warto też go rozważyć, jeśli chcemy wprowadzić klasę obsługującą logi, wykorzystywane w całej aplikacji