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.
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:
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):
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.
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.
Odwołamy się do tego w ten sposób:
Wynik wykonania kodu:
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:
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).
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
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
Nie jesteś biegły w Javie?
Interesuje Cię szerszy zakres wiedzy?