Singletons are not an anti-pattern, but they are an abused pattern. They’re popular among beginners because they’re convenient. Down the line they add complexity and cause maddening bugs.
Let’s start with the positive. Every so often, you have an object that should be represented with one and only one instance. Consider
That makes a lot more sense than:
firstInstance == secondInstance always return true? When we update one, does it update the other?
Singletons may be an appropriate metaphor when it represents an omnipresent object, one that will never change out from under you, like the current application, the device’s accelerometer, or your god.
Rarely, you need to share a single service throughout your app.
NSNotificationCenter only works if there’s a single broadcaster across all boundaries, hence the ‘center’ in its name.1
Everywhere else, don’t use singletons.
An Inappropriate Model
Sometimes I see the currently logged-in user represented by
Account.sharedAccount. I don’t blame you. It’s so convenient.
But accounts are not singletons. Users log out of accounts. Many apps represent a Logged-Out Experience through a special type of account. Services grow to support multiple simultaneous accounts. Accounts are volatile, and a singleton is a lie.
If accounts were a true singleton, this wouldn’t be a problem:
What if that photo takes minutes to validate on a slow network, and in the interim the user switches accounts? The wrong account gets the photo profile.
Singletons Share State
‘Singleton’ is a fancy word for ‘global variable.’ Sometimes— rarely— globals are necessary, but you should make every effort to avoid them. State is hard, and shared state is very, very hard.
Custom container view controllers make
viewWillAppear less predictable. Even vanilla navigation has edge cases: if you start a bezel swipe to go back, change your mind, and cancel it, you will get a false
viewWillAppear, and you’ll log events from the wrong view controller.
This is better:
But the real world often requires state. Maybe your analytics team found duplicate taps, so they’ve asked you to only log
.TappedReply once per view controller instance.
In the real world, maybe you want to keep all of your analytics requests in a single queue, so you can throttle them. Maybe a singleton would be acceptable. At least reduce its scope:
Singletons Cross Boundaries
Here’s a fun bug I hit while refactoring:
This code fails during initialization, because the
Timeline object accesses the account singleton while we’re in the middle of initializing it. Imagine this faux pas buried deeper in the object graph.
What if we want to unit test
Timeline? We have to mock the
Account object, and return a mock preferences object. Ugh.
If anyone can get ahold of an object, any object in your app can be tangled with hidden dependencies.
This clarifies ownership.
Account configures its child,
Timeline. It happens to pull that configuration from preferences, but we could assign any value we want in tests.2
It’s trivial to decouple view controllers with the Account singleton. Compare before:
On Best Practices
For very simple apps, you’ll never run into problems with singletons. When you’re first building an app, best practices can seem like overkill. “You aren’t going to need it,” someone says. “You can always refactor!”
In my experience, it’s significantly harder to pay down technical debt than to do things right the first time. When you paint yourself into a corner, and your product manager asks for a change that should be quick, there’s a lot of pressure to slap on just one hack.
This isn’t about designing around imaginary features, like supporting multiple accounts. It’s about having no idea what tomorrow brings. It’s worth a little extra work to retain flexibility.