Singleton – 7 błędów, które popełniasz wykorzystując singleton

Singleton

Mimo iż singleton jest jednym z podstawowych wzorców projektowych, zdecydowana większość jego implementacji zawiera karygodne błędy projektowe. Sam się przekonaj i sprawdź, czy dobrze to robisz. Dzięki temu materiałowi dosłownie w 15 minut nauczysz się implementować singleton poprawnie i unikniesz potencjalnych kłopotów.

Co to jest singleton?

Zacznijmy jednak od drobnego wprowadzenia – zapoznaj się z podstawową definicją singletonu oraz poniższymi przykładami, jak można wykorzystać go w praktyce.

Singleton – to kreacyjny wzorzec projektowy, który ma na celu ograniczenie ilości tworzonych obiektów danej klasy tylko do jednej instancji.

Wzorzec ten może być zastosowany wszędzie tam, gdzie chcemy przechować jeden spójny obiekt dostępny z wielu miejsc aplikacji.

Singleton – przykłady zastosowania

Singleton – przykłady zastosowania

Przykładem wykorzystania singletonu może być klasa przechowująca konfigurację aplikacji. Z każdego miejsca w systemie możemy ją zmodyfikować i chcemy, żeby zmiany były widoczne również z dowolnego miejsca.
Jednocześnie nie możemy pozwolić na to, by w systemie były utrzymywane różne wersje konfiguracji.

Struktura wzorca – implementacja

Jest wiele możliwości implementacji singletonu, jednak wszystkie można sprowadzić do dwóch głównych punktów:

1. Zapewnij tylko jedną instancję

Należy ograniczyć swobodną możliwość tworzenia nowych instancji klasy. Można to osiągnąć przez oznaczenia konstruktora prywatnym modyfikatorem dostępu.

Ponieważ „z zewnątrz” nie można już utworzyć nowej instancji, klasa sama musi zarządzać swoimi instancjami i w odpowiednim momencie ją utworzyć.

2. Przygotuj globalny punkt dostępu

Żeby móc pobrać instancje naszej klasy, należy przygotować jakiś punkt dostępu, który powinien być dostępny globalnie.

Najczęściej realizuje się to przez statyczną metodę zwracającą przygotowaną wcześniej instancję singletonu.

Singleton Java – przykładowa implementacja

Poniżej najprostsza możliwa implementacja singletonu w Javie.

public class SingletonSimpleEager {

    private static final SingletonSimpleEager instance = new SingletonSimpleEager();

    private SingletonSimpleEager() {
    }

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

// wywołanie
SingletonSimpleEager singleton = SingletonSimpleEager.getInstance();

Zgodnie z przedstawioną wcześniej strukturą wzorca mamy prywatny konstruktor oraz globalny punkt dostępu w formie statycznej metody.

Singleton inicjalizacja obiektu eager vs lazy

Powyższa implementacja nie wykorzystuje jednak bardzo ważnej zalety singletonu – mianowicie inicjalizacji obiektu, dopiero kiedy zajdzie taka potrzeba. Instancja zostanie utworzona już w momencie załadowania klasy, nawet jeżeli nigdy nie zajdzie potrzeba jej wykorzystania.

Dzięki implementacji leniwego tworzenia obiektu (ang. lazy loading) można odwlec w czasie moment kosztownego budowania instancji obiektu oraz potencjalnie oszczędzić zasoby, jeżeli nie będzie potrzeby wcale jego utworzenia.

Poniżej zmodyfikujemy przykład z nowymi założeniami.

public class SingletonSimpleLazy {

    private static SingletonSimpleLazy instance;

