← Back to Blog

[iOS] Singletons and Global Instances impact on system design and testability

2020-11-1·2 min read
iOSTestingNetworkingAI / MLDesign Patterns

Image credit: Photo by Simon Rae on Unsplash

Singletons

Singletons is a way to make sure a class only have one instance to provide a single access point to it.

It is a class that is being instantiated only one time in the whole lifecycle of the app and it still allows the creation of a new instance of the class. The constraint is up to the developer's choice or discipline to instantiate it only once.

  • Example: URLSession.shared and UserDefaults.standard. We can access it's immutable reference, and also it allows clients to create other instances through its initializers.

Singleton vs. Global Mutable Shared State

Mutable Global State usually accessed by static sharedInstance of a class and allows the access of mutation of that reference (static var instead of static let). Example: current Date, Networking, and Database components.

Also, it can be risky because its state can be changed from any process/thread in the app. But it offers ease of use when it comes to accessing objects throughout the system and easy configuration of the system environment.

When to use Singleton pattern?

When we need precisely one instance of a class, and it must be accessible to clients from well-known access point.

If we need to extend the functionality of that class, then the singleton pattern allows us to subclass or create extension on the class type.

Singleton objects should be rare in most system and need to have one-to-one relationship with the system.

Dependency Inversion

It's a common practice for 3rd-party frameworks to provide singleton objects. Although this approach provides convenience, if the singleton is used throughout the app, it can create a tight coupling between the client and external framework.

To solve it, we can use dependency inversion, for example protocol/closure, we can keep the modules of our app agnostic about the implementation details of another external system. This way, we can protect the codebase from breaking changes when updating the external framework, make the code more testable, and also easier to replace the framework in the future.

So, we can free the modules from tight coupling on shared instances by inverting the dependency with an abstract interface such as protocol or closure and injecting the instance instead of accessing it directly.