One design pattern at January — Singleton
Singleton is a design pattern that ensures that a class has only one instance and provides a global point of access to that instance. In Swift, a Singleton class is implemented by creating a static constant instance within the class and using a private initializer to prevent any other instances from being created.
class Singleton {
static let shared = Singleton()
private init() {}
}
We can easily access this shared instance using the following:
let sharedInstance = Singleton.shared
Real use case
A common practical use case would be a Logger. Sometimes, print
statements are not enough; you might want to log to a specific destination, or use different levels for example. If you are not using a proper DI mechanism, in order to avoid consuming unnecessary resources, a singleton would be an easy and quick way to share the same instance.
class Logger {
static let shared = Logger()
private init() {}
func log(_ message: String) {
print(message)
}
}
The global state testing problem
Some developers might advocate against using singletons, but they may not be able to clearly explain why. The problem is that singletons can make your code untestable by introducing global state. When an instance can be accessed by multiple parts of the application, it not only becomes difficult to understand the code, but also to predict its behavior. An alternative to avoid this issue is to use dependency injection.
The threading problem
As stated before, singletons are shared instances that can be accessed by multiple threads. There could be a problem if this same instance is accessed concurrently (by two or more threads at the same time) — that is called race condition. This can lead to, once again, unpredictable behavior and also deadlocks which can be fatal to the application. A solution for this is to use thread-safe techniques such as dispatch queue.
class Singleton {
static let sharedInstance = Singleton()
private let queue = DispatchQueue(
label: "com.example.threadSafeSingleton",
attributes: .concurrent
)
private init() {}
private var _property: Int = 0
var property: Int {
get { return queue.sync { _property }}
set {
queue.async(flags: .barrier) {
self._property = newValue
}
}
}
}
In the example above, the property
variable is only accessed over a concurrent queue.
A barrier block is a special type of block that is executed only when all previous blocks queued to the same concurrent queue have finished executing. This means that when a barrier block is executed, it acts as a barrier to any other blocks that are queued to the same queue. This can be useful in situations where you need to ensure that a specific block of code is executed only after all other blocks have finished executing.
Conclusion
While singletons can be easy to implement and useful for sharing data across an application, it’s important to consider alternative techniques such as dependency injection to avoid tight coupling and improve code testability.