#BeEffective – Wzorzec Singleton

Singleton jest wzorcem projektowym, którego celem jest zapewnienie o istnieniu dokładnie jednej instancji danej klasy i udostępnieniu jej globalnie. W tym wpisie poruszę temat jego wykorzystania, zalet, wad oraz sposobów implementacji w oparciu o książkę Joshuy Blocha – Java – Efektywne programowanie.

Przykładem zastosowania singletonu może być klasa przechowująca konfigurację aplikacji. Z każdego miejsca w systemie można ją modyfikować i naszym celem jest, aby zmiany także były wszędzie widoczne, wraz z uniemożliwieniem niespójności tych danych. Drugim przykładem niech będzie aplikacja, która wyposażona jest w klasę do obsługi błędów. Niech ta klasa loguje informacje o błędach do pliku. Należy wtedy zadbać, aby istniał jedyny i unikalny obiekt tej klasy, ponieważ w momencie otwarcia pliku logów będzie on zablokowany na edycję. W tym wypadku także warto skorzystać z singletona.

Innymi słowy – singleton używany jest wszędzie tam, gdzie wymagane jest istnienie JEDNEJ I DOKŁADNIE JEDNEJ instancji danej klasy.

Wielu programistów traktuje singleton jako antywzorzec ze względu na trudność w testowaniu takiegoż (o ile nie implementują one jakiegoś interfejsu), łamanie zasad SOLID oraz bycie czymś w rodzaju obiektowej zmiennej globalnej.

Implementacja Singletona

Istnieje kilka sposobów implementowania singletonów i choć Joshua w swojej książce (wydanie III, 2018) wspomina o dwóch, to z pomocą przychodzi devcave.pl, prezentując ich aż pięć.

Publiczna stała

Sposób polega na tym, że konstruktor singletona pozostaje prywatny, co uniemożliwia uruchomienie go zzewnątrz. Tworzona klasa ma w sobie swój własny obiekt, który zwracany jest jak klasyczna publiczna stała:

 

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();

    private Singleton(){
        // tworzenie obiektu
    }
}

Taki kod jest niestety narażony na programistę korzystającego z refleksji i ustawiającego specyfikator dostępu na publiczny za jej pomocą. Można się przed tym obronić dodając do kodu prostego instantionChecka:

public class Singleton {

    public static final Singleton INSTANCE = new Singleton();

    private Singleton(){
        if (INSTANCE != null){
            throw new IllegalStateException("Only one instance of Singleton class is allowed");
        }
        // tworzenie obiektu
    }

}

W ten sposób zapewniamy sobie istnienie dokładnie jednej instancji klasy w najprostszy sposób.

Statyczna metoda fabrykująca

Wspomniane w jednym z poprzednich wpisów statyczne metody fabrykujące tak samo wydają sę być idealne podczas wykorzystywania singletonów. Ta metoda różni się od poprzedniej jedynie sposobem zwracania instancji klasy.

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){
        if (INSTANCE != null){
           throw new IllegalStateException("Only one instance of Singleton is allowed");
        }
        // tworzenie obiektu
    }

    public static Singleton getInstance(){
        return INSTANCE;
    }

}

Lazy initialization

Obie powyższe metody tworzą instancję klasy już podczas ładowania klasy, nawet, jeśli nie będzie potrzeby z niej skorzystać. Lekarstwem na to może być tzw. leniwe tworzenie obiektu – obiekt klasy zostanie utworzony dopiero w momencie próby pobrania jego instancji.

public class Singleton {

    private static final Singleton INSTANCE;
    
    private Singleton(){
        if (INSTANCE != null){ 
            throw new IllegalStateException("Only one instance of Singleton is allowed");
        } 
        // tworzenie obiektu 
    } 

    public static Singleton getInstance(){ 
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE; 
    } 
}

Aby singleton był serializowalny, należy pamiętać o trzech warunkach, które musza być spełnione:

  1. Klasa musi implementować interfejs Serializable
  2. Wszystkie pola klasy muszą być zadeklarowane jako transient
  3. należy zdefiniować metodę readResolve() tak, aby zwracała instancję singletonu
public class Singleton implements Serializable {

    private static final long serialVersionUID = 1L;   

    private static transient Singleton INSTANCE;

    private Singleton(){
        if (INSTANCE != null){
            throw new IllegalStateException("Only one instance of Singleton is allowed");
        }
        // tworzenie obiektu
    }

    public static getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

    private Object readResolve(){
        return getInstance();
    }
}

W innym wypadku podczas deserializacji klasy tworzona będzie nowa instancja singletona.

Synchronizacja singletona

W przypadku aplikacji wielowątkowej należy zabezpieczyć się przed sytuacją, w której różne wątki w tym samym czasie sprawdzają istnienie instancji klasy. Jeśli wykona się to jednocześnie w trakcie pierwszego wykorzystania obiektu (w przypadku podejścia Lazy  initialization), możemy narazić się na to, że obie klasy sprawdzą INSTANCE ==  null  tym samym czasie, ergo – obie klasy utworzą instanancję obiektu i skończy się na dwóch instancjach Singletona. Aby się przed tym zabezpieczyć, należy wprowadzić odpowiednie mechanizmy blokujące wątki przed taką możliwością, zwane semaforami.

Synchronizowanie metody

Najprostszym ze sposobów na zapewnienie bezpieczeństwa wątkowego danej sekcji jest synchronizowanie metody. Podejście to polega na tym, że gdy jeden wątek uruchamia metodę, ta metoda jest blokowana na dostęp dla innych wątków. Rozwiąże to nasz problem, w którym oba wątki będą mogły dokonać sprawdzenia warunku, na podstawie którego tworzona jest nowa instancja. Niestety kosztem tego, że nawet po utworzeniu instancji, wszystkie wątki będą musiały czekać na swoją kolej, aby ją pobrać.

