Singleton

Share on facebook
Share on twitter
Share on linkedin

Singleton

Czas wziąć się trochę za wzorce projektowe. Są to tematy bardziej teoretyczne, jednak tak samo ważne jak samo pisanie kodu. Już tłumaczę dlaczego – do tego będziemy na początku potrzebowali definicji wzorców projektowych.

Czym są wzorce projektowe?

Wzorce projektowe to nic innego jak schematy programowania, które są już sprawdzone i pomagają rozwiązać konkretny problem. Stosuje się je zazwyczaj w językach obiektowych, więc nie są ściśle związane z żadnym z nich. Czemu tak jest?

Tak jak wspomniałem wzorzec projektowy to schemat – schemat, który można zaimplementować w prawie każdym języku (bezpieczniej mi jest powiedzieć, że prawie ;)).

Po co się je stosuje?

Wcześniej wspomniałem, że zostały stworzone i są wykorzystywane do rozwiązywania konkretnych problemów. I tak właśnie jest, dzięki pewnemu schematowi możemy pozbyć się problemu – problemu związane czasami z samą architekturą inne zaś z czytelnością aplikacji.

Najprostszym przykładem problemu, który rozwiązuje wzorzec projektowy Builder jest zbyt duża ilość parametrów w konstruktorze. Jaki był problem? Konstruktor posiadał zbyt wiele parametrów, co mogło spowodować pomyłkę podczas wpisywania do niego wartości. Jak to rozwiązano? Wprowadzono schemat – czyli wzorzec – dzięki, któremu tworzenie obiektów jest dużo czytelniejsze i prostsze.

To czym jest Singleton?

Singleton to oczywiście wzorzec projektowy – czyli schemat, który pozwala na utworzenie tylko jednego obiektu danej klasy.

Powtarzam jest to schemat – implementacja w każdym języku może wyglądać inaczej – jednak ostatateczne działanie jest takie samo. Rozwiązuje ono ten sam problem.

Jaki problem rozwiązuje Singleton?

Singleton rozwiązuje problem zbyt częstego tworzenia obiektów. Czy to faktycznie problem?

Tak jest to problem w przypadku, gdy faktycznie wystarczy nam jeden egzemplarz danego obiektu w obrębie całej aplikacji.

Czy dobrym przykładem takiego obiektu będzie jakiś model? A no nie, w końcu model służy do reprezentacji danych – a danych mogą być miliony, więc i tych obiektów możliwe, że będziemy potrzebowali tak dużo.

Jednak dobrym wykorzystaniem Singletonu mogą być klasy Service, DAO, a nawet Validator – dlaczego tak?

Służą one tak naprawdę do operowania na konkretnych obiektach, jednak same w sobie nic nie reprezentują. Nie potrzeba nam np. stu obiektów Service – ponieważ nie musza one przechowywać stu różnych haseł, tylko potrzebujemy dokładnie jednego do operowania na innych obiektach.

To samo jest z DAO – wystarczy nam jeden obiekt do komunikacji z bazą, nie potrzeba nam dwóch, pięciu, albo tysiąca.

Co zyskujemy dzięki temu?

Dzięki zastosowaniu wzorca projektowego Singleton może wydawać się, że nie otrzymujemy zbyt wiele – w końcu tylko jeden obiekt – jednak tak naprawdę nasza aplikacja nam za to podziękuje. A szczególnie maszyna wirtualna, na której to uruchamiamy.

Wzorzec projektowy Singleton blokuje możliwość tworzenia wielu obiektów – co za tym idzie nie tworzymy nowego obiektu = nie zajmujemy kolejnego miejsca w pamięci maszyny.

Rozumiesz już? Tworzenie kolejnych obiektów wymaga coraz więcej pamięci co w końcu może spowodować problem z maszyną wirtualna – oczywiście w bardziej skrajnych przypadkach, jednak jest to możliwe. I z tym walczy wielu architektów, aby stworzyć aplikację, tak aby nadążała zwalniać niepotrzebne fragmenty pamięci.

Punkt startu

Skoro już wiesz czym jest Singleton to czas ruszyć z tym jak jego schemat zaimplementować.

Do tego będzie potrzebowali jakiś prosty Service – w końcu Singleton w jego przypadku ma sens. 😉

Implementacja NumberService jest prosta, mamy pole z listą liczb, konstruktor do stworzenia obiektu oraz dwie proste metody.

package pl.maniaq;

import java.util.ArrayList;
import java.util.List;

public class NumberService {

    List<Integer> numbers = new ArrayList<>();

    public NumberService() {

    }

    public void addNumber(Integer number) {
        numbers.add(number);
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
}

Tyle nam w zupełności wystarczy – stwórzmy sobie teraz ten obiekt w main oraz użyjmy tych metod. 😉

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      NumberService numberService = new NumberService();
      numberService.addNumber(1);
      numberService.addNumber(5);

      System.out.println(numberService.getNumbers());
    }
}

Listę można wyświetlić bez pętli, wystarczy ją wrzucić do System.out.print. 😉

Jak widzisz nic skomplikowane, tworzymy obiekt przy użyciu new wykorzystujemy dostepne metody. Jednak w tym przypadku możemy tworzyć wiele obiektów, jednak jak wyglądałaby nasza klasa, gdybyśmy zaimplementowali Singleton?

Implementacja Singletonu

Głównym założeniem Singletonu jest to, że można stworzyć tylko jeden obiekt klasy. Jak możemy wypełnić to założenie w Javie?

Musimy zablokować możliwość tworzenia obiektu – możemy to zrobić tworząc prywatny konstruktor. Dzięki takiemu ruchowi nie będzie można użyć konstruktora do stworzenia obiektu.