    private SingletonSimpleLazy() {
    }

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

W tym przykładzie przenieśliśmy moment generowania instancji do statycznej metody. Kod singletonu powoli nabiera kształtów.

Pytanie jednak, czy jest on już wystarczająco dobry.

W poszukiwaniu „idealnego” singletonu

W kolejnych krokach będziemy próbować złamać pierwotne założenia singletonu oraz postaramy się znaleźć sposób, jak można się przed tym zabezpieczyć.

Każdą kolejną implementację będziemy testować, sprawdzając, czy powstałe instancje znajdują się w tym samym miejscu w pamięci. Jeżeli uda nam się utworzyć dodatkową instancję singletonu, to znaczy, że kod nie jest odporny na tę metodę.

 SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance();
 SingletonSimpleLazy secondInstance = SingletonSimpleLazy.getInstance();
 
 assertEquals(firstInstance, secondInstance);

Podstawowe wywołanie zadziałało – metoda getInstance() zwróciła dwukrotnie obiekt znajdujący się dokładnie w tym samym miejscu w pamięci.

Poszukajmy teraz czegoś trudniejszego.

1. Refleksja

Refleksja jest bardzo potężnym i potencjalnie niebezpiecznym narzędziem. Przy jej pomocy możemy modyfikować właściwości załadowanego kodu aplikacji jeszcze podczas jej działania.

W naszym wypadku, w celu utworzenia nowej instancji wykonamy trzy kroki:

  1. przy pomocy refleksji pobierzemy prywatny konstruktor z klasy;
  2. zmienimy jego modyfikator dostępu na publiczny;
  3. następnie przy jego pomocy utworzymy nową instancję naszej klasy.
SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance();

// 1
Constructor<SingletonSimpleLazy> constructor = SingletonSimpleLazy.class.getDeclaredConstructor();
// 2
constructor.setAccessible(true);
// 3
SingletonSimpleLazy secondInstance = constructor.newInstance();

assertNotEquals(firstInstance, secondInstance);
FAILED W kilku stosunkowo prostych krokach udało nam się utworzyć nową instancję klasy, czyli złamać założenia singletonu.

Spróbujmy teraz zabezpieczyć nasz kod przed taką ingerencją. W tym celu zmodyfikujemy konstruktor tak, żeby sprawdzał, czy statyczne pole instance jest już uzupełnione. Jeżeli instancja została już utworzona wcześniej, powinien zgłosić błąd.

public class SingletonReflection {

    private static SingletonReflection instance;

    private SingletonReflection() {
        if (instance != null) {
            throw new IllegalStateException("Cannot create new instance, please use getInstance method instead.");
        }
    }

