Image credit: Photo by Simon Rae on Unsplash
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.
URLSession.shared and UserDefaults.standard. We can access it's immutable reference, and also it allows clients to create other instances through its initializers.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 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.
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.