Prawdopodobnie najpopularniejszy wzorzec projektowy… Prawdopodobnie, bo wiele rozwiązań umożliwiających dostęp do instancji w przestrzeni globalnej, tudzież konstrukcja programu zapewniająca istnienie tylko jednej instancji klasy są nazywanych błędnie Singleton’em.

Dygresja na początek… niezbędny SOLID

Singleton jest wzorcem łamiącym zasadę SRP… ale co to jest zasada SRP?! Tutaj zanim przejdę do omówienia wzorca, muszę nadmienić, że w historii zostały sformułowane i spopularyzowane zasady tworzenia “czystego” kodu. Zespół 5 zasad nazwano zgodnie SOLID (czyli ang. TRWAŁY albo SOLIDNY).

SOLID jest zestawem dobrych praktyk strukturalnych propagowanym dla programowania obiektowego. Ma za zadanie sprawić, aby nasz kod był bardziej czytelny, przyjazny dla zmian i łatwiejszy w zarządzaniu. Metodologia została wymyślona i spopularyzowana poprzez Twórców czystego kodu m.in. takich jak Robert C. Martin znany w literaturze jako Uncle Bob.

Zasady SOLID, czyli:

umożliwiają projektowanie obiektów i klas w sposób modułowy i zgodny z przyjętymi dobrymi zasadami projektowania.

Zasada pojedynczej odpowiedzialności

Zasada pojedynczej odpowiedzialności (Single Responsibility Principle) jest jedną z podstawowych zasad SOLID wymaganych do spełnienia w programowaniu obiektowym. W założeniu każda zdefiniowana klasa powinna mieć tylko jedną wdrożoną funkcjonalność czyli odpowiadać za jeden aspekt działania programu. Znaczy to, że każda klasa powinna być odpowiedzialna za wykonanie jednej dedykowanej czynności lub obsługę jednej funkcji. Dzięki temu można uzyskać wyraźniejszą separację odpowiedzialności, większą czytelność kodu oraz promować możliwość ponownego wykorzystania klas. Zasada pojedynczej odpowiedzialności jest kluczowa dla utrzymania modułowości, elastyczności i łatwości rozbudowy naszego systemu.

Zasada otwarte-zamknięte

Zasada otwarte-zamknięte (Open-Closed Principle) jest kolejną z zasad SOLID w programowaniu obiektowym. Zasada ta mówi, że klasy powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację. Znaczy to, że powinno być możliwe dodawanie nowych funkcjonalności ale jedynie poprzez tworzenie nowych klas, bez zgody na ingerencję w dotychczasowy kod. Unikamy w ten sposób możliwości wprowadzania błędów i niekonsekwencji w istniejących fragmentach kodu, co również sprzyja utrzymaniu i rozwijaniu aplikacji.

Zasada podstawienia Liskov

Zasada podstawienia Liskov (Liskov Substitution Principle) jest kolejną z zasad SOLID w programowaniu obiektowym. W założeniu obiekt musi być w stanie zastąpić dowolny inny obiekt swojej klasy nadrzędnej, nie naruszając przy tym poprawności działania programu. Innymi słowy, jeśli dwa obiekty należą do różnych klas, powinny one być wzajemnie zastępowalne bez żadnych negatywnych konsekwencji dla działania programu. Zasada ta jest niezbędna dla utrzymania elastyczności i rozszerzalności kodu, umożliwiając tworzenie hierarchii klas, które mogą efektywnie współpracować i być rozbudowywane.

Zasada segregacji interfejsów

Zgodnie z zasadą segregacji interfejsów (Interface Segregation Principle – ISP), interfejsy powinny być dopasowane do modułów, które je wykorzystują, zamiast być ogólne i próbować obsługiwać więcej niż konieczne funkcjonalności. Zadaniem ISP jest dekompozycja dużej i ogólnej klasy interfejsu na mniejsze, bardziej wyspecyfikowane interfejsy dostosowane do konkretnych wymagań użytych modułów. W ten sposób udaje się uniknąć konieczności implementowania nadmiarowych metod, zwiększając zarazem elastyczność kodu i umożliwiając łatwiejszą adaptację do zmian.

