Singleton inaczej
Po dzisiejszym spotkaniu WDPSG nasunął mi się pewien wniosek. Rozmawialiśmy sobie o wzorcu Singleton i jego zastosowaniach. Dyskusja była bardzo ciekawa i rzuciła nowe światło na problem wzorca Singleton. Jednak po kolei.
Patrząc na źródła wiedzy o wzorcu na polskiej i angielskiej wikipedii można dojść do wniosku, że Singleton jest bardzo prostym wzorcem. Wrażenie to potęgowane jest przez dość prosty diagram UML przewijający się we wszystkich publikacjach związanych z tym wzorcem. Wygląda on tak:
Bardzo prosty i bardzo przejrzysty diagram mówiący nam… no właśnie co mówi nam ten diagram? Otóż sugeruje on, że klasa Singletona poza metodami biznesowymi jest też odpowiedzialna za samą siebie w zakresie zarządzania swoją liczebnością. No właśnie popatrzmy jeszcze raz na cechy jakie powinna spełniać implementacja by móc mówić o Singletonie.
- Można stworzyć co najwyżej jedną instancję, obiekt, danej klasy.
- Instancja jest tworzona przy pierwszym użyciu.
O ile pierwszy punkt jest jasny to drugi jest już moim tworem. Ma on na celu rozróżnienie klas singletonów od klas, których wszystkie metody są statyczne. Zakładam więc, że singleton to taki obiekt, który tworzony jest dopiero gdy jest potrzebny, a nie przy ładowaniu klasy. Dobrze zatem spójrzmy jeszcze raz na punkt pierwszy. Moim zdaniem, co znajduje potwierdzenie na angielskiej wiki, singletonem jest każdy obiekt, który potrafi ograniczyć ilość swoich instancji. Oznacza to, że jeżeli nasz obiekt udostępnia tylko pięć instancji samego siebie to też jest singletonem. Tu widać tą małą niedogodność związaną z nazwą wzorca. Powinna ona raczej brzmieć Kontroler Ilości Instancji (ang. Instance Number Controller). Lepiej by oddawała sens tego wzorca. Jednak nie ważne. Ważniejszym pytaniem jest to czy klasa powinna sama dbać o ilość swoich instancji. Jeżeli patrzymy na diagram UML to pierwszą odpowiedzią jest tak. Jak pisał J. Kerievski
Diagram UML wzorca jest tylko jednym z rozwiązań
Tak samo diagram singletona jest jednym z rozwiązań, które są przed nami stawiane. Rzeczą wręcz głupią jest patrzenie na niego i traktowanie jak przenajświętszej ikony w ikonostasie diagramów wzorców projektowych.
Problem ze wzorcem singleton polega przede wszystkim na złym jego wykorzystaniu. Zarówno na poziomie architektury systemu, projektu jak i implementacji. W literaturze omawia się zazwyczaj błędy popełnione w dwóch pierwszych przypadkach. Wielu ekspertów i specjalistów ds. wdrożeń przytacza setki przykładów nieprawidłowego wdrażania singletonów w systemach. Nie spotkałem się jednak z przykładem złej implementacji. Wynika to z trudności wykazania, że dana implementacja jest nieprawidłowa. Znacznie łatwiej jest pokazać, że popełniono błąd w projekcie niż w implementacji. Pytanie dlaczego? Moim zdaniem wynika to z uświęcenia diagramu UML wzorca. Bardzo łatwo poddać się w tym przypadku myśleniu „skoro to jest tak proste to nie należy tego już ulepszać”. Źródła tego sposobu postrzegania znajdują się w generalnej koncepcji wzorców, która mówi, że wprowadzone wzorce powinny upraszczać kod, domyślnie kasować pudełka z diagramu obiektów. Skoro zatem kod (abstrakcja reprezentowana przez UML) jest bardzo prosty to nie można już bardziej go uprościć. Wzorzec singleton jest doskonały, ponieważ nie da się już go bardziej uprościć.
Jednocześnie jeżeli poczytamy o sposobach zamiany wzorca singleton na inny to często przytaczany jest wzorzec fabryki abstrakcyjnej, która dostarcza zawsze tego samego obiektu. Poniższy kod reprezentuje właśnie takie rozwiązanie:
Listing 1. Fabryka czy Singleton oto jest pytanie.
package eu.runelord.blog.singleton;
public class Main {
public static void main(String[] args) {
ISingleton singleton = SingletonFactory.getSingleton();
singleton.helloWorld();
}
}
class SingletonFactory {
private static ISingleton singleton;
public static ISingleton getSingleton() {
if (singleton == null)
synchronized (SingletonFactory.class) {
if (singleton == null)
singleton = new ISingleton() {
@Override
public void helloWorld() {
System.out.println("Hello World");
}
};
}
return singleton;
}
}
interface ISingleton {
public void helloWorld();
}
Piękny przykład fabryki, która dostarcza nam jednej i tylko jednej instancji klasy singletonu. No właśnie, czy na pewno jest to tylko fabryka…
Wróćmy na chwilę do naszej listy wymagań. Czy nasze nowe rozwiązanie spełnia punkt pierwszy? Na pewno ponieważ udostępnia jedną instancję. Czy spełnia punkt drugi? Też spełnia ponieważ obiekt jest tworzony dopiero przy pierwszym użyciu. Zatem…
Fabryka czy Singleton oto jest pytanie?
No właśnie czy przedstawione powyżej rozwiązanie jest fabryką czy singletonem? Odpowiedź brzmi jest zarówno fabryką jak i singletonem. W tym przypadku nasza fabryka jest jednocześnie specyficzną implementacją, która spełnia założenia singletonu. Z drugiej strony rozwiązanie to jest singletonem, który realizowany jest w oparciu o fabrykę.
Problem z tym konkretnym rozwiązaniem jest taki, że odrzuciłem standardowy diagram UML na korzyść innej drogi. Sam diagram skomplikował się lecz rozwiązanie nadal spełnia wszystkie warunki. Dodatkowo jest znacznie prostsze.
Pytanie brzmi czy moje podejście do problemu jest lepsze? Otóż to zależy. Przedstawiłem jedno ze standardowych rozwiązań, które pretenduje do miana zamiennika dla singletonu. Tak naprawdę 99% złych wdrożeń tego wzorca odbywa się na etapie implementacji, a nie projektowania. Prezentując takie rozwiązanie pomijane jest inne znacznie prostsze, które nawet wygląda jak „prawdziwy” jedno klasowy singleton. Oto i ono:
Listing 2. Rasowy singleton w nowej odsłonie
package eu.runelord.blog.singleton;
public class Main {
public static void main(String[] args) {
ISingleton singleton = Singleton.getSingleton();
singleton.helloWorld();
}
}
class Singleton implements ISingleton {
private static Singleton singleton;
public static Singleton getSingleton() {
if (singleton == null)
synchronized (SingletonFactory.class) {
if (singleton == null)
singleton = new Singleton();
}
return singleton;
}
private Singleton(){}
@Override
public void helloWorld() {
System.out.println("Hello World");
}
}
Czy różni się ono od poprzedniego? Nie! Jedyną różnicą jest konsolidacja kodu fabryki i obiektu biznesowego w ramach jednej klasy. Jednocześnie jeżeli przedstawilibyśmy komuś ten kod i zapytali o wzorce to odpowiedź będzie brzmiała „To jest Singleton.”.
O co cały ten szum…
Mówiłem już, że 99% złych singletonów to dzieci błędnej implementacji. Nie projektu, a właśnie błędnej implementacji. Zanim przejdę do omówienia tego problemu popatrzmy na listę wad singletona:
- Singleton jest trudny do testowania
- Singletonu nie można rozszerzyć
- Singleton łamie zasadę SRP
- Singleton łamie zasadę OCP
- Singleton jest trudny w testowaniu
Programiści to osoby o bardzo zróżnicowanym charakterach i osobowościach. Jednak jeżeli miałbym wybrać jedną cechę, która łączy wszystkich programistów to wybrałbym silne poczucie indywidualizmu. Na spotkaniu WDPSG określiłem siebie jako programistę – artystę. Jest tak ponieważ tak jak każdy artysta poza warsztatem rzemieślniczym posiadam silne poczucie własnej odrębności nawet w grupie osób o podobnych zainteresowaniach.
Po tej malej dygresji popatrzmy jeszcze raz na naszą listę. Czy nie macie wrażenia, że większość problemów związanych z singletonami wynika tak naprawdę z błędów w implementacji wzorca? Przyjrzyjmy się nowej wersji „klasycznego” diagramu UML prezentującego singleton:
Takie nieortodoksyjne podejście i wprowadzenie interfejsu pozwala nam na pozbycie się trzech problemów. Nasz singleton jest już rozszerzalny. Rozszerzyć możemy go poprzez implementację interfejsu. Jeżeli nie chcemy pisać dużej ilości kodu można prowadzić dodatkowy obiekt adaptera, który będzie delegował zachowania do oryginalnego obiektu. My będziemy musieli tylko zaimplementować zmiany. Tym samym nasz singleton nie łamie już zasady OCP. W ogólności nie łamana jest też zasada SRP ponieważ wzorzec wymusza jakąś kontrolę ilości instancji. Sposób jej realizacji może być różny. Można zastosować fabrykę i po problemie. Pozostaje nam jeden problem związany z testowaniem. Tu można wyróżnić dwa rodzaje singletonów.
Pierwszy z nich to singletony bezstanowe. Ich zadanie polega na dostarczaniu metod i nie robieniu tego w sposób statyczny. Te singletony testuje się bardzo, bardzo prosto. Ponieważ na etapie inicjacji testu nie musimy ustawiać żadnych właściwości obiektu nie ma więc potrzeby jakiegoś specjalnego jego traktowania. Powiem nawet więcej, singleton będzie nam ułatwiał życie gdyż inicjację testowanego obiektu trzeba wykonać tylko raz. W tym przypadku problem jest sztuczny, czyli nie istnieje.
Druga grupa to singletony stanowe. Przechowują one pewne dane i tym samym mają stan. Pół biedy jeżeli wszystkie pola są dostępne i można wywołać dla nich metody ustawiające (settery). W takim wypadku wystarczy na etapie czyszczenia po teście zerować pola i ustawiać je na nowo przed każdym testem. Znacznie gorzej ma się sprawa pól do których jedyną metodą dostępu jest refleksja.
Uwaga, będę omawiał teraz zagadnienia związane z Javą i raczej nie znam metod rozwiązania tego problemu w innych językach.
W takim przypadku musimy poszukać jakiegoś obejścia. Kiedyś przedstawiłem już jedno z rozwiązań. Jest ono dość kontrowersyjne, ale się sprawdza. Szczególnie, że można je dość łatwo zaimplementować jako rozszerzenie JUnita.
Gdzie jest w tym myk? Otóż cała magia tego rozwiązania polega na wyrzuceniu wszystkich metod biznesowych do interfejsu. W ten oto prosty sposób realizując założenia wzorca singleton nadal mamy swobodę dostarczania innego rozwiązania. Programiści zazwyczaj nie ekstrahują interfejsu biznesowego i starają się zwalić winę na projektantów. Projektanci też nie są bez winy ponieważ dostarczają programistom diagram UML, w którym bezmyślnie kopiują nie do końca dobre rozwiązania.
Podsumowanie
Wzorzec singleton jest może prosty, ale trzeba umieć go stosować. Wiedza ta potrzebna jest zarówno architektom, projektantom jak i zwykłym programistom ponieważ błędy rodzą się na wszystkich etapach.
Wzorzec singleton jest specyficznym rodzajem fabryki i można to wykorzystać.
W końcu, diagram UML wzorca singleton nie jest świętością i nie należy stosować go w takiej postaci wszędzie. Można zaimplementować singleton na inne sposoby i choć diagram staje się trochę bardziej skomplikowany to nadal jest to singleton.