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:
-
Creational Patterns
-
Structural Patterns
-
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
-
Understand the problem before selecting a pattern.
-
Avoid using patterns unnecessarily.
-
Keep implementations simple.
-
Follow Python's philosophy of readability.
-
Combine patterns only when necessary.
-
Document the pattern usage for future developers.
-
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.