    public static SingletonReflection getInstance() {
        if (instance == null) {
            instance = new SingletonReflection();
        }
        return instance;
    }
}
SUCCEED Udało nam się w ten sposób zabezpieczyć przed utworzeniem nowej instancji. Teraz próba utworzenia kolejnego obiektu tej klasy zostanie przerwana wyjątkiem.

Nie jest to jednak idealne zabezpieczenie. Wyobraź sobie, co się stanie, jeżeli próba utworzenia instancji przy pomocy refleksji zostanie wywołana przed standardowym wywołaniem metody: Singleton.getInstance().

Zapraszam do dyskusji w komentarzach. 👇👇👇

2. Serializacja i deserializacja

Serializacja polega na zamianie obiektu np. na strumień bajtów, który później podczas deserializacji możemy odtworzyć jako nowy obiekt. Więcej o samej serializacji pisałem w tekście o klonowaniu przez serializację.

Co dla nas w tym momencie jest bardzo ważne, podczas deserializacji nie jest zachowany standardowy cykl życia obiektu – podczas jego tworzenia nie jest wywoływany żaden konstruktor!

Dlatego korzystając z tego mechanizmu, również możemy utworzyć nową instancję pierwotnego singletonu.

SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance();
SingletonSimpleLazy secondInstance = null;

try (FileOutputStream fos = new FileOutputStream("./SingletonSimpleLazy.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
    oos.writeObject(firstInstance);
}

try (FileInputStream fis = new FileInputStream("./SingletonSimpleLazy.ser"); ObjectInputStream ois = new ObjectInputStream(fis)) {
    secondInstance = (SingletonSimpleLazy) ois.readObject();
}

assertNotEquals(firstInstance, secondInstance);
FAILED Po raz kolejny udało nam się utworzyć nową instancję z pominięciem zabezpieczeń. Całe szczęście specyfikacja Javy przewidziała na taką sytuację specjalną metodę: readResolve.

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

Jeżeli klasa posiada taką metodę, to podczas deserializacji wynik jej działania jest zwracany zamiast standardowej metody: readObject.

public class SingletonSerializable implements Serializable {

    private static SingletonSerializable instance;

    private SingletonSerializable() {
    }

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

    private Object readResolve() throws ObjectStreamException {
        return getInstance();
    }
}
SUCCEED Problem z serializacją obiektów występuje tylko wtedy, jeżeli jawnie oznaczymy naszą klasę jako serializowaną przy pomocy interfejsu: java.io.Serializable. W przeciwnym wypadku obiektu wcale nie uda się zserializować.

3. Klonowanie

Java udostępnia wbudowany mechanizm klonowania obiektów, dzięki któremu również można ominąć standardowy cykl budowania tych obiektów.

SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance();
SingletonSimpleLazy secondInstance = firstInstance.clone();
        
assertNotEquals(firstInstance, secondInstance);
FAILED Można się przed tym zabezpieczyć rezygnując z klonowania obiektów lub, podobnie jak w przypadku serializacji, w metodzie clone zwrócić istniejącą już instancję singletonu.

public class SingletonCloneable implements Cloneable {

    private static SingletonCloneable instance;

    private SingletonCloneable() {
    }

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

    @Override
    public SingletonCloneable clone() throws CloneNotSupportedException {
        return getInstance();
    }
}
SUCCEED W przypadku klonowania, podobnie jak przy serializacji, klasa musi najpierw być jawnie oznaczona jako zdolna do klonowania.

4. Wielowątkowość [multi thread]

W wielowątkowym środowisku może dojść do sytuacji, w której kilka niezależnych wątków w tym samym czasie próbuje wykonać nasz kod. Jeżeli nie zabezpieczymy się przed tym odpowiednio, może to prowadzić do nieprzewidzianych sytuacji.

Przeprowadźmy test, który zasymuluje taką sytuację:

Runnable task1 = () -> {
    firstInstanceForTestBreakWithThreads = SingletonSimpleLazy.getInstance();
};
Runnable task2 = () -> {
    secondInstanceForTestBreakWithThreads = SingletonSimpleLazy.getInstance();
};

int testSuccess = 0;
int testFail = 0;
for (int i = 0; i < 1000; i++) {
    SingletonSimpleLazy.instance = null;
    ExecutorService service = Executors.newFixedThreadPool(2);
    service.submit(task1);
    service.submit(task2);
    service.shutdown();
    service.awaitTermination(1, TimeUnit.SECONDS);

    if (firstInstanceForTestBreakWithThreads != null && secondInstanceForTestBreakWithThreads != null && firstInstanceForTestBreakWithThreads.equals(secondInstanceForTestBreakWithThreads)) {
        testSuccess++;
    } else {
        testFail++;
    }
}

System.out.println(String.format("testSuccess: %d, testFail: %d", testSuccess, testFail));

Test polega na próbie pobrania instancji metodą: getInstance() przez dwa niezależne wątki. Test uznajemy za zaliczony, jeżeli obie instancje znajdują się w tym samym miejscu w pamięci. Natomiast, jeżeli jedna z nich jest pusta lub zostały utworzone dwa obiekty, test uznajemy za niezaliczony.

Dodatkowo test uruchomiłem 1000 razy, żeby zobaczyć uśredniony wynik. Żeby test mógł być uruchamiany wielokrotnie, przed każdym powtórzeniem trzeba jeszcze zresetować zmienną statyczną przechowującą instancję – SingletonSimpleLazy.instance = null;. W ramach testu chwilowo zmieniłem jej modyfikator dostępu na publiczny.

testSuccess: 854, testFail: 146

FAILED Wynik pokazuje, że około 15% wywołań kończy się porażką.

Niby większość zaliczona, natomiast jeżeli Ci to nie wystarcza, zapraszam do dalszej lektury.

Dlaczego niektóre testy zakończyły się porażką?

Żeby odpowiedzieć na to pytanie, przypomnijmy sobie, jak wygląda metoda getInstance() .

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

Jeżeli w kodzie aplikacji nie ma zdefiniowanych żadnych semaforów – czyli blokad wskazujących wątkom, w którym miejscu mają wstrzymać swoje działanie, kolejność, w jakiej poszczególne fragmenty kodu zostaną wykonane przez różne wątki, może być różna. Prowadzi to, do tak zwanych wyścigów.

W naszym przykładzie oba wątki mogły jednocześnie sprawdzić warunek, czy instancja jest równa null i w obu wypadkach była to prawda. Dlatego zostały utworzone dwie instancje.

Żeby się przed tym zabezpieczyć, wprowadzimy odpowiednie blokady/semafory.

Synchronizacja wątków

Synchronizacja wątków polega na zdefiniowaniu specjalnych sekcji krytycznych, do których w danym momencie może uzyskać dostęp tylko jeden wątek.

Warto również zauważyć, że problem ten występuje tylko, jeżeli chcemy skorzystać z leniwego tworzenia instancji.

Metoda synchronized

Najprostszą możliwą forma synchronizacji w tym wypadku jest utworzenie sekcji krytycznej w ramach całej metody getInstance.

public synchronized static SingletonSimpleLazy getInstance() {
    if (instance == null) {
        instance = new SingletonSimpleLazy();
    }
    return instance;
}

Po tej modyfikacji tylko jeden wątek będzie mógł wejść do tej metody w tym samym czasie.

INFO Uzyskaliśmy dzięki temu zabezpieczenie przed utworzeniem nowej instancji, jednak spowolniliśmy jednocześnie bardzo naszą aplikację. W obecnej wersji nawet jak instancja będzie już utworzona, wszystkie wątki będą musiały poczekać na swoją kolej, żeby ją pobrać.

Powstało w ten sposób wąskie gardło, które w kolejnych krokach postaramy się wyeliminować.

Blok synchronizowany

Przenieśmy sekcję krytyczną trochę niżej – za warunek sprawdzający istnieje instancji. Dzięki temu po utworzeniu instancji nie będzie już potrzeby kolejkowania wątków.

public static SingletonSimpleLazy getInstance() {
    if (instance == null) {
        synchronized (SingletonSimpleLazy.class) {
            instance = new SingletonSimpleLazy();
        }
    }
    return instance;
}
INFO Niestety to rozwiązanie również nie jest idealne. Może bowiem dojść do sytuacji, w której więcej niż jeden wątek będzie czekał na wejście do sekcji krytycznej. Wtedy, po zwolnieniu blokady, on również utworzy nową instancję.

Blokada z podwójnym zatwierdzeniem [Double-checked locking optimization]

Na takie sytuacje powstał specjalny wzorzec projektowy: blokada z podwójnym zatwierdzeniem. Polega to na podwójnym sprawdzeniu warunku: najpierw przed blokadą i potem jeszcze raz po przejściu przez blokadę.

public static SingletonSimpleLazy getInstance() {
    if (instance == null) {
        synchronized (SingletonSimpleLazy.class) {
            if (instance == null) {
                instance = new SingletonSimpleLazy();
            }
        }
    }
    return instance;
}
INFO Teraz nawet jak kilku wątkom uda się przejść przez blokadę, to zostaną na niej skolejkowane i nie przejdą przez drugi warunek.

Została nam do poprawy już tylko jedna rzecz. W ramach optymalizacji zmienne w pamięci komputera przechowywane są w keszu procesora. Dlatego może dojść do sytuacji, w której jeden wątek ustawi poprawnie wartość zmiennej, ale drugi podejmie próbę jej odczytania, zanim zostanie ona jeszcze zsynchronizowana.

Zmienna volatile

Specyfikacja Javy pozwala określić, które zmienne mają być zawsze przechowywane we współdzielonej pamięci, a nie w keszu. Dzięki oznaczeniu zmiennej jako volatile niezależnie od tego, który wątek próbuje ją odczytać, jej wartość będzie zawsze spójna.

private static volatile SingletonSimpleLazy instance;
SUCCEED Po tych zmianach mamy już pewność, że nasz kod jest odpowiednio przygotowany do działania w środowisku wielowątkowym.

Classloader

Java pobiera klasy przez specjalne classloadery. W prostych aplikacjach desktopowych zazwyczaj mamy do czynienia z jednym domyślnym classloaderem i najczęściej jego istnienie jest nawet niezauważalne. Jednak w środowisku JavaEE, np. w kontenerze serwletów wykorzystywanych jest więcej niż jeden classloader jednocześnie.

Może wtedy dojść do utworzenia niezależnego obiektu singletonu w ramach każdego z nich.

Zasymulujemy taką sytuację w środowisku JavaSE w trzech kolejnych krokach:

SingletonSimpleLazy firstInstance = SingletonSimpleLazy.getInstance();
// 1
String targetJarFile = "file://"+Paths.get(".").toAbsolutePath().resolve("target/singleton-1.0.jar").toString();
ClassLoader classLoader = new URLClassLoader(new URL[]{new URL( targetJarFile)}, null);
// 2
Class singletonClass = classLoader.loadClass(SingletonSynchronized.class.getCanonicalName());
// 3
Method getInstanceMethod = singletonClass.getDeclaredMethod("getInstance");
Object secondInstance = getInstanceMethod.invoke(singletonClass);

assertNotEquals(firstInstance, secondInstance);
  1. Klasa naszego singletonu została już pobrana przez domyślny classloader. Spróbujemy pobrać ją jeszcze raz, korzystając z klasy URLClassLoader. W tym celu potrzebujemy podać ścieżkę do zbudowanego wcześniej pliku jar z naszą klasą;
  2. W drugim kroku pobieramy ponownie klasę na podstawie jej pełnej nazwy (ang. fully qualified class name);
  3. W ostatnim kroku pobieramy statyczną metodę getInstance i korzystając z niej, tworzymy nowy obiekt singletonu.
FAILED Udało nam się to zrobić, ponieważ w Javie ta sama klasa załadowana przez różne classloadery jest traktowana jak dwie różne klasy.

INFO Bardzo ciężko jest się przed tym zabezpieczyć. Można próbować zdefiniować ręcznie classloader i korzystać z niego, jednak nie jest to w 100% skuteczny sposób.

private static Class getClass(String classname) throws ClassNotFoundException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if (classLoader == null) {
        classLoader = SingletonSimpleLazy.class.getClassLoader();
    }
    return (classLoader.loadClass(classname));
}

Różne maszyny wirtualne

Jeżeli aplikacja uruchamiana jest na kilku maszynach wirtualnych, np. w środowisku EJB również dojdzie do utworzenia kilku instancji singletonu na każdej maszynie wirtualnej (JVM).

W takiej sytuacji najlepiej unikać singletonów przechowujących lokalny stan obiektu.

Garbage Collection

Jeżeli klasa singletonu zostanie odśmiecona i potem ponownie załadowana, również dojdzie do utworzenia nowej instancji. Jeżeli na obiekt singletonu nie będzie wskazywała żadna referencja, może on zostać usunięty i utworzony ponownie dopiero, jeżeli zajdzie taka potrzeba.

Ten problem występował w starszych wersjach Javy, obecnie nie powinien już mieć miejsca. Więcej na ten temat można przeczytać tutaj.

Alternatywne implementacje singletonu

W poprzednich krokach rozważaliśmy tylko jedną z możliwości implementacji singletonu. Istnieją jednak alternatywne sposoby, np. z wykorzystaniem statycznej klasy lub enuma.

Static holder pattern

Singleton Static holder pattern jest to bezpieczne rozwiązanie i jednocześnie zapewniające dobrą wydajność w wielowątkowym środowisku. Dzięki niemu mamy również zapewnione leniwe utworzenie instancji.

public class SingletonStaticHolder {

