WYPRZEDAŻ ZIMOWA TRWA!

Singleton

Cel

Singleton jest kreacyjnym wzorcem projektowym, który pozwala zapewnić istnienie wyłącznie jednej instancji danej klasy. Ponadto daje globalny punkt dostępowy do tejże instancji.

Wzorzec Singleton

Problem

Wzorzec Singleton rozwiązuje jednocześnie dwa problemy, ale jednocześnie łamie Zasadę pojedynczej odpowiedzialności:

  1. Zapewnia istnienie wyłącznie jednej instancji danej klasy. Dlaczego w ogóle ktokolwiek musiałby liczyć obiekty danej klasy? Otóż, najczęstszym powodem jest potrzeba kontroli dostępu do jakiegoś współdzielonego zasobu — na przykład bazy danych, lub pliku.

    Działa to tak: wyobraź sobie, że masz już stworzony obiekt, ale po jakimś czasie potrzebujesz kolejnego. Zamiast otrzymać nowy, dostaniesz ten uprzednio stworzony.

    Zwróć uwagę, że takiego zachowania nie da się zaimplementować stosując zwykły konstruktor, ponieważ metody te z definicji muszą zawsze zwracać nowe obiekty.

Globalny dostęp do obiektu

Klienci mogą nawet nie zauważyć, że cały czas korzystają z tego samego obiektu.

  1. Pozwala na dostęp do tej instancji w przestrzeni globalnej. Pamiętasz te globalne zmienne, z których korzystaliśmy (no dobra, ja korzystałem) do przechowywania istotnych obiektów? Chociaż są one bardzo poręczne, to wiążą się też z poważnym ryzykiem nadpisania ich zawartości i tym samym awarii programu.

    Zupełnie, jak w przypadku zmiennej globalnej, wzorzec Singleton pozwala skorzystać z jakiegoś obiektu w dowolnym miejscu programu. Jednakże, zapewnia też ochronę tego obiektu przed działaniami innego kodu.

    Jest też inna strona tego problemu: nie chcemy, aby kod, który pozwala nam rozwiązać pierwszy z powyższych problemów był porozrzucany po całym programie. Dużo lepiej jest trzymać go w jednej klasie, szczególnie, jeśli reszta kodu już od niego zależy.

Wzorzec Singleton stał się obecnie tak popularny, że czasem nazywa się Singletonem rozwiązania odnoszące się tylko do jednego z powyższych problemów.

Rozwiązanie

Wszystkie implementacje wzorca Singleton współdzielą poniższe dwa etapy:

  • Ograniczenie dostępu do domyślnego konstruktora przez uczynienie go prywatnym, aby zapobiec stosowaniu operatora new w stosunku do klasy Singleton.
  • Utworzenie statycznej metody kreacyjnej, która będzie pełniła rolę konstruktora. Za kulisami, metoda ta wywoła prywatny konstruktor, aby utworzyć instancję obiektu i umieści go w polu statycznym klasy. Wszystkie kolejne wywołania tej metody zwrócą już istniejący obiekt.

Jeżeli twój kod ma dostęp do klasy Singleton, to będzie mógł wywołać jej statyczną metodę i tym samym za każdym razem otrzyma ten sam obiekt.

Analogia do prawdziwego życia

Rząd jest doskonałym przykładem wzorca Singleton. Kraj może mieć wyłącznie jeden oficjalny rząd. Niezależnie od składu personalnego członków rządu, pojęcie “Rząd kraju X” jest uniwersalnym odwołaniem do organu władzy kraju.

Struktura

Struktura wzorca projektowego SingletonStruktura wzorca projektowego Singleton
  1. Klasa Singleton deklaruje statyczną metodę getInstance, która zawsze zwraca tę samą instancję swej klasy.

    Konstruktor klasy Singleton musi być ukryty przed kodem klienckim. Tylko i wyłącznie wywołanie metody getInstance powinno dawać dostęp do takiego obiektu.

Pseudokod

W poniższym przykładzie, połączenie z bazą danych zrealizowane jest jako Singleton. Klasa ta nie ma publicznie dostępnego konstruktora, więc aby uzyskać dostęp do jej instancji, trzeba wywołać metodę getInstance. Metoda ta zachowuje pierwszy stworzony egzemplarz klasy i zwraca go przy każdym kolejnym wywołaniu.

// Klasa Database definiuje metodę `getInstance` która pozwala
// klientom na dostęp do tej samej instancji połączenia
// bazodanowego z dowolnego miejsca w programie.
class Database is
    // Pole służące przechowywaniu instancji singleton powinno
    // być zadeklarowane jako statyczne.
    private static field instance: Database

    // Konstruktor singletona powinien zawsze być zadeklarowany
    // jako prywatny, aby uniemożliwić wywoływanie na klasie
    // operatora `new`.
    private constructor Database() is
        // Jakiś kod inicjalizacyjny, na przykład faktyczne
        // nawiązanie połączenia z serwerem bazy danych.
        // ...

    // Statyczna metoda kontrolująca dostęp do instancji
    // singletona.
    public static method getInstance() is
        if (Database.instance == null) then
            acquireThreadLock() and then
                // Trzeba się upewnić, że instancja nie została
                // już zainicjalizowana przez inny wątek w
                // czasie gdy ten wątek oczekiwał na zwolnienie
                // blokady.
                if (Database.instance == null) then
                    Database.instance = new Database()
        return Database.instance

    // Wreszcie — każdy singleton powinien definiować jakąś
    // logikę biznesową którą można wywołać z instancji
    // singletona.
    public method query(sql) is
        // Przykładowo, wszystkie zapytania do bazy danych w
        // całej aplikacji muszą przejść przez tę metodę. Możesz
        // tu więc umieścić kod pamięci podręcznej lub kontroli
        // przepustowości.
        // ...

