Singleton
Koziołek
1 Informacje podstawowe
2 Omówienie szczegółowe
2.1 Wstęp
2.2 Opis problemu
2.3 Implementacja
2.4 Koszty i Problemy
3 Powiązane wzorce
4 Przykładowe implementacje
4.5 Java
Informacje podstawowe
Nazwa: Singleton
Klasyfikacja: Wzorzec konstrukcyjny - Design Patterns: Elements of Reusable Object-Oriented Software - GoF (Gang of Four)
Diagram Obiektów:
Omówienie szczegółowe
Wstęp
Singleton jest jednym ze wzorców zdefiniowanych przez GoF. Wywodzi się z języka C++ gdzie był następcą zmiennej globalnej wstawianej przez preprocesor. Ze względu na prosty diagram klas, złożony z jednej klasy, jest mylnie postrzegany jako prosty i łatwy w zrozumieniu wzorzec. Wielu programistów rozpoczynających swoją przygodę ze wzorcami projektowymi zaczyna od singletonu. Pozory jednak mylą.
Opis problemu
Załóżmy, że mamy pewien obiekt, o którym wiemy, że powinien być jeden w całym systemie. Ograniczenie do tylko jednego egzemplarza wymusza na nas zastosowanie jakiegoś rozwiązania pozwalającego na kontrolę ilości tworzonych egzemplarzy. Najprostszym rozwiązaniem jest sprawienie, że implementacja obiektu będzie wykluczać możliwość stworzenia wielu egzemplarzy. Sposobami implementacji zajmiemy się za chwilę. Skupmy się jeszcze przez moment na celowości stosowania tego wzorca.
Joshua Kerievsky w swojej książce Refactoring to Patterns (recenzja) użył określenia "singletonism" do opisania swoistej jednostki chorobowej, na którą zapadają programiści. Głównym objawem jest stosowanie Singletonów zawsze i wszędzie nie zależnie od tego czy są faktycznie potrzebne i czy ich prowadzenie nie utrudni rozbudowy lub modyfikacji kodu. Chorują zazwyczaj programiści i projektanci, którzy nie mieli jeszcze okazji do refaktoryzacji kodu wyposażonego w same singletony. Z tego właśnie powodu bardzo ważne jest zrozumienie gdzie należy stosować obiekty, które są singletonami.
Popatrzmy na klasę i problem, który chcemy rozwiązać za pomocą singletonu raz jeszcze i odpowiedzmy na następujące pytania:
- Czy klasa którą chcemy przerobić na singleton rzeczywiście będzie występować tylko w jednym egzemplarzu?
- Czy istnieje jakiekolwiek prawdopodobieństwo, że będziemy musieli rozszerzyć tą klasę?
- Czy obiekt, który będziemy tworzyć będzie przechowywał stan?
- Czy stan jest współdzielony przez różne inne obiekty?
- Czy język programowania, którego używamy umożliwia ręczne ładowanie tej samej klasy w wielu różnych punktach kodu?
- Czy naprawdę jesteśmy przekonani do tego rozwiązania?
Jeżeli odpowiedź na którekolwiek z tych pytań brzmi "nie" to należy poszukać innej drogi rozwiązania problemu. W przeciwnym wypadku zamiast wzorca zastosujemy wzorowy antywzorzez.
Oczywiście praktyka pokoleń programistów i projektantów stworzyła listę problemów, które idealnie nadają się do rozwiązania za pomocą singletonu. Poniższa lista zawiera klasy, które można co do zasady zaimplementować jako singletony: - Główna klasa programu - zazwyczaj jest tylko jedna. Reprezentuje środowisko pracy i dobrze by było gdyby była tylko jedna.
- Klasa reprezentująca główne okno programu w aplikacji okienkowej - taki obiekt jest zazwyczaj tylko jeden i należy unikać sytuacji w której tworzone jest wiele jego egzemplarzy.
Jak widać jest to krótka lista, reprezentująca w ogólności klasy, które są korzeniem systemu i o których można powiedzieć, że reprezentują program przed użytkownikiem. Można zatem powiedzieć, że jeżeli tworzymy singleton i nie jest to miejsce jedynego kontaktu systemu z użytkownikiem to należy się nad tym dokładnie zastanowić.
Implementacja
Najprostszy singleton składa się z:
- prywatnego konstruktora domyślnego i prywatnego konstruktora kopiującego (jeżeli język umożliwia jego tworzenie np. C++), które wykluczają tworzenie instancji poza obiektem.
- prywatnego statycznego pola reprezentującego jedyną instancję singletonu.
- metody dostępowej zwracającej instancję singletona.
Metoda dostępowa zazwyczaj sprawdza czy pole instancji nie jest puste (null) i jeżeli jest to tworzy obiekt. Oznacza to oczywiście, że obiekt jest tworzony przy pierwszej próbie pobrania. Jest to forma opóźnionej/leniwej inicjacji (Lazy Initialization), która pozwala na oszczędzenie zasobów poprzez alokowanie ich w ostatnim momencie. Metoda dostępowa może byś też zaimplementowana tak by inicjacja obiektu była bezpieczna z punktu widzenia wątków. Implementacja taka jest rożna w zależności od konkretnego języka.
Koszty i Problemy
Jak łatwo zauważyć singleton pomimo prostoty jest bardzo skomplikowanym wzorcem, który może przysporzyć wielu problemów. Ich źródłem jest zazwyczaj prywatny konstruktor, który skutecznie uniemożliwia rozszerzanie klasy (oczywiście w zależności od języka, ale zazwyczaj tak jest). Kolejnym problemem jest mała odporność na zmiany. Jeżeli w nieprzemyślanym projekcie nagle okazuje się, że potrzeba wielu instancji klasy to przebudowa całości do jakiejś znośnej postaci jest zazwyczaj bardzo kosztowna. Bardzo często popełnianym błędem jest tworzenie fabryk jako singletonów idące w parze z pominięciem tworzenia interfejsu dla fabryki. Takie rozwiązanie powoduje, że nie dość iż jesteśmy skazani na "jedyną słuszną" implementację to nie mamy jeszcze punktu zaczepienia, który umożliwiał by stworzenie własnej. Rozwiązaniem jest stworzenie w klasie abstrakcyjnej metody fabrykującej, która będzie dostarczała domyślnej implementacji danego obiektu. Jedno cześnie nie ma tu ograniczeń w rozszerzaniu klas. Kolejnym problemem, który pojawia się razem z singletonem jest utrudnienie stowsowania wzorca Dekorator. Nie mamy możliwości przesłonięcia obektu dekoratorami ponieważ nie ma możliwości rozszerzenia go.
Problematyczne jest też to, że klasy singletonu łamią zasadę pojedyńczej odpowiedzialności (SRP - Single Responsibility Principle). Odpowiedają one nie tylko za logikę biznesową związaną z obiektem, ale też za tworzenie i zarządzanie własnym cyklem życia.
Jak widać wprowadzenie tego wzorca "na pałę" może okazać się kosztowne. Chcąc uniknąć problemów i jednocześnie zachować możliwość kontroli ilości tworzonych instancji należy użyć wzorca Metoda Fabrykująca lub Fabryka Abstrakcyjna.
Powiązane wzorce
Singleton nie wiąże się bezpośrednio z żadnym ze wzorców.
Przykładowe implementacje
Przykładowe implementacje wzorca w różnych językach.
Java
Implementacja naiwna. Zakłada, że nie mamy do czynienia z środowiskiem wielowątkowym.
package eu.runelord.programmers.io.dp.singleton;
public class SimpleSingleton {
private static SimpleSingleton INSTANCE;
private SimpleSingleton(){}
public static SimpleSingleton getInstance(){
if(INSTANCE==null)
INSTANCE = new SimpleSingleton();
return INSTANCE;
}
}
Ta wersja nie jest bezpieczna w środowisku wielowątkowym. Operacja if(INSTANCE==null) może zostać przerwana po sprawdzeniu warunku, ale przed ustawieniem wartości pola INSTANCE. Jeżeli inny wątek wywoła w tym momencie tą metodę i otrzyma obiekt to nasz pierwotny wątek utworzy nowy obiekt i otrzyma jego kopię.
Wersja bezpieczna, która działa w dowolnej wersji JVM.
package eu.runelord.programmers.io.dp.singleton;
public class ThreadSafeSingleton {
private ThreadSafeSingleton() {}
private static class SingletonHolder {
private final static ThreadSafeSingleton instance = new ThreadSafeSingleton();
}
public static ThreadSafeSingleton getInstance() {
return SingletonHolder.instance;
}
}
W tym przypadku obiekt jest tworzony w momencie wywołania, ale jako, że ładowanie klasy SingletonHolder, i tym samym tworzenie obiektu ThreadSaveSingleton dla pola instance, jest synchronizowane więc nie ma możliwości stworzenia dwóch instancji. Diagram UML tej implementacji:
Wraz z naprawieniem wielu bugów w sposobie zarządzania wątkami w Javie 1.5 jest też dostępna inna możliwość implementacji takiego rozwiązania dla kolejnych wersji języka[#]_.
package eu.runelord.programmers.io.dp.singleton;
public class DoubleCheckSingleton {
private static DoubleCheckSingleton INSTANCE;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (INSTANCE == null)
synchronized (DoubleCheckSingleton.class) {
if (INSTANCE == null)
INSTANCE = new DoubleCheckSingleton();
}
return INSTANCE;
}
}
W tym przypadku mamy do czynienia z synchronizacją wątków na DoubleCheckSingleton.class. Jest to najwydajniejsze rozwiązanie dla javy od wersji 1.5 i należy je stosować. Diagram UML jest taki sam jak dal podstawowej wersji wzorca.
.. [#]
http://kadoel-kawaczyjava.blogspot.com/2011/08/jak-poprawnie-zaimplementowac-wzorzec.html
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
Proponuję poprawić słowo: niezależnie - pisze się razem :) i nie "tą klasę" tylko "tę klasę". Propagujmy poprawną pisownię :D
Poza tym dobry artykuł, dzięki za udostępnienie!
"wzorowy antywzorzez."
@Koziołek
Z tego co się orientuję to Singlentonów nie można rozszerzać. Dlatego w opisie problemu i pytaniach na które trzeba sobie odpowiedzieć przed użyciem singlentona to w punkcie 2 który brzmi "Czy istnieje jakiekolwiek prawdopodobieństwo, że będziemy musieli rozszerzyć tą klasę?" odpowiedź powinna być "nie" ale wg. tego co pod tymi punktami piszesz to w takim wypadku nie powinno się stosować singlentona więc chyba jest tam drobny błąd.
@GhostDog, a to ciekawe podejście. Zastanawiam się tylko jaki jest cel użycia typu wyliczeniowego.
Swoją drogą różne implementacje tego samego wzorca:
http://koziolekweb.pl/2008/12/10/singleton-inaczej/
Spotkałem się z taką implementacją singletonu, co o tym myślisz, nie jest to po prostu zwykłe opakowanie globalnej metody?
enum HelloWorldSingleton {
INSTANCE;
void sayHello(print("Hello World");
}
Jawny konstruktor kopiujący jest cechą C++ i nie występuje w np. Javie, Rubym. Zapraszam do dyskusji.