Singleton Pattern

Senthil Nayagan
Senthil Nayagan           

The singleton pattern ensures controlled access to a single instance of a class. While it offers significant benefits in terms of resource management and access control, developers must be mindful of its downsides, such as potential scalability issues and the introduction of global states. When used carefully, it can be an invaluable design choice for managing resources and coordinating actions across complex systems.
Work in progress
If you have any suggestions for improving the content or notice any inaccuracies, please email us at [email protected]. Thanks!
Singleton Pattern

Image Credits: Image generated by DALL-E.


Overview

A singleton pattern limits the number of instances of a class to one. It falls under a creational design pattern1 because it deals with an object creation procedure. The singleton pattern ensures that a class has only one instance and provides a global access point2 to it. With that said, its primary purpose is to control object creation, limiting the number of instances to just one, thereby preventing the instantiation of more than one object of a class.

Except for the special creation method (magic method in Python), the singleton pattern prevents the creation of objects via any other means. If an object has already been created, this method either returns it or creates a new one if needed.

Dunder methods: The dunder methods are special methods that start and end with double underscores. These double underscores are referred by the acronym “dunder.”


Use cases

Who would want to limit the number of instances a class has? The most common justification for this is to manage access to a shared resource, such a file or database.

Creation of a singleton object

Singleton in Python

The following shows the creation of a singleton object in Python. Here, __new__ is a special or magic method (aka the dunder method). This method is invoked every time a class is instantiated. Note that the __new__ method is invoked before the __init__ method gets called.

When we attempt to create an object inside of the __new__ method in the usual way (ClassName()), it runs recursively. It loops back on itself until the maximum recursion depth is reached and a RecursionError error is thrown.

class Singleton(object):
    """Represents Singleton class."""

    __instance = None

    # This __new__ method is invoked every time a class is instantiated.
    # It's invoked before the __init__ method gets called.
    def __new__(cls, *args, **kwargs):
        if not cls.__instance:
            print("Creating singleton object...")
            cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
            # cls.__instance = object.__new__(cls, *args, **kwargs)  # Other way of creating an object
            # cls.__instance = Singleton() #  Will get into a RecursionError error
        return cls.__instance

def main():
    singleton_object_1 = Singleton()
    singleton_object_2 = Singleton()

if __name__ == '__main__':
    main()

Output:

Creating singleton object...

The output shows that the new object is created just once.


Issues and challenges associated with the singleton pattern

Singleton pattern is popular in software engineering for various applications, including logging, driver objects, caching, thread pools, and configuration settings. However, it also introduces several issues and challenges, including:

Global State

Global state refers to data that is accessible from anywhere in an application, at any time, without being passed through the application flow. In the context of the singleton pattern, the single instance of the class that is globally accessible acts as this global state.

Issue with global state: Consider an application that manages configuration settings for different parts of the system. Let’s say these settings are accessed via a singleton instance, say for example ConfigManager, which loads settings from a file and provides global access to these settings. While initially, this approach might seem convenient, it can lead to several problems as follows:

Tight coupling

Let’s take the same example of ConfigManager. All components in the application directly depend on ConfigManager for configuration data, making them tightly coupled to this singleton. This coupling makes it harder to modify or replace ConfigManager without affecting the entire system.

Scalability concerns

Scalability issues with the singleton pattern arise mainly because it enforces a single shared instance across an application, leading to problems in a distributed system or a system that requires concurrency. Consider an application using the singleton pattern for managing database connections through a DatabaseConnectionManager class. This class ensures that there is only one global database connection throughout the application, which seems efficient at first glance. However, as the application scales and the number of simultaneous user requests increases, several problems can emerge. For instance, as the number of concurrent requests increases, the single connection becomes a bottleneck. Requests may start to queue up, waiting for their turn to use the connection, leading to increased response times and degraded application performance.

Testing difficulties

Because singletons are accessible throughout the application, isolating tests to check individual components can be challenging. When tests run, they may inadvertently alter the singleton’s state, which could affect other tests. - Global state and side effects: The Logger instance maintains a global state through its logs. When multiple tests log different messages, they can interfere with each other. For example, one test might expect the logs to contain specific messages, but if tests are run in parallel, previous test cases might have left residual data, leading to unpredictable results. - Difficulty in mocking: The difficulty in mocking singleton instances in unit tests is a common challenge that stems from the pattern’s design, which ensures a class has only one instance throughout the application’s lifecycle. This design makes it hard to replace a singleton with a mock object during testing.

Unpredictable behavior

Since any part of the application can modify the global state, tracking down who changed the state can become difficult. For example, if one component changes a setting in ConfigManager, it might have unintended side effects on other parts of the system that rely on that setting, leading to bugs that are hard to diagnose and fix immediately.


  1. Design patterns: In software engineering, a design pattern is a general, reusable solution to a commonly occurring problem in software design. It’s not a fully finished design that can be put into source code right away. Instead, it serves as a description, model, or template for problem-solving that may be used in a variety of situations. 

  2. Global access: Singleton provides a global access point to that instance, making it easily accessible from any point in the application without passing the object around. 

Comments

comments powered by Disqus