Chris Dzombak

Singletons in Cocoa applications

The Singleton pattern is a powerful tool in every programmer’s toolbox. But singletons today are widely overused in most codebases.

The consequences of misusing such a powerful tool are too far-reaching and long-lived to take lightly. And overuse or misuse of this tool leads to tightly-bound spiderwebs of dependencies in untestable code that’s difficult to refactor and impossible to reuse.

(I’m writing here specifically about common practices in Cocoa applications, but the same arguments and rules generally apply to most object-oriented environments.)

The singleton pattern exists to solve exactly one problem: to ensure that there is one, and only ever one, instance of a given class. This is explicitly achieved by the best-practice singleton implementation in Objective-C, which uses GCD to provide exactly that guarantee:

+ (instancetype)sharedInstance {
    static id sharedInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

There are few objects for which this requirement makes really good sense, and this should be your guiding principle when deciding whether a singleton is appropriate: am I using a singleton to guarantee that there is only ever one instance of this class? And if so, does there really need to be exactly one instance? Why?

Note that convenient access to a globally shared instance is practically an accidental side effect of this implementation. Don’t use singletons just to create and allow access to global state.

To ensure that singletons in my codebases can’t do any damage, I have some rules to consider when deciding whether, and how, to use the singleton pattern. Violating these rules will pretty quickly lead to:

Rules for singleton usage

Use a singleton to provide a single instance of a given class, not for convenient access to global state.

Think: global notifications that mustn’t be duplicated.

Is it really critical that there’s only one instance during the app lifecycle? And is it critical to enforce this through the singleton pattern, instead of creating one instance and passing a pointer to interested clients?

Are you really trying to provide access to global state? Is it critical that this state be global, not owned by some other object?

Consider whether you can use dependency injection to provide a single instance to clients.

In most cases, this isn’t painful.

Obey the single responsibility principle.

Singletons often lend themselves to SRP violations. These violations are one of the biggest problems I see in most singleton classes. To avoid this, ensure your singleton has a well-documented responsibility before you even begin writing its implementation.

Consider whether you’re using a singleton for behaviors or responsibility that should be split among several classes, which may not be singletons, collaborating.

Are you considering a singleton to have a place to put logic or state with no clear home? Find a different home — or several homes — for that state and logic. This may require breaking up part of your app into other classes with well-defined responsibilities and better interfaces for collaboration.

Corollary: No singleton may ever be named “…Manager”. Classes whose names end in “Manager” inevitably become thousand-line dumping grounds for anything related to whatever they’re managing. (I’ll allow an exception if the “manager” is used as a facade around a poorly-designed third-party API.)

Limit usage of the -sharedInstance method.

Don’t make views (for example, or models) aware of shared instances. Consider instead: does it make more sense to inject the necessary dependency into methods when they are called, or into objects at initialization? In many cases, the answer is yes.

Don’t use other singletons from your singletons.

Using singletons from other singletons is an easy way to grow a web of dependencies. Don’t even #import one singleton from another.

Allow alloc/init for tests.

Expose the class’s underlying designated initializer, perhaps in a category, for testing purposes.

Finally, use more descriptive names than -sharedInstance.

In many cases, -sharedInstance is just not a descriptive name. Try -currentUser, -sharedReachability, -currentUserSession, or similar names instead.

Followup