Singleton

The singleton design pattern ensures that a class has only one instance and provides a global point of access to that system. This is often used for managing shared resources or configurations.

TL;DR

Ensures that only one object of a particular class is ever created.

https://refactoring.guru/design-patterns/singleton

Problem

  1. To control access to a shared resource, such as a database. The issue is that a class constructor, by design, always returns a new object.
  2. Provides a global access point to an instance without defining a global variable that can be overwritten (in particular with variables that are used to solve problem 1, above)

The Singleton pattern is generally considered an anti-pattern if overused, since it introduces a global state in an application that could have large downstream effects (since it makes code very tightly coupled). The way it works is by ensuring that after an object is created, any constructor calls for an object of that type returns the already existing object - knowledge that clients don’t necessarily need to know about.

Real-world analogy

A President can only be the only person in that seat of government; regardless of who the specific person who oocupies the office, the point of access for the rest of the government (or other governments) is the office of the President.

Solution

All implementations of the Singleton have these two steps in common:

  • Make the default constructor private, to prevent other objects from using the new operator with the Singleton class.
  • Create a static creation method that acts as a constructor. Under the hood, this method calls the private constructor to create an object and saves it in a static field. All following calls to this method return the cached object.

If your code has access to the Singleton class, then it’s able to call the Singleton’s static method. So whenever that method is called, the same object is always returned.

class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        # Initialize only once
        if not hasattr(self, 'initialized'):
            self.initialized = True
            self.value = 0
    
    def set_value(self, value):
        self.value = value
    
    def get_value(self):
        return self.value
 
 
# Example usage
if __name__ == "__main__":
    # Create two instances
    singleton1 = Singleton()
    singleton2 = Singleton()
    
    # They are the same object
    print(f"singleton1 is singleton2: {singleton1 is singleton2}")  # True
    print(f"ID of singleton1: {id(singleton1)}")
    print(f"ID of singleton2: {id(singleton2)}")
    
    # Setting value through one affects the other
    singleton1.set_value(42)
    print(f"singleton1 value: {singleton1.get_value()}")  # 42
    print(f"singleton2 value: {singleton2.get_value()}")  # 42
    
    singleton2.set_value(100)
    print(f"singleton1 value: {singleton1.get_value()}")  # 100
    print(f"singleton2 value: {singleton2.get_value()}")  # 100