Kurs Java

Singleton

Wiemy, że jesteś tutaj nie po to, aby zaczynać od teorii, która nic Ci nie mówi i przy której zdążysz znudzić się, zanim dojdziesz do konkretów. Też tak mamy. Z tego powodu naszym celem jest, aby podstawowe definicje w rozdziale o wzorcach nie były dłuższe niż trzy zdania. Dla zainteresowanych rozwinięcie tematu będziemy zamieszczać w dalszej części rozdziału. Zaczynamy.

Singleton to wzorzec projektowy, według którego w ramach maszyny wirtualnej Javy (JVM) może istnieć tylko jedna instancja obiektu danej klasy i musi być zapewniony globalny dostęp do tej instancji. Zakładając, że nasz program, czy też aplikacja, działają na jednej maszynie wirtualnej, zasada ta dotyczy automatycznie jednej instancji w ramach tego programu/aplikacji.

Pełna wersja

Tak wygląda pełna wersja singletona. Pełna, czyli działająca poprawnie podczas pracy z wieloma wątkami oraz zapewniająca odporność na próby wykonywanie kodu spoza programu.
Java CheckedException
Appa Notka. Co to jest wątek? Niezależny proces (w znaczeniu ścieżki wykonania) w programie, który wykonuje pewną operację. Wielowątkowość programu oznacza, że w tym samym czasie mogą pracować różne wątki wykonujące różne zadania. Mogą oczywiście pracować również w kolejności (jeden po drugim). Współbieżne działanie wątków można przyrównać do pracy zespołowej, jakby kilka osób wykonywało pewną pracę, dzieląc się zadaniami do wykonania. Zwykle praca ta zostanie wykonana szybciej (wydajniej).
Appa Notka. Co to jest refleksja? Refleksja albo precyzyjniej mówiąc Reflection API, jest zbiorem rozwiązań dostarczanych z Javą, których zadaniem jest sprawdzanie lub/i modyfikowanie zachowań klas, interfejsów i metod w czasie wykonania programu. Używając Reflection API, możemy więc stworzyć instancję obiektu, mimo że klasa ma prywatny konstruktor. Przypomina to trochę działanie hakera, który potrafi z zewnątrz dostać się do naszego komputera, aby coś pozmieniać bez naszej wiedzy, tyle że tutaj inny programista dostaje się do kodu programu spoza tego kodu. Rzucanie wyjątku w konstruktorze uchroni nas zatem przed stworzeniem obiektu za pomocą refleksji.
Kod do skopiowania:
public class ItemNotificationService {

    private static volatile ItemNotificationService instance = null;

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

    public static ItemNotificationService getInstance() {

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

        return instance;
    }
}

Wersja podstawowa

W przypadku, gdy piszesz prosty program, który uruchamiasz "z palca" i w jednym momencie pracuje na nim jeden użytkownik oraz sam program nie tworzy nowych wątków, wówczas wystarczy Ci podstawowa wersja singletona (niepełna):
public class ItemNotificationService {

    private static ItemNotificationService instance = null;

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

    public static ItemNotificationService getInstance() {

        if(instance == null) {
            instance = new ItemNotificationService();
        }

        return instance;
    }
}
Pamiętaj jednak, że w ten sposób można przyzwyczaić się do prostoty tego rozwiązania, a wtedy w przyszłości łatwiej będzie popełnić błąd, używając tej wersji w środowisku, gdzie działa współbieżnie kilka wątków. Możesz po prostu zapomnieć o użyciu słów kluczowych volatile i synchronized.

Gdzie używamy wzorca Singleton ?

Wzorzec został wymyślony głównie po to, aby umożliwić programiście stworzenie jednego obiektu do obsługi zadań przekrojowych dla całej aplikacji. Na przykład, jeśli nasz program nawiązuje połączenie z bazą danych, to będziemy chcieli mieć tylko jeden obiekt utrzymujący to połączenie. Wtedy możemy go zwrócić na żądanie dowolnego innego obiektu w programie. To, że mamy dostęp do obiektu singletona z każdego miejsca, jest zapewnione przez metodę statyczną getInstance().

Wzorzec wykorzystamy również wtedy, gdy będziemy chcieli stworzyć jedno miejsce do rejestrowania obiektów jakiegoś typu i przechowywania ich w tym jednym miejscu, w celu wykorzystania ich przyszłości. Na przykład możemy z tego jednego miejsca uruchomić w jednym momencie metodę na wszystkich zarejestrowanych obiektach, po to aby przekazać do tych obiektów jakieś dane, używając parametrów tej metody. W ten sposób stworzymy prosty system notyfikacji. Jednak to zahacza już o kolejny wzorzec o nazwie Obserwator, który omówimy osobno w niedalekiej przyszłości.