    private SingletonStaticHolder() {
    }

    private static class Holder {
        private static final SingletonStaticHolder INSTANCE = new SingletonStaticHolder();
    }

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

W tym rozwiązaniu cały trud poprawnej implementacji wzorca zrzucamy na maszynę wirtualną Javy. Zmienna INSTANCE zostanie zainicjowana dopiero w momencie załadowania klasy Holder, czyli podczas pierwszego wywołania metody getInstance().

Enum

Implementacja singletonu opartego na enumie również zrzuca cały ciężar poprawnej implementacji na Javę. Enumy charakteryzują się tym, że może istnieć tylko jedna instancja danej klasy.

public enum SingletonEnum {
    INSTANCE;
    
    // public methods
}

W tym miejscu warto sprawdzić, jak enum jest wewnętrznie zaimplementowany przez Javę. Posłużymy się w tym celu narzędziem javap:

root@tw:/# javap SingletonEnum.class 
Compiled from "SingletonEnum.java"
public final class pl.stormit.singleton.SingletonEnum extends java.lang.Enum<pl.stormit.singleton.SingletonEnum> {
  public static final pl.stormit.singleton.SingletonEnum INSTANCE;
  public static pl.stormit.singleton.SingletonEnum[] values();
  public static pl.stormit.singleton.SingletonEnum valueOf(java.lang.String);
  static {};
}

Enum to zwyczajna finalna klasa dziedzicząca po java.lang.Enum.

Korzystając z tego rozwiązania, osiągnęliśmy wynik bardzo podobny do klasy SingletonSimpleEager, czyli singletonu z zachłannym tworzeniem instancji.

Gotowe biblioteki a singleton

Zamiast implementować wzorzec ręcznie, można również posłużyć się gotowymi już rozwiązaniami.

Immutables

Immutables to biblioteka do pracy z klasami typu: value object. Jedną z jej funkcjonalności jest automatyczna implementacja wzorca singleton.

@Value.Immutable(singleton = true)
public class SingletonImmutables {
}

// wywołanie
SingletonImmutables firstInstance = ImmutableSingletonImmutables.builder().build();
SingletonImmutables secondInstance = ImmutableSingletonImmutables.builder().build();

assertTrue(firstInstance == secondInstance);

Singleton – zalety