public class Singleton implements Serializable { 
  private static final long serialVersionUID = 1L; 
  private static transient Singleton INSTANCE; 
  
  private Singleton(){ 
    if (INSTANCE != null){
      throw new IllegalStateException("Only one instance of Singleton is allowed");
    } 
    // tworzenie obiektu 
  } 
  
  public synchronized static getInstance(){ 
    if (INSTANCE == null){ 
      INSTANCE = new Singleton(); 
    } 
    return INSTANCE; 
  } 
  
  private Object readResolve(){ 
    return getInstance(); 
  } 
  
}
Synchronizowanie bloku tworzącego instancję
public class Singleton implements Serializable { 
  private static final long serialVersionUID = 1L; 
  private static transient Singleton INSTANCE; 
  
  private Singleton(){ 
    if (INSTANCE != null){
      throw new IllegalStateException("Only one instance of Singleton is allowed");
    } 
    // tworzenie obiektu 
  } 
  `
  public static getInstance(){ 
    if (INSTANCE == null){
      synchronized(Singleton.class){ 
        INSTANCE = new Singleton(); 
      } 
    }
    return INSTANCE; 
  } 
  
  private Object readResolve(){ 
    return getInstance(); 
  } 
  
}

Jest to już trochę szybsze podejście, ponieważ wątki są zatrzymane w momencie, gdy okaże się, że instancja nie została jeszcze utworzona. Niestety wciąż narażeni jesteśmy na sytuację, w której dwa wątki sprawdzą warunek z linii 13 i zostaną zakolejkowane jedynie przed tworzeniem instancji. Będzie to skutkowało utworzeniem dwóch instancji klasy Singleton. Można to w prosty sposób poprawić wprowadzając tzw. double-check (podwójne sprawdzenie):

public class Singleton implements Serializable { 
  private static final long serialVersionUID = 1L; 
  private static transient Singleton INSTANCE; 
  
  private Singleton(){ 
    if (INSTANCE != null){
      throw new IllegalStateException("Only one instance of Singleton is allowed");
    } 
    // tworzenie obiektu 
  } 
  `
  public static getInstance(){ 
    if (INSTANCE == null){ 
      synchronized(Singleton.class){
        if (INSTANCE == null){
          INSTANCE = new Singleton(); 
        }
      } 
    }
    return INSTANCE; 
  } 
  
  private Object readResolve(){ 
    return getInstance(); 
  } 
  
}

Zwróćcie uwagę na linie 15-17. Po zablokowaniu klasy (synchronized(Klasa.class)) dla pozostałych wątków sprawdzamy po raz kolejny, czy instancja została utworzona. Dzięki temu możemy zabezpieczyć się przed tworzeniem kilku instancji, a w sytuacji, gdy instancja już będzie istnieć – wątki nie będą czekać na siebie, tylko będą pobierać zasób.

Zalety i wady

Zalety wykorzystywania singletona

Główną zaletą i powodem stosowania singletona jest przede wszystkim globalny dostęp do obiektu będącego dla każdego miejsca w systemie w takim samym stanie. Oznacza to, że gdy w miejscu X systemu zmienimy coś w singletonie, w miejscu Y też będzie to widoczne, ponieważ obiekt jest jeden wspólny dla całego systemu. To tyle z kwestii biznesowej przydatności singletona.

Techniczna przydatność jest trochę szersza, ponieważ umożliwia wykorzystanie wzorca singleton do budowy bardziej złożonych konstruktji. Singleton jako klasa sama pilnuje ilości instancji samej siebie, i tak po niewielkich modyfikacjach można zmienić ją w konstrukcję zarządzającą pulą obiektów, lub po prostu na fabrykę.

Wady singletona

Największa zaleta singletona jest jednocześnie jego największą wadą. Dostępność jednego i tego samego obiektu na przestrzeni całego systemu powoduje, że oprócz dostępu w dowolnym miejscu do pewnych danych, możliwa jest często modyfikacja tychże. Powoduje to kłopot, w którym miejsce X systemu oczekuje, że singleton będzie w stanie A, natomiast działania w miejscu Y zamieniają stan singletona na B. Można to zminimalizować ograniczając możliwości wpływania na stan do absolutnego minimum.

Singleton z założenia łamie zasadę SRP – pojedynczej odpowiedzialności. Jest natomiast, poza odpowiedzialnością za wykonywane działania, obarczony także pilnowaniem ilości instancji samej siebie.

Podsumowanie

Nie taki singleton straszny. Dzięki odpowiedniemu wykorzystaniu singletona możemy skorzystać na jednej instancji danej klasy i w ten sposób ograniczyć wykorzystanie zasobów. Należy jednak pamiętać, by nie wprowadzać go na siłę, ponieważ dostępność obiektu z dowolnego miejsca w systemie, przez dowolny wątek naraża programistę na pewne niebezpieczeństwa, szczególnie wydajnościowe.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Strona używa danych zapisanych na komputerze odwiedzającego. Kliknij przycisk AKCEPTUJ COOKIES, aby nie widzieć więcej tego komunikatu Polityka prywatności

Gniado IT wykorzystuje cookies do przechowywania informacji na TWOIM komputerze. Cookies wykorzystywane przez tę witrynę to standardowe cookies wykorzystywane przez silnik Wordpress. Możesz ustawić swoją przeglądarkę w ten sposób, aby blokowała wszelkie próby użycia cookies, jednakże może to skutkować nieprawidłowym działaniem witryny. Dane nie są wykorzystywane do profilowania reklamowego, czy pozyskania wrażliwych / poufnych informacji.

Zamknij