class Application is
    method main() is
        Database foo = Database.getInstance()
        foo.query("SELECT ...")
        // ...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        // Zmienna `bar` będzie zawierała ten sam obiekt, co
        // zmienna `foo`.

Zastosowanie

Korzystaj z wzorca Singleton, gdy w twoim programie ma prawo istnieć wyłącznie jeden ogólnodostępny obiekt danej klasy. Przykładem może być połączenie z bazą danych, którego używa wiele fragmentów programu.

Wzorzec projektowy Singleton uniemożliwia tworzenie obiektów danej klasy inaczej, niż przez stosowną metodę kreacyjną. Ta z kolei zwróci albo nowy obiekt, albo wcześniej stworzony.

Stosuj wzorzec Singleton gdy potrzebujesz ściślejszej kontroli nad zmiennymi globalnymi.

 W przeciwieństwie do zmiennych globalnych, wzorzec Singleton gwarantuje istnienie tylko jednego obiektu danej klasy. Nic, oprócz samej klasy, nie jest w stanie zamienić tego obiektu.

Zwróć uwagę, że zawsze można zmienić ograniczenie dotyczące ilości i pozwolić na jakąś inną maksymalną liczbę jej instancji. Wystarczy zmienić jeden fragment kodu — metodę getInstance.

Jak zaimplementować

  1. Dodaj prywatne, statyczne pole klasy celem przechowywania w nim instancji singleton.

  2. Zadeklaruj publicznie dostępną, statyczną metodę kreacyjną która daje dostęp do instancji klasy singleton.

  3. Zaimplementuj w tej metodzie statycznej “leniwą inicjalizację”. Oznacza to, że nowy obiekt powinien być tworzony tylko przy pierwszym wywołaniu metody i zdeponowany w statycznym polu klasy. Przy kolejnych wywołaniach, metoda powinna zwracać już istniejący egzemplarz.

  4. Uczyń konstruktor klasy prywatnym. Statyczna metoda klasy będzie miała do niego dostęp, ale obiekty innych klas — już nie.

  5. Przejrzyj kod kliencki i zamień wszystkie bezpośrednie wywołania konstruktora klasy singleton na wywołania jej statycznej metody kreacyjnej.

Zalety i wady

  • Masz pewność, że istnieje tylko jedna instancja klasy.
  • Zyskujesz globalny dostęp do tej instancji.
  • Obiekt singleton inicjalizowany jest dopiero wtedy, gdy jest po raz pierwszy potrzebny.
  • Łamie Zasadę pojedynczej odpowiedzialności. Wzorzec rozwiązuje bowiem dwa różne problemy jednocześnie.
  • Zastosowanie wzorca Singleton może zamaskować niewłaściwe projektowanie. Można na przykład doprowadzić do sytuacji, w której komponenty programu wiedzą zbyt wiele o sobie nawzajem.
  • Wzorzec ten wymaga specjalnej uwagi w środowisku wielowątkowym, w którym trzeba unikać tworzenia wielu instancji singletona przez wiele wątków.
  • Utrudnieniu mogą ulec testy jednostkowe kodu klienckiego klasy singleton, ponieważ wiele frameworków testujących polega na dziedziczeniu przy produkcji atrap obiektów. Ponieważ konstruktor klasy Singleton jest prywatny, a nadpisywanie statycznych metod jest niemożliwe w większości języków programowania, będzie trzeba wykazać się kreatywnością i znaleźć jakiś inny sposób tworzenia atrapy singletona. Albo nie pisać testów, albo zrezygnować z tego wzorca.

Powiązania z innymi wzorcami

  • Klasa Fasada może często być przekształcona w Singleton, ponieważ pojedynczy obiekt fasady jest w większości przypadków wystarczający.

  • Pyłek mógłby przypominać Singleton, gdybyśmy zdołali zredukować wszystkie współdzielone stany obiektów do tylko jednego obiektu-pyłka. Ale są jeszcze dwie fundamentalne różnice między tymi wzorcami:

    1. Powinna istnieć tylko jedna instancja interfejsu Singleton, zaś instancji Pyłka będzie wiele, o różnym stanie wewnętrznym.
    2. Obiekt Singleton może być zmienny. Pyłki są zaś niezmienne.
  • Fabryki abstrakcyjne, Budowniczych oraz Prototypy można zaimplementować jako Singletony.

Przykłady kodu

Singleton w języku C# Singleton w języku C++ Singleton w języku Go Singleton w języku Java Singleton w języku PHP Singleton w języku Python Singleton w języku Ruby Singleton w języku Rust Singleton w języku Swift Singleton w języku TypeScript