  1. Klasa zaimplementowana jako singleton samodzielnie kontroluje liczbę swoich instancji, dzięki czemu, po wprowadzeniu niewielkich modyfikacji, można przerobić ją na pulę zarządzającą większą ilością obiektów.
  2. Sam proces tworzenia nowej instancji jest niewidoczny dla użytkownika. Dlatego można zaimplementować leniwe tworzenie instancji i przyczynić się do zaoszczędzenia zasobów.
  3. Łatwo można przerobić go na fabrykę obiektów, uniezależniając się od konkretnej implementacji.

Singleton jako antywzorzec

Singleton przez wielu programistów uważany jest za antywzorzec projektowy. Dzieje się tak głównie dlatego, że jest on dość często nadużywany.

Poniżej zestawienie głównych zarzutów wobec tego wzorca. Niektóre z nich przy odrobienie elastyczności można obejść lub nawet odrzucić.

  1. Poważnie utrudnia testowanie aplikacji
    Testy są tylko utrudnione, jeżeli w singletonie przechowywany jest stan. Należy wtedy pamiętać, by był on odpowiednio zainicjowany lub wyczyszczony przed każdym wywołaniem testu.
  2. Brak elastyczności
    Taka jest właśnie specyfika singletonu, że już na poziomie kodu jest na sztywno określona liczba instancji.
  3. Łamie zasadę jednej odpowiedzialności (single responsibility principle)
    Klasa zaimplementowana jako singleton z założenia jest już odpowiedzialna za dwie rzeczy: za realizację swoich funkcji biznesowych oraz zarządzanie instancją.
  4. Łamie zasadę otwarte-zamknięte (Open/Closed principle), ponieważ nie można go rozszerzać
    W pierwotnej wersji wzorca rzeczywiście ciężko jest go rozszerzać. Można jednak połączyć singleton z fabryką i nie będzie stanowiło to już problemu.