Przykłady użycia wzorca Singleton:

  • Nawiązywanie i przechowywanie połączenia do bazy danych (lub innego zasobu zewnętrznego, takiego, którego inicjujemy raz w ramach działania programu).
  • Rejestrowanie obiektów jakiegoś typu w celu późniejszego grupowego uruchomienia konkretnej metody na każdym z nich (co jest szczególnie przydatne w prostych systemach powiadamiających).
  • Stworzenie w jednym miejscu mechanizmu wykorzystywanego wielokrotnie w ramach aplikacji (na przykład mechanizmu logowania, czyli zapisu informacji i błędów do logów).
Regularne korzystanie z singletona (gdzie popadnie) może wydawać się kuszące, ale takie podejście jest najkrótszą drogą do przekształcenia tego wzorca w antywzorzec. Zatem nie używamy singletona, tylko dlatego, że w każdym miejscu programu mamy dostęp do naszego obiektu, ale dlatego, że napotykamy na zagadnienie, do którego jest on dedykowany. Dobre przykłady podaliśmy wyżej.

Eager vs Lazy

Eager kontra lazy, czyli z polskiego zachłanne kontra leniwe. Takie dwa rodzaje inicjalizacji obiektu instance możemy zaimplementować w ramach singletona. Dotychczasowe przykłady są typu lazy (leniwe), ponieważ inicjujemy obiekt na żądanie (wywołanie metody getInstance)

Przykład inicjalizacji typu eager będzie wyglądał tak, jak w poniższym fragmencie kodu. Instancja klasy tworzona jest zawsze (w momencie inicjalizacji pola). Tak więc nawet, jeśli nie odwołamy się do tego pola, będzie ono miało przypisaną referencję do obiektu. W przypadku, gdyby singleton ten obsługiwał połączenie do bazy danych, wówczas niepotrzebnie stworzylibyśmy połączenie do tej bazy (niepotrzebnie, bo i tak nie zostało użyte). Tak więc przy pracy z zasobami lepiej będzie stworzyć instancję w stylu lazy. Zresztą, po co w ogóle tworzyć obiekt tylko dla samego stworzenia? Lepiej zrobić to dopiero wtedy, kiedy do czegoś go potrzebujemy.
public class ItemNotificationService {

    private static ItemNotificationService instance = new ItemNotificationService();

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

    public static ItemNotificationService getInstance() {
        return instance;
    }
}

Nazwa metody getInstance

We wszystkich powyższych przykładach użyliśmy nazwy pola instance oraz metody getInstance. Nie jest to obowiązkowe, jednak ogólnie takie nazewnictwo przyjęło się i stanowi niepisaną regułę, którą warto stosować. To, co jest ważne, to brak parametrów. Metoda nie może posiadać parametrów, ponieważ wtedy zacznie wyglądać jak Metoda fabrykująca (kolejny wzorzec). O tym i o innych wzorcach napiszemy już niedługo.
Appa Notka. Na koniec ciekawostka. Klasa Calendar z pakietu java.util także posiada metodę getInstance, ale mimo tego, że wiele osób jest przekonanych, iż tworzy ona tutaj singletona, to tak nie jest. Metoda nazywa się co prawda w sposób sugerujący tworzenie obiektu według wzorca Singleton, ale w rzeczywistości za każdym razem tworzy nową instancję obiektu.

Singleton w postaci enuma

Na wstępie tego paragrafu przytoczymy definicję enuma, czyli typu wyliczeniowego. Tak więc enum to tak typ danych, który zawiera zbiór predefiniowanych stałych. Zmienna musi być równa jednej z predefiniowanych wartości. Na przykład w aplikacji przechowującej dokumenty możemy określić nazwy typów dokumentów. W ten sposób ustalamy, że tylko takie wartości mogą zostać wybrane.
public enum DocumentType {

    INVOICE, CONTRACT, CERTIFICATE, NOTARIAL_ACT;

    DocumentType() {
    }

    // Metody
}
Odwołamy się do tego w ten sposób:
public class Start {

    public static void main(String[] args) {

        Enum contract = DocumentType.CONTRACT;
        Enum invoice = DocumentType.INVOICE;
        Enum notarialAct = DocumentType.NOTARIAL_ACT;
        Enum certificate = DocumentType.CERTIFICATE;

        checkEnum(contract);
        checkEnum(invoice);
        checkEnum(notarialAct);
        checkEnum(certificate);
    }

    static void checkEnum(Enum documentType) {

        if(documentType.equals(DocumentType.CERTIFICATE)) {
            System.out.println(documentType);
        }
    }

}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
Co ciekawe enum jest rozszerzeniem klasy abstrakcyjnej o tej samej nazwie, dlatego też może zawierać konstruktor oraz metody. Nas jednak będzie teraz interesowało coś zupełnie innego. Otóż jest taka możliwość, aby stworzyć singletona na podstawie enuma i – co więcej – jest jest to bardzo krótka i prosta droga do osiągnięcia celu. Robimy to tak:
public enum ItemNotificationService {

    INSTANCE;

    ItemNotificationService() {
    }
    
