Singleton Pattern
Singletons are classes which can be instantiated once, and can be accessed globally. This single instance can be shared throughout our application, which makes Singletons great for managing global state in an application.
First, let’s take a look at what a singleton can look like using an ES2015 class. For this example, we’re going to build a Counter
class that has:
- a
getInstance
method that returns the value of the instance - a
getCount
method that returns the current value of thecounter
variable - a
increment
method that increments the value ofcounter
by one - a
decrement
method that decrements the value ofcounter
by one
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
However, this class doesn’t meet the criteria for a Singleton! A Singleton should only be able to get instantiated once
. Currently, we can create multiple instances of the Counter
class.
By calling the new method twice, we just set counter1 and counter2 equal to different instances. The values returned by the getInstance method on counter1 and counter2 effectively returned references to different instances: they aren’t strictly equal!
Let’s make sure that only one instance of the Counter class can be created.
One way to make sure that only one instance can be created, is by creating a variable called instance. In the constructor of Counter, we can set instance equal to a reference to the instance when a new instance is created. We can prevent new instantiations by checking if the instance variable already had a value. If that’s the case, an instance already exists. This shouldn’t happen: an error should get thrown to let the user know
let instance;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
Perfect! We aren’t able to create multiple instances anymore.
Let’s export the Counter instance from the counter.js file. But before doing so, we should freeze the instance as well. The Object.freeze method makes sure that consuming code cannot modify the Singleton. Properties on the frozen instance cannot be added or modified, which reduces the risk of accidentally overwriting the values on the Singleton.
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;
Let’s take a look at an application that implements the Counter
example. We have the following files:
counter.js
: contains theCounter
class, and exports aCounter instance
as its default exportindex.js
: loads theredButton.js
andblueButton.js
modulesredButton.js
: importsCounter
, and addsCounter
’sincrement
method as an event listener to the red button, and logs the current value ofcounter
by invoking thegetCount
methodblueButton.js
: importsCounter
, and addsCounter
’s increment method as an event listener to the blue button, and logs the current value ofcounter
by invoking thegetCount
method
import "./redButton";
import "./blueButton";
console.log("Click on either of the buttons 🚀!");
Both blueButton.js
and redButton.js
import the same instance from counter.js
. This instance is imported as Counter
in both files.
When we invoke the increment method in either redButton.js or blueButton.js, the value of the counter property on the Counter instance updates in both files. It doesn’t matter whether we click on the red or blue button: the same value is shared among all instances. This is why the counter keeps incrementing by one, even though we’re invoking the method in different files.
Tradeoffs
However, the class implementation shown in the examples above is actually overkill. Since we can directly create objects in JavaScript, we can simply use a regular object to achieve the exact same result. Let’s cover some of the disadvantages of using Singletons!
Using a regular object
Let’s use the same example as we saw previously. However this time, the counter is simply an object containing:
- a
count
property - an
increment
method that increments the value ofcount
by one - a
decrement
method that decrements the value ofcount
by one
let count = 0;
const counter = {
increment() {
return ++count;
},
decrement() {
return --count;
}
};
Object.freeze(counter);
export { counter };
Since objects are passed by reference, both redButton.js
and blueButton.js
are importing a reference to the same counter
object. Modifying the value of count
in either of these files will modify the value on the counter
, which is visible in both files.
Exercise
Challenge
Turn this class into a singleton, to ensure that only one DBConnection instance can exist.