    interface Singleton {
    }
    
    public class SingletonFactory {
    
        private static Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton() {
                    // singleton implementation
                };
            }
    
            return instance;
        }
    }
  5. Jest to obiektowy zamiennik zmiennej globalnej
    Tu ciężko się spierać. Rzeczywiście singleton bywa dla wielu programistów swego rodzaju substytutem nieobsługiwanych przez Javę zmiennych globalnych. Dlatego właśnie jest tak ważne, by go nie nadużywać.

Podsumowanie

Dzięki wykorzystaniu singletonu w swoich aplikacjach można zyskać na wydajności oraz przejrzystości kodu. Należy jednak pamiętać, by go nie nadużywać i nie starać się go wprowadzić wszędzie na siłę. Wzorzec ten powinien być stosowany raczej sporadycznie.

Podobnie sprawa ma się, jeżeli chodzi o wszystkie zabezpieczenia przedstawione w artykule. Tu również należy podejść z rozwagą, nie zawsze jest sens wprowadzać wszystko u siebie. Jeżeli piszemy prostą aplikację, uruchamianą w środowisku jednowątkowym i dodatkowo jesteśmy jedyną osobą odpowiedzialną za jej rozwój, z powodzeniem sprawdzi się u nas najprostsza implementacja. Oszczędzimy w ten sposób czas potrzebny na napisanie oraz utrzymanie nadmiarowego kodu.


20+ BONUSOWYCH materiałów z programowania

e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!

Jak zostać programistą

6 komentarzy
Share:

6 Comments

