Tiny System Design - Design Patterns - Creational - Singleton

Tiny System Design - Design Patterns - Creational - Singleton

Read Part 1 here.

https://www.dhirubhai.net/posts/pillairajesh_tinysystemdesign-activity-7112844796683051008-w-Qq?utm_source=share&utm_medium=member_desktop

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.

  • 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.

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.

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        


(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

  1. Used the __new__ method to implement the Singleton pattern in Python.
  2. Removed the explicit error for direct instantiation since the Singleton pattern is now handled by the __new__ method.
  3. Used Python's built-in dictionary methods for each_object, values, and keys.

(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

  1. Used the Lazy<T> type to implement the Singleton pattern in C#. This ensures thread safety and lazy initialization.
  2. Used C#'s Dictionary<TKey, TValue> for the cache storage.
  3. Used properties for Instance and Count.

(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:

  1. Used the Singleton pattern in Go with the help of sync.Once to ensure that only one instance of CacheManager is created.
  2. Used Go's built-in map for cache storage.
  3. Added mutex locks (sync.RWMutex) to ensure thread safety when reading/writing to the cache.

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).

Applications

The following applications/areas uses Singleton or similar pattern extensively.

  • Game Programming (The Game Environment is usually a Singleton class which includes scores, list of enemies etc.
  • Service or Factory Objects in typical web applications (They are usually mixed with Dependency Injection.
  • Caching (Singleton or an adaptation of the pattern)

Related Patterns and principles

This section lists down patterns that makes singleton effective.

  • Factory Method
  • Dependency Inversion Principle (from SOLID principles)


要查看或添加评论,请登录

社区洞察

其他会员也浏览了