Planetary Transits and Travel · CodeAmber

Implementing Singleton and Factory Design Patterns in Python

To implement the Singleton and Factory design patterns in Python, use a custom __new__ method or a decorator for Singletons to ensure only one instance exists, and create a creator class with a centralized method for the Factory pattern to instantiate various related objects. These patterns decouple object creation from business logic, improving system maintainability and resource management.

Implementing Singleton and Factory Design Patterns in Python

Design patterns provide standardized solutions to recurring architectural challenges in software engineering. In Python, the Singleton and Factory patterns are essential for managing shared resources and simplifying the instantiation of complex object hierarchies. Mastering these is a critical step when implementing common design patterns in Java and Python to ensure a codebase remains scalable.

The Singleton Pattern: Ensuring Single Instance Control

The Singleton pattern restricts the instantiation of a class to one single object. This is particularly useful for managing shared resources such as database connection pools, configuration managers, or logging services where creating multiple instances would lead to memory waste or inconsistent state.

Implementation via the __new__ Method

In Python, the most robust way to implement a Singleton is by overriding the __new__ method. While __init__ initializes an object, __new__ is the method that actually creates the instance.

class DatabaseConnection:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(DatabaseConnection, cls).__new__(cls)
        return cls._instance

    def __init__(self, connection_string=None):
        # Ensure initialization only happens once
        if not hasattr(self, 'initialized'):
            self.connection_string = connection_string
            self.initialized = True
            print(f"Connecting to {connection_string}...")

# Usage
db1 = DatabaseConnection("mysql://localhost:3306")
db2 = DatabaseConnection("postgresql://remote:5432")

print(db1 is db2)  # Output: True

When to Use the Singleton Pattern

Use a Singleton when a single point of truth is required across the entire application. If your software requires a global state that must be accessed by multiple modules without passing a reference through every function call, the Singleton is the appropriate architectural choice.

The Factory Pattern: Decoupling Object Creation

The Factory pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It abstracts the instantiation logic, meaning the client code does not need to know the specific class it is instantiating—only the interface it provides.

Implementation of a Concrete Factory

The Factory pattern is most effective when you have a family of related classes and want to delegate the choice of which class to instantiate to a specialized creator.

from abc import ABC, abstractmethod

# Product Interface
class FileParser(ABC):
    @abstractmethod
    def parse(self, file_content):
        pass

# Concrete Products
class JSONParser(FileParser):
    def parse(self, file_content):
        return f"Parsing {file_content} as JSON"

class XMLParser(FileParser):
    def parse(self, file_content):
        return f"Parsing {file_content} as XML"

# The Factory
class ParserFactory:
    @staticmethod
    def get_parser(file_type):
        parsers = {
            "json": JSONParser,
            "xml": XMLParser
        }
        parser_class = parsers.get(file_type.lower())
        if parser_class:
            return parser_class()
        raise ValueError(f"Unsupported file type: {file_type}")

# Usage
parser = ParserFactory.get_parser("json")
print(parser.parse("data.json")) # Output: Parsing data.json as JSON

The Advantage of the Factory Approach

The primary benefit of the Factory pattern is the adherence to the Open/Closed Principle: the code is open for extension but closed for modification. If a new file format (e.g., CSV) needs to be supported, you simply add a new CSVParser class and update the dictionary in the factory, leaving the existing client logic untouched. This is a core component of best practices for writing clean code.

Side-by-Side Comparison: Singleton vs. Factory

While both patterns manage object creation, they solve fundamentally different problems.

Feature Singleton Pattern Factory Pattern
Primary Intent Control instance count (exactly one) Abstract the creation process
Object Lifecycle Persistent throughout application life Created on-demand as needed
Key Benefit Resource conservation and global state Decoupling and flexibility
Typical Use Case Config files, Loggers, DB Connections UI Elements, Document Parsers, API Adapters

Architectural Integration and Best Practices

Integrating these patterns requires a balance between structure and over-engineering. CodeAmber recommends the following guidelines for professional software architecture:

  1. Avoid "Singleton Abuse": Overusing Singletons can turn your application into a collection of global variables, making unit testing difficult because state persists between tests.
  2. Prefer Composition: Use the Factory pattern to inject dependencies into your classes. This makes your code more modular and easier to test using mocks.
  3. Combine Patterns for Scalability: In large systems, a Factory may return a Singleton instance if the requested object is a shared resource. This hybrid approach is common when structuring a large-scale coding project for scalability.

Key Takeaways

Original resource: Visit the source site