Singleton Pattern
Definition
Frequency of use (in JavaScript):
●●●●● high
Motivation
It’s important for some classes (or equivalent types in non class based languages) to have exactly one instance.
Some examples for your kind perusal.
- There should a single instance of URL router for your application (ideally)
- Caching strategy design (ideally only one instance should be there)
- Services Objects
How do we ensure that a class has only one instance and that the instance is easily accessible?
A global variable makes an object accessible, but it doesn’t keep you from instantiating multiple objects.
A better solution is to make the class itself responsible for keeping track of its sole instance.
The class can ensure that no other instance can be created (by intercepting requests to create new objects), and it can provide a way to access the instance. This is the Singleton pattern.
Points to Ponder
Singleton is not in a fundamental way inherently bad in the sense that anything in design or computing or technology can be good or bad (As you will hear and read a lot about bad things on singleton). The idea behind the design pattern series is you understand the core concepts, read other people’s opinion and then make a value judgement.
It’s not that if it didn’t worked for them, it may not work for you. Let’s get started. And also even if you don’t want to use the pattern, when you encounter them you know what you are dealing with rather than taking guess work.
Applicability
Use the Singleton pattern when
– There must be exactly one instance of a class, and it must be accessible to
clients from a well known access point.
– When the sole instance should be extensible by sub-classing, and clients
should be able to use an extended instance without modifying their code.
Structure
As you can observe by the above diagram a singleton class should have a public static method to create an instance (in this case the method name is instance(). Additionally it can have other methods.
Yet another conceptual diagram for more indepth understanding.
Conceptual Code (Best Approach)
There are various ways to create a singleton class/function in javascript. Do note that we will use the ES6+ class notation for all examples. This will help you carry your understanding to other languages like c#, java etc (though not exactly but enough to understand other language nuisances and adapt as needed).
Here we will make use of the Symbol feature in JavaScript.
NOTE: (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
The data type symbol is a primitive data type. The Symbol()
function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class, but is incomplete as a constructor because it does not support the syntax “new Symbol()
“.
Every symbol value returned from Symbol()
is unique. A symbol value may be used as an identifier for object properties; this is the data type’s primary purpose, although other use-cases exist, such as enabling opaque data types, or serving as an implementation-supported unique identifier in general.
Conceptual Code -Singleton – Approach 1
// Used for hash key to store instance
const singleton = Symbol();
// The unique identifier for the only instance
const singletonIdentifier = Symbol();
class Singleton {
constructor(enforcer) {
if (enforcer !== singletonIdentifier) {
throw new Error('Cannot construct singleton');
}
}
// Getter method to return a new or stored instance
static get instance() {
if (!this[singleton]) {
this[singleton] = new Singleton(singletonIdentifier);
}
return this[singleton];
}
singletonMethod() {
return 'singletonMethod';
}
static staticMethod() {
return 'staticMethod';
}
}
Dry running/testing the code
function runSingleton() {
// Both of the below instance point to same instance in memory
const instance1 = Singleton.instance;
const instance2 = Singleton.instance;
// This will give you error
//const instance3 = new Singleton();
// Call instance method or /prototype method
console.log(instance1.singletonMethod());
// Static method
console.log(Singleton.staticMethod());
// Should return true as both instance are same
console.log("instance1 == instance2 ? " + (instance1 === instance2));
}
runSingleton(); // Let the action begin
Practical Example – Singleton – CacheManager
Let’s now take a look at a practical example of Singleton pattern in action by building a cache manager (key/value pair)
const instanceKey = Symbol();
const instanceKeyIdentifier = Symbol();
class CacheManager {
constructor(enforcer) {
this.items = {};
this.count = 0;
if (enforcer !== instanceKeyIdentifier) {
throw new Error('Cannot construct cacheManagerKey');
}
}
// instance getter
static get instance() {
if (!this[instanceKey]) {
this[instanceKey] = new CacheManager(instanceKeyIdentifier);
}
return this[instanceKey];
}
// Add key/value pair
add(key, value) {
this.items[key] = value;
this.count++;
}
// Get the value by key
read(key) {
return this.items[key];
}
eachObject(fn) {
for (let [key, value] of Object.entries(this.items)) {
fn({key, value});
}
}
values(fn) {
for (let [key, value] of Object.entries(this.items)) {
fn(value);
}
}
keys(fn) {
for (let [key, value] of Object.entries(this.items)) {
fn(key);
}
}
delete(key) {
delete this.items[key];
this.count--;
}
}
CacheManager Test Code
function runSingletonPractical() {
const instance = CacheManager.instance;
instance.add("key1", "value1");
instance.add("key2", "value2");
instance.add("key3", "value3");
const instance2 = CacheManager.instance;
console.log(instance.read("key2"), instance2.read("key2"));
console.log("Count: ", instance2.count);
instance.delete("key2");
console.log("Count: ", instance2.count);
// Test each method
instance.eachObject(item => console.log(item));
instance.values(value => console.log("value: ", value));
instance.keys(key => console.log("key: ", key));
}
runSingletonPractical();
Limitations of the pattern (adapted from web, from my peer groups)
- People think it’s hard to test (but depends on your goal and testing strategy and tools).
- Singletons’ create hidden dependencies (well not really if the purpose of singletons are made clear withing the working group members)
- Singletons breaks OCP and SRP principles of SOLID (well again not really, as patterns always works best in groups rather than isolation. So based on needs you can bring in other patterns to fix the issue).
Source Code + Unit Testing
The source code + unit testing (mocha+chai) is available at https://github.com/rajeshpillai/designpatterns-code-js
A note
Please note, this is an early release of the post and will be updated more benefits, alternate approach ,cons and some solutions for fixing the cons.