July 10th, 2022
In this post, we will be going over the Singleton Design Pattern in JavaScript.
Singletons are a necessary component in software where only one instance of an object is necessary but is instantiated many times.
For example, if we were to implement LocalStorage
in JavaScript, it would look something like this:
class LocalStorage {
#data = {}
constructor(options) {
this.options = options || {}
}
get(key) {
return this.#data[key]
}
set(key, value) {
this.#data[key] = value
}
remove(key) {
delete this.#data[key]
}
clear() {
this.#data = {}
}
}
Let's now think about our choices in code decision making here whenever a function requests to use this interface.
Every time we instantiate a new instance of LocalStorage
, the 4 methods get
, set
, remove
and clear
will exist in all instances. However, new instances will not copy these methods from the original instance. Since they are included as part of the prototype of LocalStorage
they are shared methods, meaning every new instance of LocalStorage
will have the methods become references to the ones in the original instance.
This is crucial to understand when making decisions between factory functions and classes. You probably read one or more tutorials about factories before where they recommend them over classes. It's important to know that tips like those are not a one-size-fits-all solution because some situations suffer different problems.
I agree with the recommendation to prefer factories over classes however when your program also creates instances of an object often then it's time to consider classes because they have the advantage of sharing its methods throughout all object creations.
Here is the factory function equivalent of our LocalStorage
class:
function LocalStorage() {
let _data = {}
let _options = {}
return {
get(key) {
return _data[key]
},
set(key, value) {
_data[key] = value
},
remove(key) {
delete _data[key]
},
clear() {
_data = {}
},
}
}
Instantiating this would look something like this:
const localStorage = LocalStorage()
This is not a good decision since the returned object is a newly created object everytime LocalStorage
produces the object.
So if a program were to create hundreds of these it's wasteful because the implementation details of get
, set
, remove
and clear
never changes, so there is really no point in having them copied and take up memory space.
At this moment its best to use the Singleton Design Pattern on LocalStorage
so that get
, set
, remove
and clear
will be shared amongst all constructed instances.
We can actually use factories to implement a singleton if we were to encapsulate the prototype class:
function LocalStorage(options) {
let _data = {}
let _options = options || {}
return {
get(key) {
return _data[key]
},
set(key, value) {
_data[key] = value
},
remove(key) {
delete _data[key]
},
clear() {
_data = {}
},
}
}
const createLocalStorage = (function () {
return function createLocalStorage(options) {
if (!ls) {
ls = LocalStorage(options)
}
return ls
}
})()
Whenever our createLocalStorage
factory function is called it will check if a previously instantiated one exists on the ls
variable. If it does, it returns the existing one. If not, it will create a new one and slap that right on the ls
variable:
So far i've covered the factory function way of maintaining a Singleton. Here are other ways that can achieve this:
This implementation uses a class object and implements two static properties (technically it's one property and one method). Notice how it's equivalent to a global variable:
Name | Description |
---|---|
_instance |
Holds the global instance of LocalStorage . This is where it will be stored by instantiators |
getInstance |
Performs a check on LocalStorage._instance . If an instance was created previously, that will be returned every time. If it is empty, a new instance will be created and attached onto it |
class LocalStorage {
#data = {}
static _instance = null
static getInstance(options) {
if (!LocalStorage._instance) {
LocalStorage._instance = new LocalStorage(options)
}
return LocalStorage._instance
}
constructor(options) {
this.options = options || {}
}
get(key) {
return this.#data[key]
}
set(key, value) {
this.#data[key] = value
}
remove(key) {
delete this.#data[key]
}
clear() {
this.#data = {}
}
}
This is the simplest way to create a Singleton. It works because it's declared in the global scope and cannot be re-written due to the const
keyword. However this is not the best way to create singletons when you have more efficient ways to do it:
const MySingleton = {
sayHello() {
console.log('hello')
},
}
In this approach we keep the LocalStorage
class. However, this time we don't create any static
properties onto the class. We freeze the instance instead:
class LocalStorage {
#data = {}
static _instance = null
static getInstance(options) {
if (!LocalStorage._instance) {
LocalStorage._instance = new LocalStorage(options)
}
return LocalStorage._instance
}
constructor(options) {
this.options = options || {}
}
get(key) {
return this.#data[key]
}
set(key, value) {
this.#data[key] = value
}
remove(key) {
delete this.#data[key]
}
clear() {
this.#data = {}
}
}
const localStorage = new LocalStorage()
Object.freeze(localStorage)
This way we are ensured that nothing in the class can get written. However, maintaining the singleton works a little bit differently.
By using Object.freeze
we must ensure to never attempt to create or instantiate a LocalStorage
because our program will throw an error. This can mean something as simple as renaming createLocalStorage
to getLocalStorage
and utilizing it throughout the lifetime of the app.
Thank you for reading and look forward for more quality posts coming from me in the future!
Subscribe to my posts :)
Tags
© jsmanifest 2022