JavaScript Design Pattern - Singleton Pattern
✅ Common Pattern of the Recurring Problem
Sometimes a program has to restrict the number of instances of a class to one, where
- an instantiation is costly (e.g. DB connection)
- or a single object has to coordinate other objects across a system (e.g. config object)
✅ Solution
Singleton pattern restricts the number of class instantiations to one with the following strategies:
- It creates a new instance of the class if one doesn’t exist. If there is a pre-existing instance, it simply returns a reference to that object.
- The single instance can be accessed globally throughout the application from a well-known access point.
- This single instance is unmodifiable
Singleton pattern has the following tradeoffs:
🟢 memory-efficient; There is no need for each and every new instance to take up its own space since only one instance is created and referenced throughout the application,
🟢 controlled initialization
Consider an object instantiated by initialize()
method.
const MyDatabase = (function() {
// (...)
return {
// (...)
initialize: function (uri) {
if (!instance) {
if (isSQL(uri)) instance = new MySQL();
else if (isNoSQL(uri)) instance = new MyFirebase()
}
return instance;
}
// (...)
}
})();
Here, initialize()
serves as a factory method and provides global access to MyDatabase
instance. By creating an instance of the database with MyDatabase.initialize('MY_DB_URI')
instead of directly by new MyDatabase('MY_DB_URI')
, one can add extra logic to instantiation.
- You can defer the instantiation of expensive classes via deferred/lazy instantiation.
- The sole class
MyDatabase
can be extended by subclasses. In the above code,MySQL
andFirebase
should implement the same interface asMyDatabase
and are thus subclasses ofMyDatabase
. By accessingMyDatabase
viainitialize()
, client code can use extended subclasses without modifying each access point, for example fromMyDatabase
toMyFirebase
.
❌ tight coupling; However, one should be wary of the singleton pattern, since it could be an indication that modules are tightly coupled and one change in this module can affect another module.
❌ dependency hiding: Also, when it is not made clear that an imported module is a singleton, it might lead to unexpected behavior and propagate throughout the application.
Singleton Pattern (Function implementation)
Note that I didn't call new MyDatabase('MY_DB_URI')
directly. Instead, I invoked global access to the instance (getInstance()
in the example). getInstance()
checks if an instance of MyDatabase
already exists. If not, it creates one, and if there is one already, it returns the existing DB instance. Then, it export default
s the instance to make it globally accessible. Upon export
, db.js module freezes the instance with JavaScript's built-in Object.freeze()
method to ensure that the one and only instance is not modifiable.
Singleton Pattern (Class implementation)
Here, note that the explicit global access in the previous example(.getInstance()
) is replaced by built-in constructor()
syntax.
✅ Language Integration
ES2015 Modules are singletons by default. Therefore, one doesn't have to explicitly implement the global, non-modifiable behavior of the exported module.
In the above example, index.html imports file1.js and file2.js in order, which respectively imports db.js
and connects to the database instance, and increments connectionCount
by 1. Both files are looking at the same instance of MyDatabase
. As a result, even though file2.js invokes connect()
method only once, connectionCount
is now 2, on top of the connection made by file1.js.
References
Learning JavaScript Design Patterns, Osmani, Addy. O'Reilly
Hallie, Lydia. (2022, August 18). A Tour of JavaScript & React Patterns [video file]. Retrieved from https://frontendmasters.com/courses/tour-js-patterns/