Wzorzec projektowy Singleton - okładka

Wzorzec projektowy Singleton

Opublikowano Kategorie Czysty kod, TypeScriptCzas czytania 5min

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.

Wzorzec Singleton - diagram UML

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

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Notify of
guest

2 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Kuba Orlik
1 year ago

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:

class Something{}
export default new Something()
Kuba | oracleDev.pl
3 years ago

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 🙂