Three singletons types frequently occur throughout iOS SDK and codebases. Let us name them and examine their benefits and risks. They are as follows:
class ExampleOne { static let shared = ExampleOne() private init {} }
class ExampleTwo { static let shared = ExampleTwo() }
class ExampleThree { static var sharedInstance = ExampleThree() }
ExampleOne
is by the book Singleton implementation. In Swift static let
is a lazily loaded constant, which preserves system resources at launch. That class has only one instance. It provides a single point of access to it. As described in the Design Patterns book (GOF) it makes sure that the class itself keeps track of its sole instance and prohibits creating over one instance.
ExampleTwo
depicts a variation of Singleton a.k.a. singleton with lowercase ‘s’. Take for example URLSession.shared
or UserDefaults.standard
. The code instantiates the class only one time in the whole life-cycle of the app. One difference between the Singleton and singleton is that the interface does allow creating a new instance of the class.
ExampleThree
shows a mutable global shared state. Usually accessed by a variable named static var sharedInstance
, it allows the access and mutation of that reference. This exemplifies Ambient Context anti-pattern. Apple uses it too. For instance URLCache.shared
property is a static, globally accessible property that we can change.
Problems singletons cause
Frequent usage in unfortunate scenarios gave rise the singleton to be considered an anti-pattern.
Singletons can set up unnecessary restrictions in situations where a sole instance of a class is not required. If not carefully considered, they make dependencies implicit, highly couple modules, prevent testability, introduce global state and force thread-safety. Ambient Contexts are even worse. They allow changing the dependency causing problems such as temporal coupling, global state or runtime inconsistency.
When singletons suit
When one requires precisely one instance of a class, and it must be accessible to clients from a well-known access point. A case in point is a class that logs messages to the console. Its public API is simple and we don’t need over one instance or even re-create or mutate its reference in memory.
Bad Singleton candidates are all objects not mandatory to have only one instance in a system.
Access the concrete singleton instance directly should be avoided. To escape the tight coupling one can use dependency inversion. By hiding the third-party dependency behind an interface one can keep the app modules agnostic about the implementation details.
Conclusion
It is widely known that singletons may damage system design and testability, but when used as intended they are a useful tool.