  1. Elon says:

    Temat opisany chyba z każdej możliwej strony i ciężko tu cokolwiek dodać ale spróbuję 😉

    Jako jeden z minusów pierwszej implementacji podajesz eager initialization. Tylko w tym konkretnym przypadku jest ona całkowicie lazy. Nadal do utworzenia obiektu wymagane jest wywołanie dowolnej metody klasy czyli w tym przypadku getInstance(). A skoro już wywołujemy tę metodę to znaczy, że chcemy w tym konkretnym momencie korzystać z obiektu, czyli minus przestaje być minusem.

    1. Tomek says:

      Elon słuszna uwaga.
      Jest to rzeczywiście bardzo specyficzny przypadek i dla tego testu obie implementacje zachowają się bardzo podobnie.

      Rozwijając, inicjalizacja w stylu:
      private static final SingletonSimpleEager instance = new SingletonSimpleEager();
      zostanie wywołana w momencie załadowania klasy SingletonSimpleEager, a klasa jest ładowana dopiero podczas jej pierwszego aktywnego wykorzystania, czyli np. wywołania jednej z metod.
      Drugie rozwiązanie jest o tyle lepsze (klasa SingletonSimpleLazy), że możemy wywołać inne statyczne metody klasy lub wykorzystać jej statyczne zmienne, bez tworzenia instancji singletonu.

Dodaj komentarz

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