    // Metody
}
Taki sposób implementacji singletona jest bardzo wygodny, natomiast niekoniecznie każdemu musi pasować. Ba, czasem nawet zbyt szybkie zachłyśnięcie się prostotą tego rozwiązania i nieznajomość tradycyjnego singletona może stanowić duży problem (o tym nieco niżej).
  • Pewną wadą jest to, że jednak enum to typ wyliczeniowy i trochę przez przypadek złożyło się, że spełnia wszystkie cechy wzorca Singleton (jest threadsafe oraz odporny na refleksję). Ale to wciąż typ wyliczeniowy.
  • Kolejny problem związany jest z punktem pierwszym. Chodzi tu o konwencję nazewniczą. Nazwa idealnie pasująca do konkretnej koncepcji biznesowej (na przykład usługa powiadamiania o itemach - ItemNotificationService) średnio pasuje jako nazwa dla enuma.
  • Problem jest jeszcze jeden. Załóżmy, że ktoś trafia do projektu, w którym został zastosowany tradycyjny singleton. Niech się okaże, że użytkownicy systemu zgłaszają błąd, który pojawił się nagle i najprawdopodobniej jest związany z większym niż dotychczas obciążeniem systemu. Ewidentnie z aplikacją dzieje się coś złego w obrębie naszego singletona, ale my nawet nie zastanawiamy się nad tym głębiej, bo singleton jest i ma się dobrze. W ten sposób możemy długo szukać rozwiązania problemu, jeśli nie wiemy, że jego źródłem jest brak obsługi wielowątkowości w tym singletonie (i nagle zamiast jednej instancji mamy ich kilka).
Podsumowując, trendy są teraz takie, żeby używać enuma do singletonów (nawołują do tego puryści językowi), ale ...jednocześnie nie wszyscy się z tym zgadzają. Dlatego najlepiej jest znać oba podejścia i w razie potrzeby umieć je wytłumaczyć na przykład na rozmowie kwalifikacyjnej.

Singleton Static Holder

Istnieje jeszcze jedna wariacja wzorca Singleton o nazwie Singleton Static Holder. To również klasa tak jak na początku rozdziału, ale w tym przypadku korzystamy z wewnętrznej klasy statycznej. Pole INSTANCE jest tutaj inicjowane dopiero przy pierwszym odwołaniu i mamy to zapewnione przez samą Javę (załatwia nam to temat współbieżności).
public class ItemNotificationService {

    private ItemNotificationService() {
    }

    public static ItemNotificationService getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {

        private static final ItemNotificationService INSTANCE = new ItemNotificationService();
    }
}
Jeśli interesują Cię wzorce projektowe, możesz być również zainteresowany naszym Kursem Aplikacji Web, w którym uczysz się kompleksowo Springa, Hibernate'a i innych aktualnych rozwiązań technologicznych na bazie gotowej aplikacji webowej. Oto co dokładnie znajdziesz w kursie:
  • Kurs implementacji aplikacji webowej zbudowanej za pomocą:
    • Spring Boot 2, Spring Framework 5, Spring Data JPA, Spring MVC
    • Spring Security, Spring AOP
    • Hibernate
    • REST Api, Format JSON
    • Maven
    Aplikacja obejmuje funkcjonalności od rejestracji użytkownika i logowania, przez tworzenie, edycję oraz usuwanie danych, aż po różne formy prezentacji, w tym tabele i wykresy. Kurs obejmuje również implementację resetowania hasła z linkiem potwierdzającym wysyłanym na adres email.
  • Atak CSRF i jak się przed nim bronić w Springu
  • Clickjacking za pomocą iframe - jak się zabezpieczyć za pomocą Spring Security
  • CORS i jego możliwe opcje w Springu
  • Testowanie CORS-a z użyciem cURL
  • Dodatkowy projekt do nauki samego Hibernate'a, przygotowany w dwóch wersjach (z Lombokiem i bez). Ta część bazy wiedzy zawiera ponad 60 omówionych snippetów kodu, wzbogaconych dodatkowo ponad 40 zdjęciami. W projekcie zostały zaimplementowane i omówione między innymi:
    • Podstawy Hibernate'a (adnotacje, encje itp.)
    • Wszystkie rodzaje relacji bazodanowych
    • Orphan Removal
    • Single Table Discriminator
    • Table Per Class
  • ...oraz wiele innych zagadnień.
Sam kurs implementacji aplikacji to ponad 150 stron (całość online) analizy kodu od frontendu przez REST-owe wysyłanie żądań i odbieranie odpowiedzi, po zapis w bazie i wizualizowanie zapisu we frontendzie. Dokładnie rozpisane ścieżki w kodzie z tabelami kolejnych kroków dla poszczególnych funkcjonalności. Drogowskazy do ogromnej bazy wiedzy dołączonej w postaci kursów Spring i Hibernate (z projektami wspierającymi naukę).
Zobacz aplikację
Mapa umiejętności programisty Java

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE