Tiny System Design - Design Patterns - Creational - Singleton
Rajesh Pillai
Co-Founder, Software Engineer and CTO @Algorisys Technologies - Building Products and Upskilling upcoming generations
Read Part 1 here.
Welcome to part 2 and in this article, we will take a practical look at some simplified version of use cases where patterns shines.
Creational Patterns
We already know from part 1 that creational patterns helps is creating objects. Well that' easy said than done. When you want to create an object, but it’s complex or you want to have only one, or you want to decide what type to create at the last moment, you use these patterns. They help in the process of object creation while hiding the creation logic.
Let's begin with the Singleton Pattern.
Singleton
Definition
Ensure a class has only one instance and provide a global point of access to it.
?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.
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.
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.
A note on code examples
I have purposefully kept code examples simple so that even beginner could follow.
Conceptual Code (Approach 1) – Object Literal
The quick way to create Singleton or Singleton like object in JavaScript is using Object Literal.
var Singleton = {
attribute1: true,
attribute2: 10,
method1: function() {
console.log("This is method1");
},
method2: function(arg) {
}
};
// The usage is quite simple as shown below.
Singleton.attribute = 20;
Singleton.method1(); // prints => This is method1
Conceptual Code (Approach 2) – Revealing Module Pattern (JavaScript)
Let’s use the Revealing Module Pattern to build up a structure for an image gallery. More about RMP will be covered separately.
var ImageGallery = (function () {
// Let's make sure no one can directly access our images
let index = 0;
let images = ["sun.jpg", "star.jpg", "moon.jpg"];
let paused = false;
// We'll expose all these functions to the user
function show () {
console.log('Showing image...', images[index]);
}
function add (imageData) {
images.push(imageData);
console.log(`Image ${imageData} added.`);
}
function next () {
index++;
if (index >= images.length) {
index = 0; // Reset index to beginning of array
}
console.log('Next Image is', images[index]);
}
function loadImages(imageArray) {
images = imageArray;
index = 0;
}
return {
show: show,
next: next,
add: add,
load: loadImages
}
})(); // our IIFE function (surrounded with parens) is invoked here
Lets take a moment to look at how the client will use our ImageGallery code.
ImageGallery.show(); // prints => Showing image sun
ImageGallery.next(); // prints => Next image is star.jpg
ImageGallery.show();
// Reload the data (you can fetch from remote api as well)
ImageGallery.load(["apple","orange","pears"]);
ImageGallery.show(); // prints => Showing image apple
Conceptual Code (Approach 3) – Class + Symbol (JavaScript)
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.
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
(JavaScript) Practical Example – Singleton – Simple 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 directly instantiate CacheManager. Use CacheManager.instance instead.');
}
}
// 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();
(Python) Practical Example – Singleton – Simple CacheManager
class CacheManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.items = {}
cls._instance.count = 0
return cls._instance
def add(self, key, value):
self.items[key] = value
self.count += 1
def read(self, key):
return self.items.get(key)
def delete(self, key):
if key in self.items:
del self.items[key]
self.count -= 1
def each_object(self):
return self.items.items()
def values(self):
return self.items.values()
def keys(self):
return self.items.keys()
def run_singleton_practical():
instance = CacheManager()
instance.add("key1", "value1")
instance.add("key2", "value2")
instance.add("key3", "value3")
instance2 = CacheManager()
print(instance.read("key2"), instance2.read("key2"))
print("Count:", instance2.count)
instance.delete("key2")
print("Count:", instance2.count)
# Test each method
for item in instance.each_object():
print(item)
for value in instance.values():
print("value:", value)
for key in instance.keys():
print("key:", key)
run_singleton_practical()
Python notes
(c#) Practical Example – Singleton – Simple CacheManager
using System;
using System.Collections.Generic;
public sealed class CacheManager
{
private static readonly Lazy<CacheManager> _instance = new Lazy<CacheManager>(() => new CacheManager());
private Dictionary<string, string> _items = new Dictionary<string, string>();
public static CacheManager Instance => _instance.Value;
private CacheManager() { }
public int Count => _items.Count;
public void Add(string key, string value)
{
_items[key] = value;
}
public string Read(string key)
{
return _items.TryGetValue(key, out var value) ? value : null;
}
public void Delete(string key)
{
_items.Remove(key);
}
public IEnumerable<KeyValuePair<string, string>> EachObject()
{
return _items;
}
public IEnumerable<string> Values()
{
return _items.Values;
}
public IEnumerable<string> Keys()
{
return _items.Keys;
}
}
public class Program
{
public static void Main()
{
var instance = CacheManager.Instance;
instance.Add("key1", "value1");
instance.Add("key2", "value2");
instance.Add("key3", "value3");
var instance2 = CacheManager.Instance;
Console.WriteLine(instance.Read("key2"));
Console.WriteLine(instance2.Read("key2"));
Console.WriteLine($"Count: {instance2.Count}");
instance.Delete("key2");
Console.WriteLine($"Count: {instance2.Count}");
// Test each method
foreach (var item in instance.EachObject())
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
foreach (var value in instance.Values())
{
Console.WriteLine($"value: {value}");
}
foreach (var key in instance.Keys())
{
Console.WriteLine($"key: {key}");
}
}
}
C# Notes
(Golang) Practical Example – Singleton – Simple CacheManager
package main
import (
"fmt"
"sync"
)
type CacheManager struct {
items map[string]string
mu sync.RWMutex
}
var instance *CacheManager
var once sync.Once
func GetCacheManagerInstance() *CacheManager {
once.Do(func() {
instance = &CacheManager{
items: make(map[string]string),
}
})
return instance
}
func (cm *CacheManager) Add(key, value string) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.items[key] = value
}
func (cm *CacheManager) Read(key string) string {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.items[key]
}
func (cm *CacheManager) Delete(key string) {
cm.mu.Lock()
defer cm.mu.Unlock()
delete(cm.items, key)
}
func (cm *CacheManager) EachObject() map[string]string {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.items
}
func main() {
instance := GetCacheManagerInstance()
instance.Add("key1", "value1")
instance.Add("key2", "value2")
instance.Add("key3", "value3")
instance2 := GetCacheManagerInstance()
fmt.Println(instance.Read("key2"), instance2.Read("key2"))
instance.Delete("key2")
// Test each method
for key, value := range instance.EachObject() {
fmt.Printf("key: %s, value: %s\n", key, value)
}
}
Go Notes:
Limitations of the pattern (adapted from web, from my peer groups)
Applications
The following applications/areas uses Singleton or similar pattern extensively.
Related Patterns and principles
This section lists down patterns that makes singleton effective.