Singletons are compulsive liars
Well, you’ve just joined a new project, which already has a mature and very extensive base code. Your new boss asks you to implement a new feature and, as a good developer, you start by writing a test. But since you’re new to the project, you do a lot of exploratory tests like "what happens if I run this method?". You start by writing the following code:
public function testCreditCardCharge() {
$cc = new CreditCard( '1234 5678 9012 3456', 5, 2013 );
$cc -> charge( 100 );
}
This code:
- It only works when you turn it as part of the whole system, never in isolation
- When we try to run in isolation, an exception is thrown
- When you receive your card bill, you realize that you had to pay $100 for each test run (TENSO!)
Now I want to focus on the last item. How did a simple test cause a real charge on my credit card? Charging a credit card is not easy. The test needs to talk to a web-service credit card (third party), need to know the URL to the web-service, need to authenticate, pass credentials and identify in which business I am buying...
None of this information was present in the test. Even worse, as I don’t even know where this information is, how would I "fool" the external dependencies (through a Mock Object) so that I don’t lose $100 every time the test runs? As you are new to the project, how could you know that what the test performed would make you $ 100,00 poorer? That sounds like a ghost story.
But why do I see an exception occurring when running the program in isolation, but everything works when I test the class in conjunction with the system? And how do I fix that? Tired of searching for thousands of lines of code, you decide to go ask the developers who have been in the project the longest. After much digging, you learn that you need to initialize the Creditcardprocessor.
public function testCreditCardCharge() {
CreditCardProcessor::init();
$cc = new CreditCard( '1234 5678 9012 3456', 5, 2013 );
$cc -> charge( 100 );
}
You run the test again, still unsuccessful and you still come across another type of exception. Again, you will consult your colleagues. Someone tells you that Creditcardprocessor needs a Offlinequeue to work.
public function testCreditCardCharge() {
OfflineQueue::init();
CreditCardProcessor::init();
$cc = new CreditCard( '1234 5678 9012 3456', 5, 2013 );
$cc -> charge( 100 );
}
Excited, you run the test again. Another different exception. A little more "digging" and you find that you also need to initialize the connection to the database.
public function testCreditCardCharge() {
Database::init();
OfflineQueue::init();
CreditCardProcessor::init();
$cc = new CreditCard( '1234 5678 9012 3456', 5, 2013 );
$cc -> charge( 100 );
}
Finally the test runs in isolation mode, but again, you just lost another $100.00.
The problem is that existing Apis are compulsive liars. Creditcard pretends that you can only instantiate it and call the method Creditcard::Credit card(), but secretly, she collaborates with Creditcardprocessor.
The API of Creditcardprocessor says it can be initialized in isolation, but in fact, it needs Offlinequeue, which, in turn, needs to Database.
For the developers who wrote the code, it’s obvious that Creditcard needs Creditcardprocessor, after all, they wrote the code. But for anyone new to the project, this is a total mystery and makes the learning curve difficult.
But it’s not over yet! Suppose a colleague who joined you in the project decides to take a look at its implementation. From what he can tell, the three initiations and instantiation are independent, that is, they can happen anywhere. During some refactoring or code cleaning, maybe he rearranges the lines of code that way:
public function testCreditCardCharge() {
CreditCardProcessor::init();
OfflineQueue::init();
$cc = new CreditCard( '1234 5678 9012 3456', 5, 2013 );
$cc -> charge( 100 );
Database::init();
}
The code simply stopped working, but my colleague would have no way of knowing, or would have?
In this simple example, until it is not difficult to see this, but in a real project, startup can usually occur for several classes and you must initialize hundreds of objects. The exact initialization order will become a mystery.
How do we fix this? With Apis that declare dependency!
public function testCreditCardCharge() {
$db = new Database;
$queue = new OfflineQueue( $db );
$processor = new CreditCardProcessor( $queue );
$cc = new CreditCard( "1234 5678 9012 3456", 5, 2008 );
$cc -> charge( $processor, 100 );
}
Since the method of Creditcard declares that he needs a Creditcardprocessor I don’t need to ask anyone about it. The code just won’t run without it.
I have a very clear tip that I need to instantiate Creditcardprocessor. When I try to instantiate Creditcardprocessor I come across need to provide a Offlinequeue.
In continuity, when I try to instantiate the Offlinequeue, I’m gonna need a Database to work.
The instantiation order is clear! Not only is it clear but it is impossible to reverse the odem of the declarations but the code does not execute.
And the best of the benefits, of course, is that every time you run the test you won’t have $100 charged on your card.
Singletons are nothing more than global states. Global state allows its objects to secretly keep things undeclared in their Apis and, as a result, Singletons make their Apis pathological liars.
Think differently. You can live in a society where everyone (all classes) declares who their friends are (collaborators). If I know that José knows Maria but that neither Maria nor José know João, then it is safe for me to assume that if I say something to José he can comment with Maria, but under no circumstances will João know anything.
Now imagine that everyone (all classes) declares some of their friends (collaborators), but other friends (collaborators who are Singletons) are kept confidential. Now you wonder how John got to know that information you gave to José.
And here’s the interesting part. If you are the person who originally built the relationships (code), you know the true dependencies, but any others who come after you are perplexed since the friends you stated are not the only friends of the objects, and the information flows through secret paths that are not clear to you. You live in a society of liars.
Original Article: Singleton are Pathological Liars
Partial Translation: Henrique Barcelos
Where did you see it?
– uaiHebert
A simple Google search finds so many fonts that I wouldn’t know where to start: http://stackoverflow.com/questions/1020312/are-singletons-really-that-bad, http://blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx, http://www.codeproject.com/Articles/634723/Singletons-Why-Are-They-Bad, http://molecularmusings.wordpress.com/2011/11/singleton-is-an-anti-pattern/, http://blog.code-cop.org/2012/why-singletons-are-evil.html, http://c2.com/cgi/wiki?SingletonsreEvil, http://accu.org/index.php/journals/337, http://tech.puredanger.com/2007/07/03/pattern-hate-singleton/
– Maniero
Um... I didn’t know about all this hatred. I find a Pattern so good and useful because Spring implements it all the time. Unfortunately there are people who do not know how to use the Pattern, and then comes out affirming this lot. =/
– uaiHebert
So the question is here to try to reverse this as objectively as possible :)
– Maniero
This question is being discussed at http://meta.pt.stackoverflow.com/q/1437/101
– Maniero
I honestly didn’t think the question was wrong to be here, but I was surprised at Singleton’s hatred. [=
– uaiHebert
Hate to Singleton is like hate to a screwdriver. It’s just another tool. Misuse does hateful things, but it’s the user’s fault.
– OnoSendai