Design Patterns
Design patterns are proven solutions to common problems in software design. They're like recipes that developers have refined over decades.
You don't invent a new way to build a door every time you build a house. You use a proven door design. Design patterns are the "door designs" of software.
The Three Categories
Design patterns fall into three categories based on what they do:
How objects are created
How objects are composed
How objects communicate
---
Pattern 1: Factory (Creational)
> Create objects without specifying the exact class to instantiate.
The Factory pattern hides the complexity of object creation. The client just asks for what it needs, and the factory figures out which class to use.
from abc import ABC, abstractmethod
# Abstract product
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount): pass
# Concrete products
class CreditCardProcessor(PaymentProcessor):
def process(self, amount):
return f"Charged ${amount} to credit card"
class PayPalProcessor(PaymentProcessor):
def process(self, amount):
return f"Charged ${amount} via PayPal"
class UPIProcessor(PaymentProcessor):
def process(self, amount):
return f"Charged ₹{amount} via UPI"
# Factory
class PaymentFactory:
@staticmethod
def create(payment_type):
processors = {
"credit_card": CreditCardProcessor,
"paypal": PayPalProcessor,
"upi": UPIProcessor
}
return processors[payment_type]()
# Client code - doesn't know about specific classes
processor = PaymentFactory.create("upi")
print(processor.process(500)) # "Charged ₹500 via UPI"
---
Pattern 2: Singleton (Creational)
> Ensure a class has only one instance and provide a global access point.
Some resources should only exist once: database connections, configuration managers, logging services.
Multiple database connections opened. Inconsistent config values. Log files scattered.
One shared connection. Single source of truth. Centralized logging.
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._load_config()
return cls._instance
def _load_config(self):
# Load from file/env once
self.database_url = "postgres://localhost/mydb"
self.api_key = "secret-key-123"
self.debug_mode = True
# Both variables point to the SAME instance
config1 = Config()
config2 = Config()
print(config1 is config2) # True
print(config1.database_url) # "postgres://localhost/mydb"
---
Pattern 3: Strategy (Behavioral)
> Define a family of algorithms and make them interchangeable.
Strategy lets you swap algorithms at runtime without changing the code that uses them. Perfect for when you have multiple ways to do something.
from abc import ABC, abstractmethod
# Strategy interface
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, base_price): pass
# Concrete strategies
class RegularPricing(PricingStrategy):
def calculate(self, base_price):
return base_price
class PremiumPricing(PricingStrategy):
def calculate(self, base_price):
return base_price * 0.9 # 10% discount
class BlackFridayPricing(PricingStrategy):
def calculate(self, base_price):
return base_price * 0.5 # 50% off!
# Context
class ShoppingCart:
def __init__(self, pricing_strategy):
self.strategy = pricing_strategy
self.items = []
def add_item(self, price):
self.items.append(price)
def total(self):
base_total = sum(self.items)
return self.strategy.calculate(base_total)
# Usage - easily swap strategies
cart = ShoppingCart(BlackFridayPricing())
cart.add_item(100)
cart.add_item(50)
print(cart.total()) # 75.0 (50% off)
# Change strategy at runtime
cart.strategy = RegularPricing()
print(cart.total()) # 150.0
---
Pattern 4: Observer (Behavioral)
> When one object changes, notify all dependent objects automatically.
Observer creates a subscription mechanism. Objects can subscribe to events and get notified when something happens.
from abc import ABC, abstractmethod
# Observer interface
class OrderObserver(ABC):
@abstractmethod
def update(self, order): pass
# Concrete observers
class EmailNotifier(OrderObserver):
def update(self, order):
print(f"📧 Email sent for order #{order.id}")
class SMSNotifier(OrderObserver):
def update(self, order):
print(f"📱 SMS sent for order #{order.id}")
class InventoryUpdater(OrderObserver):
def update(self, order):
print(f"📦 Inventory updated for order #{order.id}")
# Subject
class Order:
_observers = []
def __init__(self, id):
self.id = id
self._observers = []
def subscribe(self, observer):
self._observers.append(observer)
def unsubscribe(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self)
def place(self):
print(f"Order #{self.id} placed!")
self.notify() # Notify all observers
# Usage
order = Order(123)
order.subscribe(EmailNotifier())
order.subscribe(SMSNotifier())
order.subscribe(InventoryUpdater())
order.place()
# Output:
# Order #123 placed!
# 📧 Email sent for order #123
# 📱 SMS sent for order #123
# 📦 Inventory updated for order #123
---
Quick Reference
| Pattern | Use When | Example |
|---|---|---|
| Factory | Multiple related classes to create | Payment processors |
| Singleton | Only one instance should exist | Config, Logger, DB pool |
| Strategy | Swap algorithms at runtime | Pricing, sorting, auth |
| Observer | React to events/changes | Notifications, UI updates |
Key Takeaways
- Patterns are tools - use the right one for the problem
- Don't force patterns - if simple code works, use simple code
- Patterns support SOLID - Factory uses Open/Closed, Strategy uses Dependency Inversion
- Learn more patterns - Builder, Adapter, Decorator, Command are also very useful
Next up: Case Study: Designing a Parking Lot - applying everything we've learned to a real LLD problem.