Wzorzec projektowy Singleton - okładka

Wzorzec projektowy Singleton

Ten wpis jest jednym z serii wpisów o wzorcach projektowych. Zachęcam też do sprawdzenia innych wzorców:

Struktura

Na sam początek przedstawiam strukturę wzorca projektowego Singleton w postaci diagramu klas. Już sam diagram powinien powiedzieć Ci wiele o charakterystyce oraz potencjalnych zastosowaniach tego wzorca.

Wzorzec Singleton - diagram UML

Klasa Singleton posiada prywatną właściwość typu Singleton, w której przechowywana jest instancja wcześniej wspomnianej klasy. Oprócz tego klasę wyróżnia prywatny konstruktor oraz 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.

Podstawowa implementacja tego wzorca w TypeScript może wyglądać następująco:


 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 przejdę 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. W skrócie – następujący kod nie zadziała:


const singleton: Singleton = new Singleton(); // Do not do that!

Jedynym sposobem na pozyskanie obiektu, jest skorzystanie z metody getInstance().

W czystym JavaScripcie problem ten można rozwiążać poprzez rzucenie Errora w konstruktorze, w momencie gdy instancja istnieje.

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 instacji byłoby konieczne. W sytemie zwykle obowiązuje tylko jeden format daty i czasu (a przynajmniej jeden dla danego użytkownika).

Rozwiązanie tego problemu z użyciem Singletona może wyglądać następująco:


 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 );

Natomiast 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ść. Więcej informacji na ten temat znajdziesz w źródłach na końcu artykułu.

Serdecznie 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 instacja 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). Obiety 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 instacji Singletona.

Co więcej, przez niektórych Singleton określany jest jako obiektowy zamiennik zmiennej globalnej, a zmienne globalne nie należą do best practices.

Podsumowanie

Mimo tego, że wokół tego wzorca projektowego 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 myślę, że 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 i oceny pod artykułem!

Źródła i materiały dodatkowe: