Problem
Just imagine we need to build a simple web page to show live subscribers of a Youtube channel. Main object here is "subscriber" so we need a tool (counter) to update number of subscribers in real-time.
There are 2 actors: subscribers (Youtube) and visitors (our web page)
When a user subscribes to a channel, we update the counter by +1. And we do the same for other incoming subscribers, but there are 2 things we have to keep in mind:
- Handle concurrent subscription requests from users properly. Or else there will be a chance that we misscount a portion of subscribers
- Use the same counter to update/get live subscriptions for all actors (subscribers & visitors)
Singleton Overview
Singleton is one of Creational design patterns. It has some characteristics:
- One class/type can only create/have one single instance
- Provide a global access to that single instance
- We have many Singleton implementations to be used in single/multi-thread environment based on specific use case
What does Singleton resolve?
Back to our above analogy with subscription counter, we can use Singleton to instatiate the counter and provide its global access.
- All subscribers and visitors will access the same counter so data will be consistent
- Since this is a multi-thread use case (e.g. multiple subscribers can subscribe simultaneously), we need to make sure the counter instantiation thread-safe
Applicability
Use Singleton when you need to manage one & only instance of a resource (e.g. configuration, logger, etc.)
Approaches
There are 2 ways to implement Singleton pattern:
Pseudocode (golang)
First, define counter
struct
type counter struct {
views int
}
Eager initialization
var instance *counter = &counter{}
func getCounter() *counter {
return instance
}
Lazy initialization: use double-checked locking mechanism to avoid unnecessary lock when the instance has already been initialized
var instance *counter
func getCounter() *counter {
// instance has not been initialized
if instance == nil {
// lock to avoid multiple instances are created simultaneously by multi threads
lock.Lock()
defer lock.Unlock()
// need to recheck because first check can be passed by multiple gorountines
if instance == nil {
instance = &counter{}
}
}
// when instance has already been initialized -> return
return instance
}
Benefits & Drawbacks
Benefits
You can assure that the class/type has only one instance if the implementation is done properly.
Drawbacks
- Tight coupled codebase
- Hard to debug
- Write unit tests will be tricky
- Limited use cases