Jak na każde ogólne pytanie dotyczące IT, należy odpowiedzieć „To zależy.”. W jednym przypadku Singleton może być antywzorcem, który ześle na projekt egipskie plagi. W drugim będzie kompromisowym rozwiązaniem problemu. Każdy wzorzec zastosowany tam, gdzie nie powinien być zastosowany, jest antywzorcem. Problem z singletonem jest taki, że jest niesamowicie prosty i bardzo łatwo można skrócić sobie nim drogę do rozwiązania problemu. Niestety droga na skróty nie zawsze jest opłacalna.
Co programiści sądzą o Singletonie?
Postanowiłem sprawdzić w publikacjach, książkach i komentarzach co myślą o singletonie doświadczeni programiści.
Martin Fowler
„This also brings out some problems that singletons can store up. In particular if you use a singleton (or other form of Registry) make sure that they can be easily substituted and that their initialization can also be easily replaced.”
https://martinfowler.com/bliki/StaticSubstitution.html
„I was surprised that Singleton got away with a split decision, considering how unpopular it’s become amongst my friends. Most of the others were voted off because people felt they were sufficiently uncommon and that other patterns would probably take their place, sadly we didn’t have time to consider new members.”
https://martinfowler.com/bliki/OOPSLA2004.html
Erich Gamma
W wywiadzie udzielonemu dla Larrego O’Briena:
Larry: How would you refactor „Design Patterns”?
Erich: We did this exercise in 2005. Here are some notes from our session. We have found that the object-oriented design principles and most of the patterns haven’t changed since then. We wanted to change the categorization, add some new members and also drop some of the patterns. Most of the discussion was about changing the categorization and in particular which patterns to drop.
When discussing which patterns to drop, we found that we still love them all. (Not really—I’m in favor of dropping Singleton. Its use is almost always a design smell.)
https://www.informit.com/articles/article.aspx?p=1404056
Ralph E. Johnson
„Singleton is often used as a justification of global state.”
https://www.youtube.com/watch?v=ALxQdnOdYXQ&feature=emb_title
John Vlissides
„I used to think Singleton was one of the more trivial of our patterns, hardly worthy to hobnob with the likes of Composite, Visitor, etc.; and maybe that explains its silence on some issues. Boy, was I wrong!”
https://sourcemaking.com/design_patterns/to_kill_a_singleton
Michael Feathers
Kent Beck

Robert C. Martin

https://blog.cleancoder.com/uncle-bob/2015/07/01/TheLittleSingleton.html
Ward Cunningham

Joshua Kerievsky


