Gebruik geen singletons!

Vroeger was ik altijd erg happig om singletons te gebuiken. Immers, het leek mij logisch om iets te hebben zoals Database.GetInstance().DoQuery(query); want ik heb namelijk maar één database en mijn applicatie heeft maar één verbinding met die database. Toch is het gebruik van singletons problematisch. Het is namelijk een verkapte vorm van een global state, iets wat je niet wilt hebben. Je code wordt moeilijker testbaar, en wordt moeilijker om te onderhouden.

Als je een singleton moet hebben om overal ergens bij te kunnen, dan duidt dat op een code smell. Waarschijnlijk is je code niet goed losgekoppeld en moet je gaan nadenken over een beter ontwerp. Waarom heb je een sterke koppeling? Omdat je singleton je afhankelijkheden verstopt.

Bij het gebruik van singletons gaat je API onwaarheden vertellen. Singletons zorgen ervoor dat je interfacedefinities patheologische leugenaars worden. Een voorbeeld:

In principe zou de bovenstaande code moeten werken. Echter, bij het uitvoeren van de test krijg je een NullReferenceException. Waarom? Omdat er een singleton binnen de methode Buy aangeroepen wordt dat nog niet geïnitieerd is:

De OrderService is dus een verborgen afhankelijkheid dat niet gedocumenteerd is in de interface. Vanuit de interface wordt het niet duidelijk dat er samengewerkt wordt met een OrderService. Ik kan nu ook niet testen zonder een OrderService te initialiseren. Ik moet dus in mijn test OrderService.Setup() aanroepen om mijn test werkend te krijgen. Misschien wil ik mijn OrderService ook wel mocken om te testen, zodat ik niet steeds een email krijg met het bericht dat ik een product gekocht heb.

Daarnaast kan iedereen bij de OrderService, dus als ik meerdere tests naast elkaar draai, kan het zijn dat ze invloed op elkaar hebben. Dit noemen we ook wel “spooky action at a distance”. Hoe kunnen we dit verbeteren? Bijvoorbeeld met dependency injection. Dan denken mensen misschien dat je dan meteen een duur Inversion-of-Control framework nodig hebt, maar niets is minder waar. Ik kan mijn afhankelijkheid simpelweg in de constructor definiëren:

De volgorde van statements is nu oninteressant, een klant kan altijd dingen kopen, omdat hij niet kan bestaan zonder OrderService. Nu heb ik de afhankelijkheid expliciet gemaakt. Ook kan ik nu een mock OrderService injecteren om te testen.

De code wordt op deze manier minder afhankelijk van de rest van het systeem. Voor nieuwe ontwikkelaars wordt het op basis van de interfacedefinitie snel duidelijk dat ze weinig kunnen beginnen zonder een OrderService. Anderzijds was het een proces van vallen en opstaan om te bepalen wat precies de functionaliteit van een klasse is en waarom zaken zich anders gedragen dan verwacht.

Het kan echter zo zijn dat je toch bepaalde zaken applicatiebreed wilt delen en waarschijnlijk dan ook maar één instantie van is. Een object zou in principe niet zijn eigen lifecycle mogen beheren middels static velden. Bij web applicaties die draaien op een webserver zoals IIS wordt dit problematisch. IIS ruimt namelijk zo nu en dan zijn worker process op, waardoor je instanties verdwijnen. Eveneens heeft jouw singleton te maken met meerdere HTTP requests en consequente threads die die requests afhandelen. Kan jouw singleton meerdere threads aan? Threads gaan dus door elkaar aan de singleton zitten waardoor de state verandert, wederom spooky action at a distance.

De lifecycle van applicatiebrede objecten wil je dus idealiter laten afhandelen door het framework. In ASP.NET heb je meerdere hooks in het framework zoals bijvoorbeeld een IHttpModule. IHttpModule wordt vaak gebruikt om bijvoorbeeld NHibernate sessies te beheren. Je legt de verantwoordelijkheid van het instanciëren van je object neer bij de verantwoordelijke, namelijk bij het begin van een http request. In een desktop omgeving zou je je applicatiebrede object als member in je mainform kunnen stoppen.

In conclusie, singletons zijn eigenlijk een schending van de regels van Object Georiënteerd Programmeren. OOP stelt dat je alleen met je vrienden praat, maar middels singletons kun je iedereen met iedereen laten praten waardoor je een warboel aan verborgen afhankelijkheden krijgt.