Global State & Singletons

Reference: The Clean Code Talks - Global State and Singletons

Global State in Programming

Using global variables can create hard-to-detect bugs and make the system behavior unpredictable. Changes in one part of the system can unintentionally affect other parts, leading to a codebase that’s challenging to debug and maintain.

global_count = 0

def increment_global_count():
    global global_count
    global_count += 1

def get_global_count():
    return global_count

print(get_global_count())  # Output: 0
increment_global_count()
print(get_global_count())  # Output: 1
More details

singleton vs Singleton Pattern

singleton (lowercase s)

Refers to the practice of using a single instance of a class throughout the application. It’s a pattern where the class itself doesn’t restrict instantiation, but the application does so by convention.

class DatabaseConnection:
    pass

database_connection = DatabaseConnection()
More details

Singleton (uppercase S)

This design pattern ensures that a class has only one instance and provides a global point of access to that instance. It’s enforced by making the constructor private and controlling the instance creation within the class.

class SingletonDatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
connection = SingletonDatabaseConnection()
More details

Deceptive APIs

These are APIs that conceal their dependencies, making the system more complex and the code harder to test. They often lead to unexpected behavior as the dependencies are not clear from the interface.

class PaymentProcessor:
    _instance = None

    def __new__(cls):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def process(self, amount):
        print(f"Processing payment of {amount}")

class CreditCard:
    def charge(self, amount):
        processor = PaymentProcessor()
        processor.process(amount)

card = CreditCard()
card.charge(100)
More details

Dependency Injection

This approach involves supplying objects with their dependencies from the outside rather than hardcoding them within the object. It leads to more testable, maintainable, and modular code.

class DatabaseConnection:
    pass

class UserRepository:
    def __init__(self, db_connection):
        self.db_connection = db_connection

db = DatabaseConnection()
user_repo = UserRepository(db)
More details