Inspired by a Twitter discussion with Liam Devine about my previous blog post, I've decided to take a bit of time to talk about my opinion on Singletons. In my previous post, I used a lot of them, though generally, I'm not a huge fan. In fact, I've been known to dedicate non-trivial amounts of time in talks to the subject of why singletons suck. Singletons are just globals under another name, and they have almost all of the same drawbacks as globals, and don't provide too many benefits. Each singleton is a potential bottleneck, a potential threading nightmare, and almost always creates giant side effects throughout the code base every time they're used. In addition, if you're doing unit testing, use of singletons / globals requires significant set-up overhead, which can make tests more complicated.
So what gives? Why am I using them here?
Generally, there's nothing intrinsically wrong with a Singletons or globals. It's their overuse that becomes a problem. As mentioned above, each singleton needs to be managed carefully because it can become a potential bottleneck across your entire code. However, when you're working with something that does need to be global, a Singleton is sometimes a better option, since you can insist certain conditions be met before an instance can be returned.
A good example is a Fixed String Table. If you're unfamiliar with this, here's the idea: every string in your code is translated to a hashed number. In some cases, this translation is done manually, but for the sake of code readability and simplicity, I do the translation in code any time a FixedString object is created. In order to make sure that same string always returns the same hash, and to make sure we can translate the hash to a string, we put the string into a table. This table could be a static member of FixedString, but if you want to, say, tell the table where to allocate its memory from, or how much memory it's allowed to use, you need to initialize said table. Again, you could use a static method for this, but this is hiding what's actually happening. Furthermore, if your table class is any amount of complex, or has functions for accessing usage data, you're essentially going to be pushing that all into static methods of FixedString. It's a pain, when you are, in essence, creating a global interface anyway, since FixedString itself will probably be in global scope. At least by moving things into a singleton you potentially have the option of creating multiple tables in the future, should you so choose.
But what about all those Factory singletons from my initial post?
First, a point of clarification: in the previous post, I made it look like every factory was a singleton. I did this for simplicity, and I apologize. It actually isn't true. My factories aren't singletons. In reality, the code reads like this:
Application::Instance()->GetFactory()->RegisterComponent(…);
Application is one of the other few singletons I have in my engine. Now, I'll admit that this isn't much better than having the factories themselves be singletons, since they're retrievable from a singleton anyway. If you look at the code, it looks like my component code is now coupled to both the factory AND the application.
But in reality that's not true. My factories aren't coded or tested as singletons. If I wanted to create another factory I could, and all my unit tests will still work. And the components themselves aren't coupled to the factories or the application at all. The only thing that couples factories, the components, and the application is the auto-registration system, which serves as a bridge between all three systems. Each system is still usable stand alone.
This just leaves whether or not the Application as a singleton / manager is a good idea. The answer is, no. No it isn't. But since it is one of few, it sticks out like a sore thumb when you type it, and in code review. Every time you see or type Application::Instance() should be like warning lights going off in your brain that you're potentially doing something dangerous. BUT, since I'm mostly doing this during initialization, and at no other point, I'm okay with it.