package pl.maniaq;

import java.util.ArrayList;
import java.util.List;

public class NumberService {

    List<Integer> numbers = new ArrayList<>();

    private NumberService() {

    }

    public void addNumber(Integer number) {
        numbers.add(number);
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
}

Jak zajrzysz w między czasie na Main to IntelliJ już nam mówi, że nie jest w stanie stworzyć obiektu. Jednak przejdźmy dalej.

Drugim założeniem jest to, aby móc umożliwić tworzenie tylko jednego obiektu – jak to zrobić?

W tym celu trzeba stworzyć metodę statyczną – czemu statyczną?

Musi być statyczna, aby móc się do niej dostać bez tworzenia obiektu – będzie dla nas taka furtka, przez którą będziemy mogli wyprowadzić obiekt. 😉

Metoda zazwyczaj nazywa się getInstance() – która mówi nam, że zwraca instancję danej klasy. Równie dobrze może się nazywać getNumberService lub jeszcze inaczej – tak naprawdę tak jak tylko chcesz. Jednak my zostaniemy przy getInstance.

public static NumberService getInstance() {
    
}

Zauważ, że nasza metoda ma zwracać instancję klasy, więc musi być takiego samego typu jak klasa.

Teraz potrzebujemy gdzieś przechowywać instancję klasy – będzie to pole prywatne, statyczne gdzie będzie własnie ten jeden kokretny nasz wymarzony obiekt. 😉

private static NumberService instance = new NumberService();

No i teraz możemy zwrócić naszą instancję przez naszą furtkę – czyli getInstance().

public static NumberService getInstance() {
    return instance;
}

I cała nasza klasa wygląda tak:

package pl.maniaq;

import java.util.ArrayList;
import java.util.List;

public class NumberService {

    List<Integer> numbers = new ArrayList<>();

    private static NumberService instance = new NumberService();

    public static NumberService getInstance() {
        return instance;
    }

    private NumberService() {

    }

    public void addNumber(Integer number) {
        numbers.add(number);
    }

    public List<Integer> getNumbers() {
        return numbers;
    }
}

Spójrzmy teraz jak tworzy się obiekt takiej klasy.

Daj mi instancję

Przejdźmy do poprzedniego Maina:

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      NumberService numberService = new NumberService();
      numberService.addNumber(1);
      numberService.addNumber(5);

      System.out.println(numberService.getNumbers());
    }
}

Który już nie działa po naszych zmianach. Chcielibysmy jednak mieć ciągle NumberService, jak to zrobić?

Oczywiście musimy usunąć wywołanie konstruktora czyli new NumberService() – i zastąpić go naszą furtką czyli – getInstance().

NumberService numberService = NumberService.getInstance();

I cały kod już się kompiluje i wygląda tak:

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      NumberService numberService = NumberService.getInstance();
      numberService.addNumber(1);
      numberService.addNumber(5);

      System.out.println(numberService.getNumbers());
    }
}

I nawet działa poprawnie. 😉

[1, 5]

Czy, aby na pewno Single?

Na początku mówiłem, że założeniem singletonu jest to, aby móc utworzyć tylko jedną instancję obiektu – sprawdźmy to.

„Stwórzmy” dwa obiekty NumberService i sprawdźmy ich adresy przy użyciu operatora == – w końcu już wiesz, że dzięki nim możemy sprawdzić czy wskazują na ten sam obszar w pamięci.

package pl.maniaq;

public class Main {

    public static void main(String[] args) {
      NumberService numberService1 = NumberService.getInstance();
    NumberService numberService2 = NumberService.getInstance();

    System.out.println("Equals: " + (numberService1==numberService2));
    }
}

Po uruchomieniu otrzymujemy:

Equals: true

Czyli jak widać są to te same obiekty – Singleton działa, nie ma prawa być inaczej. 😉

Wersja ulepszona

Singleton jest zazwyczaj pokazywany w innej wersji, nieco ulepszone. W naszym przypadku od razu tworzyliśmy obiekt:

private static NumberService instance = new NumberService();

Jednak lepszym rozwiązaniem jest tego nie robić i przypisać mu na początku null.

private static NumberService instance = null;

A dopiero w metodzie getInstance() sprawdzać czy instance jest nullem czy nie.

public static NumberService getInstance() {
    if (instance == null) {
        instance = new NumberService();
    }

    return instance;
}

Czemu jest to wersja ulepszona?

Jest to wersja nieco lepsza, ponieważ obiekt jest tworzony dopiero podczas chęci użycia go.

Póki nie użyjemy metody getInstance, to tak naprawdę obiekt NumberService nie zostanie jeszcze utworzony. Czyli nie zajmie zbędnej pamięci.

Podsumowanie

Po tej lekcji powinieneś już wiedzieć czym są wzorce projektowe oraz jaki problem rozwiązuje wzorzec projektowy Singleton.

Implementacja nie powinna być już dla Ciebie problemem, nie jest zbyt skomplikowana – choć to nie wyklucza, aby stworzyć kilka Singletonów. Jednak czas na to przyjdzie w co tygodniowej aplikacji domowej. 😉

Kamil Klimek

Kamil Klimek

Pierwszy kalkulator napisany w języku Pascal w podstawówce. Później miałem trochę przygód z frontendem oraz PHP, na studiach poznałem C++ oraz Jave. Obecnie prawie 3 letnie doświadczenie jako Java full stack develop. Blog jest miejscem, dzięki któremu mogę się dzielić wiedzą i pomagać innym w nauce programowania.
Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x