Python - Python Design Patterns (Singleton, Factory, Observer, etc.)

Introduction

Design patterns are proven and reusable solutions to common software design problems. They are not ready-made code that can be copied directly into a project; instead, they are templates or best practices that help developers structure their code effectively.

Python design patterns improve code maintainability, scalability, readability, and flexibility. They help developers create applications that are easier to modify and extend in the future.

Design patterns are generally classified into three categories:

  1. Creational Patterns

  2. Structural Patterns

  3. Behavioral Patterns

Why Design Patterns Are Important

Without proper design patterns, software projects can become difficult to manage as they grow. Design patterns provide:

  • Better code organization

  • Reduced code duplication

  • Easier maintenance

  • Improved scalability

  • Enhanced collaboration among developers

  • Increased software reliability

For example, when developing a large e-commerce application, different modules such as payment processing, inventory management, and user authentication need a well-structured architecture. Design patterns help achieve this structure.


1. Singleton Pattern

Definition

The Singleton pattern ensures that a class has only one instance throughout the application's lifetime and provides a global point of access to that instance.

When to Use

  • Database connections

  • Configuration settings

  • Logging systems

  • Cache management

  • Application settings

Example

class Singleton:
    _instance = None

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

obj1 = Singleton()
obj2 = Singleton()

print(obj1 is obj2)

Output

True

Both variables point to the same object.

Advantages

  • Saves memory

  • Prevents duplicate resource allocation

  • Provides centralized control

Disadvantages

  • Difficult to test

  • Can create hidden dependencies

  • Reduces flexibility in some situations


2. Factory Pattern

Definition

The Factory pattern creates objects without exposing the object creation logic to the client.

Instead of creating objects directly using constructors, a factory method decides which object should be created.

Real-World Example

Consider a vehicle showroom.

Instead of customers creating cars or bikes themselves, the showroom factory provides the requested vehicle.

Example

class Car:
    def drive(self):
        return "Driving a car"

class Bike:
    def drive(self):
        return "Riding a bike"

class VehicleFactory:

    @staticmethod
    def create_vehicle(vehicle_type):

        if vehicle_type == "car":
            return Car()

        elif vehicle_type == "bike":
            return Bike()

        else:
            raise ValueError("Unknown vehicle")

vehicle = VehicleFactory.create_vehicle("car")
print(vehicle.drive())

Output

Driving a car

Benefits

  • Simplifies object creation

  • Makes code flexible

  • Easy to add new object types


3. Observer Pattern

Definition

The Observer pattern defines a one-to-many relationship between objects.

When one object changes its state, all dependent objects are automatically notified.

Real-World Example

Social media platforms use the observer pattern.

When a user posts new content:

  • Followers receive notifications.

  • The user is the subject.

  • Followers are observers.

Example

class Subscriber:

    def update(self, message):
        print(message)

class Channel:

    def __init__(self):
        self.subscribers = []

    def subscribe(self, subscriber):
        self.subscribers.append(subscriber)

    def notify(self, message):

        for subscriber in self.subscribers:
            subscriber.update(message)

channel = Channel()

user1 = Subscriber()
user2 = Subscriber()

channel.subscribe(user1)
channel.subscribe(user2)

channel.notify("New video uploaded")

Output

New video uploaded
New video uploaded

Applications

  • Notification systems

  • Event handling

  • Real-time dashboards

  • Stock market monitoring


4. Builder Pattern

Definition

The Builder pattern constructs complex objects step by step.

Instead of creating a large object in a single constructor call, the builder gradually assembles it.

Example

Imagine building a computer:

  • CPU

  • RAM

  • Storage

  • Graphics Card

Different configurations can be built using the same process.

Example Code

class Computer:

    def __init__(self):
        self.parts = []

    def add(self, part):
        self.parts.append(part)

    def show(self):
        print(self.parts)

computer = Computer()

computer.add("CPU")
computer.add("RAM")
computer.add("SSD")

computer.show()

Output

['CPU', 'RAM', 'SSD']

Benefits

  • Simplifies creation of complex objects

  • Improves readability

  • Supports different configurations


5. Adapter Pattern

Definition

The Adapter pattern allows incompatible interfaces to work together.

It acts as a bridge between two different systems.

Real-World Example

A mobile charger adapter converts electrical connections so devices can use them properly.

Example

class OldPrinter:

    def print_text(self):
        return "Printing"

class Adapter:

    def __init__(self, printer):
        self.printer = printer

    def print(self):
        return self.printer.print_text()

printer = OldPrinter()
adapter = Adapter(printer)

print(adapter.print())

Output

Printing

Applications

  • Legacy system integration

  • Third-party library integration

  • API compatibility


6. Strategy Pattern

Definition

The Strategy pattern allows selecting an algorithm at runtime.

Instead of hardcoding behavior, different strategies can be switched dynamically.

Example

class CreditCard:

    def pay(self, amount):
        print(f"Paid {amount} using Credit Card")

class PayPal:

    def pay(self, amount):
        print(f"Paid {amount} using PayPal")

class Payment:

    def __init__(self, strategy):
        self.strategy = strategy

    def make_payment(self, amount):
        self.strategy.pay(amount)

payment = Payment(CreditCard())
payment.make_payment(500)

Output

Paid 500 using Credit Card

Benefits

  • Flexible algorithms

  • Easy maintenance

  • Reduces conditional statements


7. Command Pattern

Definition

The Command pattern converts requests into objects.

This allows requests to be stored, queued, logged, or undone.

Example

class Light:

    def turn_on(self):
        print("Light ON")

class Command:

    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.turn_on()

light = Light()
command = Command(light)

command.execute()

Output

Light ON

Applications

  • Undo and redo functionality

  • Task scheduling

  • Menu actions in software


8. Prototype Pattern

Definition

The Prototype pattern creates new objects by copying existing objects.

Instead of creating objects from scratch, an existing object is cloned.

Example

import copy

class Employee:

    def __init__(self, name):
        self.name = name

employee1 = Employee("John")

employee2 = copy.deepcopy(employee1)

employee2.name = "David"

print(employee1.name)
print(employee2.name)

Output

John
David

Benefits

  • Faster object creation

  • Useful for large complex objects

  • Reduces initialization cost


Best Practices for Using Design Patterns

  1. Understand the problem before selecting a pattern.

  2. Avoid using patterns unnecessarily.

  3. Keep implementations simple.

  4. Follow Python's philosophy of readability.

  5. Combine patterns only when necessary.

  6. Document the pattern usage for future developers.

  7. Test implementations thoroughly.


Conclusion

Python design patterns provide standardized solutions to recurring software design problems. Patterns such as Singleton, Factory, Observer, Builder, Adapter, Strategy, Command, and Prototype help developers create software that is flexible, reusable, maintainable, and scalable. Understanding these patterns enables programmers to design robust applications that can adapt to changing requirements while maintaining clean and organized code structures.