Zasada odwrócenia zależności

Zasada odwrócenia zależności (Dependency Inversion Principle, DIP) mówi, że moduły powinny zależeć od abstrakcji, a nie od konkretnych implementacji. Oznacza to, że w kodzie powinny być używane przede wszystkim interfejsy lub klasy abstrakcyjne. Dzięki temu zmniejsza się zależność pomiędzy modułami i ułatwia wprowadzenie zmian czy wymianę konkretnych implementacji bez konieczności modyfikacji całego kodu.

Ponownie SINGLETON

Wzorzec Singleton jest wzorcem kreacyjnym, czyli wykorzystywanym do ukształtowania architektury naszego systemu. Stosując Singleton zapewniamy (1) istnienie wyłącznie jednej instancji danej klasy. Wzorzec ten daje (2) globalny punkt dostępowy do tejże instancji. Przykładem konieczności zapewnienia istnienia tylko jednej instancji danej klasy jest jej funkcjonalność wymagająca ograniczenia, nadzoru lub kontroli np. dostęp do bazy danych. Jednym słowem, niezależnie ile podejść wykonamy dla utworzenia kolejnej instancji klasy de facto zwrotnie otrzymamy referencję do pierwotnie utworzonej instancji tej klasy. Poprzez daleko idącą analogię można odwołać się do przykładu przekazywania tablicy poprzez parametr… niektórzy “programiści” spodziewają się utworzenia kopii lokalnej danej struktury, na której mogą pracować, ale którą potem “muszą” zwrócić by zachować jej dane w strukturze pierwotnej. Nie zauważają nawet wtedy, że pracują na TYM SAMYM OBIEKCIE, a nie tworzy się żadna nowa kopia (instancja) tylko przekazywana jest referencja do obiektu źródłowego.

Kiedy stosować?

Singleton używamy wtedy (i w zasadzie tylko wtedy), gdy w programie ma prawo istnieć wyłącznie jeden ogólnodostępny obiekt tej klasy. Jest to na przykład wtedy, gdy chcemy “zmusić” do użycia tylko jednej instancji obsługi bazy danych, by wykluczyć konflikty.

Charakterystyka

Singleton oferuje:

Uniemożliwienie tworzenia instancji danej klasy poprzez metodę new, zamiast tego wprowadza metodę getInstance(); W tym przypadku, przy pierwszym wywołaniu jest uruchomiony konstruktor klasy i tworzony jest obiekt, przy każdym kolejnym wywołaniu jest to już zablokowane, a próba utworzenia obiektu zwróci referencję do już utworzonej poprzednio instancji tej klasy.

Przykład użycia

Kod prostej klasy z wykorzystaniem wzorca zamieszczony jest poniżej:

package wzorceprojektowe1;

/**
 *
 * @author Utracki
 */
public class SingletonService {
    private static volatile SingletonService instance = null;

    private SingletonService() {
        if(instance != null) {
            throw new RuntimeException("Not allowed. Please use getInstance() method");
        }
    }

    public static SingletonService getInstance() {

        if(instance == null) {
            synchronized(SingletonService.class) {
                if(instance == null) {
                    instance = new SingletonService();
                }
            }
        }

        return instance;
    }
}

Sposób użycia z przykładem porównawczym jest umieszczony poniżej:

package wzorceprojektowe1;


public class WzorceProjektowe1 {

 /**
 *
 * @author Utracki
 */
    public static void main(String[] args) {
    
       // SingletonService serv0 = new SingletonService();  // <- tutaj widać nałożoną blokadę użycia <<new>>.
        SingletonService serv1 = SingletonService.getInstance();
        SingletonService serv2 = SingletonService.getInstance();
        
        if (serv1.equals(serv2)) System.out.println("Usługi są tym samym obiektem."); else System.out.println("Usługi posiadają różne instancje");
        
        Boo b1 = new Boo();
        Boo b2 = new Boo();
        if (b1.equals(b2)) System.out.println("Usługi są tym samym obiektem."); else System.out.println("Usługi posiadają różne instancje");
        
    }
    
}

class Boo {
    
} 

Pytanie: Wzorzec projektowy Singleton nazywany jest także antywzorcem – dlaczego?