Design Patterns in Python

Design patterns are essential tools for developers to create maintainable, efficient, and scalable software solutions. These patterns provide reusable solutions to common problems in software design. In Python, a versatile and dynamic language, you can implement various design patterns to enhance code organization and readability. In this article, we’ll explore some of the most commonly used design patterns in Python.

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when you want to restrict the instantiation of a class to a single object, such as a configuration manager or a logging service.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

2. Factory Method Pattern

The Factory Method pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created. It’s useful for creating objects with similar behavior but different implementations.

from abc import ABC, abstractmethod

class Creator(ABC):
    @abstractmethod
    def factory_method(self):
        pass

    def some_operation(self):
        product = self.factory_method()
        return f"Creator: {product.operation()}"

class ConcreteCreator1(Creator):
    def factory_method(self):
        return ConcreteProduct1()

class ConcreteCreator2(Creator):
    def factory_method(self):
        return ConcreteProduct2()

class Product(ABC):
    @abstractmethod
    def operation(self):
        pass

class ConcreteProduct1(Product):
    def operation(self):
        return "ConcreteProduct1"

class ConcreteProduct2(Product):
    def operation(self):
        return "ConcreteProduct2"

3. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is widely used in event handling systems.

class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def detach(self, observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update()

class ConcreteSubject(Subject):
    def set_state(self, state):
        self._state = state
        self.notify()

    def get_state(self):
        return self._state

class Observer:
    def update(self):
        pass

class ConcreteObserver(Observer):
    def __init__(self, subject):
        self._subject = subject
        self._state = None
        self._subject.attach(self)

    def update(self):
        self._state = self._subject.get_state()

4. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows you to select an algorithm at runtime, providing flexibility in behavior.

from abc import ABC, abstractmethod

class Strategy(ABC):
    @abstractmethod
    def execute(self):
        pass

class ConcreteStrategyA(Strategy):
    def execute(self):
        return "Strategy A"

class ConcreteStrategyB(Strategy):
    def execute(self):
        return "Strategy B"

class Context:
    def __init__(self, strategy):
        self._strategy = strategy

    def context_interface(self):
        return self._strategy.execute()

5. Decorator Pattern

The Decorator pattern allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It’s useful for adding features to classes without subclassing.

class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        return "ConcreteComponent"

class Decorator(Component):
    def __init__(self, component):
        self._component = component

    def operation(self):
        return self._component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        return f"ConcreteDecoratorA({self._component.operation()})"

class ConcreteDecoratorB(Decorator):
    def operation(self):
        return f"ConcreteDecoratorB({self._component.operation()})"

5. Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface to a complex system of classes, making it easier to use and understand. It acts as a “facade” or an entry point to a set of interfaces or classes, hiding the underlying complexity from the client. Here’s an example of the Facade pattern in Python:

# Complex subsystem classes
class Subsystem1:
    def operation1(self):
        return "Subsystem 1: Operation 1"

    def operation2(self):
        return "Subsystem 1: Operation 2"

class Subsystem2:
    def operation1(self):
        return "Subsystem 2: Operation 1"

    def operation2(self):
        return "Subsystem 2: Operation 2"

# Facade class
class Facade:
    def __init__(self):
        self._subsystem1 = Subsystem1()
        self._subsystem2 = Subsystem2()

    def operation(self):
        result = []
        result.append("Facade initializes subsystems:")
        result.append(self._subsystem1.operation1())
        result.append(self._subsystem2.operation1())
        result.append("Facade orders subsystems to perform the action:")
        result.append(self._subsystem1.operation2())
        result.append(self._subsystem2.operation2())
        return "\n".join(result)

# Client code
def main():
    facade = Facade()
    result = facade.operation()
    print(result)

if __name__ == "__main__":
    main()

6. Dependency Injection Pattern

The Dependency Injection pattern revolves around the concept of separating the concerns of creating objects and using objects. It ensures that an object’s dependencies (e.g., other objects, services, or values) are provided from the outside, rather than being created or instantiated internally. This approach promotes the Single Responsibility Principle and allows for better testability and flexibility in your codebase.

There are three common types of dependency injection:

  1. Constructor Injection: Dependencies are injected through a class’s constructor. This is the most common form of dependency injection and is often used when dependencies are required for the class to function correctly.
  2. Method Injection: Dependencies are injected through methods, typically setter methods. This is useful when you have optional dependencies or when you want to change a class’s behavior during runtime by providing different dependencies.
  3. Property Injection: Dependencies are injected into a class’s properties or attributes. This is less common but can be used when you need to inject dependencies into an existing instance of a class.
class DataFetcher:
    def fetch_data(self):
        # Simulate fetching data from a database
        return "Data fetched from the database"


class ReportGenerator:
    def __init__(self, data_fetcher):
        self.data_fetcher = data_fetcher

    def generate_report(self):
        data = self.data_fetcher.fetch_data()
        # Generate the report using the fetched data
        return f"Generated report with data: {data}"


def main():
    # Create instances of DataFetcher and ReportGenerator
    data_fetcher = DataFetcher()
    report_generator = ReportGenerator(data_fetcher)

    # Generate a report
    report = report_generator.generate_report()

    # Print the report
    print(report)


if __name__ == "__main__":
    main()

7. Adapter Pattern

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them compatible without changing their source code. This pattern is especially useful when you want to reuse existing classes, integrate third-party libraries, or work with legacy code that doesn’t match your required interface.

Key Components of the Adapter Pattern:

  1. Target: This is the interface or the expected behavior that the client code interacts with.
  2. Adapter: This is the class that adapts the interface of an existing class or object to match the target interface.
  3. Adaptee: This is the class or object that has the behavior or interface that needs to be adapted.
# Adaptee (existing class)
class OldSystem:
    def legacy_operation(self):
        return "Legacy operation from the OldSystem"


# Target interface
class NewSystem:
    def new_operation(self):
        pass


# Adapter class
class Adapter(NewSystem):
    def __init__(self, old_system):
        self._old_system = old_system

    def new_operation(self):
        return self._old_system.legacy_operation()


# Client code using the NewSystem
def use_new_system(new_system):
    result = new_system.new_operation()
    print(result)


def main():
    # Using the Adapter to make OldSystem compatible with NewSystem
    old_system = OldSystem()
    adapter = Adapter(old_system)

    # Client code uses NewSystem interface
    use_new_system(adapter)


if __name__ == "__main__":
    main()