Yegor Bugayenko
„Forget about singletons; never use them. Turn them into dependencies and pass them from object to object through the operator
https://www.yegor256.com/2016/06/27/singletons-must-die.htmlnew
.”
Jak widzisz, wzorzec ten nie cieszy się popularnością wśród doświadczonych programistów. Sam Erich Gamma, który jest współautorem książki „Design Patterns: Elements of Reusable Object-Oriented Software” uważa, że nie ma już tam miejsca dla singletona.
Wzorzec ten został stworzony w dobrym i konkretnym celu. Żaden z jego autorów na pewno nie myślał, że ludzie zaczną używać go tam, gdzie nie powinni.
Przegląd problemów z Singletonem
Problem z testowaniem
Problem z testowaniem Singletona bardzo dobrze opisa Robert C. Martin na swoim blogu we wpisie „The Little Singleton” https://blog.cleancoder.com/uncle-bob/2015/07/01/TheLittleSingleton.html. Weźmy przykład z tego artykułu:
public class X { private static X instance = null; private X() {} public static X instance() { if (instance == null) instance = new X(); return instance; } // more methods... }
Można dojść do wniosku, że trzeba umożliwić zmianę zachowania funkcji instance()
w taki sposób aby móc przetestować klasę. Aby to zrobić, powinno się poluzować hermetyzację zmiennej instance
:
public class X { public static X instance = null; private X() {} public static X instance() { if (instance == null) instance = new X(); return instance; } // more methods... }
Zmienna instance stała się publiczna po to, aby móc kontrolować jej wartość podczas testów. Dochodzi się do prostego wniosku, że singleton może przyjąć postać zmiennej globalnej:
public class X { public static X instance = null; }
Czy coś jest złego w użyciu zmiennej globalnej zamiast singletona? Jeśli nie jest to zewnętrzna biblioteka to nie. Użycie Singletona w przypadku projektów, które są wewnętrznymi projektami, jest przesadą – można stwierdzić, że jest to „over engineering”. Mając dostęp do kodu źródłowego programista i tak może zmienić implementację singletona. Dlatego użycie singletona w celu zabezpieczenia się przed wieloma instancjami w momencie gdy wszyscy użytkownicy kodu mają dostęp do implementacji singletona jest całkowicie pozbawione sensu.
Jak wykorzystać mechanizmy językowe Javy i Kotlina aby poluzować hermetyzację w taki sposób aby Singleton był bezpieczny w przypadku gdy projekt jest zewnętrzną biblioteką?
W przypadku Javy można wykorzystać modyfikatory dostępu protected lub domyślny w Kotlinie internal.
Problem z abstrakcją.
Jednym z problemów wzorca Singleton jest brak możliwości wykorzystania abstrakcji, ponieważ wykorzystywana jest statyczna metoda, która należy do klasy, a nie do obiektu danej klasy. I tak można dojść do problemu w, którym chce się dostarczyć różne implementacje Singletona np. w zależności od platformy.
Można jednak zrobić małą modyfikację, w której Singleton dostarcza obiekt innej klasy niż on sam np:
interface X { public void someMethod() } public class Singleton { public static X instance = null; public static X instance() { if (instance == null) instance = create(); return instance; } private static X create() { if(System.Platform == Windows) { return new WindowsX() }else { return new DefaultX() } } }
W przypadku gdy projekt byłby biblioteką do użytku zewnętrznego a konstruktory klas WindowsX
oraz zDefaultX
byłby niedostępne spoza biblioteki, to takie rozwiązanie rozwiązuje problem.
Jednakże w momencie, gdy Singletona używamy wewnętrznie projekcie, to nadal nie ma pewności, że nie zostanie nigdzie indziej stworzona instancja klasy WindowsX()
lub DefaultX()
. Oczywiście można byłoby stworzyć Singletony także dla WindowsX
i DefaultX
w formie DefaultX.getInstance()
oraz WindowsX.getInstance()
, ale nadal byłoby ryzyko wykorzystania jednocześnie klasy WindowsX
oraz DefaultX
. Dlatego taki Singleton zastosowany w wewnętrznym projekcie nie ma sensu – nie daje żadnej wartości dodanej.
Problem z globalnym dostępem.
Kiedyś korzystałem z biblioteki, w której punktem dostępowym był Singleton. Do pobrania instancji służyła metoda typu getInstance
. Oczywiście jako niedoświadczony programista, używałem Singletona globalnie, wszędzie gdzie tylko się da, bez testowania. Próba napisania jakiegokolwiek testu kończyła się fiaskiem – przez globalne dostępy do zmiennych. Przy którejś aktualizacji twórcy postanowili nie używać Singletona i stworzyli fabrykę do tworzenia instancji. Jak możecie się domyślić, zmiana wywołania typu Singleton.getInstance().doSomething()
na wstrzykiwanie zależności w kilkuset miejscach była niesamowicie kosztowna.
Zabezpieczenie się przed tego typu sytuacjami jest całkiem proste, zamiast dostępu poprzez statyczną metodę getInstance()
wewnątrz klas:
class A{ init{ Singleton.getInstance().doSomething() } }
użyj wstrzykiwania zależności przez konstruktor:
class A(singleton : Singleton){ init{ singleton.doSomething() } }
Jeśli używany Singleton jest z biblioteki zewnętrznej i instancja będzie używana w wielu miejscach to warto schować go za interfejsem – Dependency Inversjon.
To w końcu jest dobry, czy jest zły?
Nie ma jednoznacznej odpowiedzi, na przykład sierp jest narzędziem przydatnym do ścinania małej ilości zboża, ale do krojenia chleba lub obcinania paznokci już nie. Singleton jest właśnie takim prymitywnym wzorcem, który prawidłowo zastosowany spełnia swoje zadanie ale użycie go w nieodpowiedni sposób jest bardzo niebezpieczne. Jest o wiele więcej prostszych rozwiązań (globalna zmienna) lub lepszych wzorców np. kontrola przy dependency injection.
Wnioski
Singleton jest bardzo prostym wzorcem, ale może sprawić wiele problemów podczas testowania, utrzymywania lub refaktorowania kodu. Dlatego powinno się go unikać, jeśli jest to tylko możliwe.
W projektach, które nie są współdzielonymi lub zewnętrznymi bibliotekami, zabezpieczanie się przed tworzeniem wielu instancji nie ma większego sensu. Każdy z członków zespołu i tak może zmienić kod źródłowy – dlatego singleton nie jest żadnym zabezpieczeniem.
Jeśli jednak Singleton znajduje się wewnątrz projektu, który jest zewnętrzną biblioteką, można nieco poluzować hermetyzację i umożliwić ręczne ustawianie zmiennej do testów. Dla korzystających z biblioteki taka opcja nie będzie dostępna, więc wzorzec będzie spełniał swoją funkcję.
Sam Singleton jako zabezpieczenie przed stworzeniem kilku instancji nie jest problemem. Problem stanowi wykorzystanie globalnego dostępu do Singletona w całym projekcie. Jeśli korzystasz z Singletona to nie traktuj go jako globalny punkt dostępu do instancji. Używaj wstrzykiwania zależności w każdej klasie, która korzysta z Singletona tak aby, móc łatwo przetestować taką klasę lub wprowadzić zmiany gdy twórca biblioteki zrezygnuje z globalnego dostępu do instancji i zdecyduje się na lepsze rozwiązanie.
Singleton jak każdy inny wzorzec musi rozwiązywać problem a nie go tworzyć. Zdecydowanie się na Singletona z powodu lenistwa, ucieczki od innych rozwiązań, bardziej elastycznych, dostosowanych do problemu może spowodować w przyszłości spore problemy. Nigdy nie idź na skróty, nie rób niczego szybko i bezmyślnie, przemyśl rozwiązanie i konsekwencje jakie ze sobą niesie dane rozwiązanie.