🔥 0
0
Lesson 3 of 4 20 min +125 XP

Design Patterns

Design patterns are proven solutions to common problems in software design. They're like recipes that developers have refined over decades.

Think of patterns like this:

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:

🏭
Creational

How objects are created

Factory, Singleton, Builder
🧱
Structural

How objects are composed

Adapter, Decorator, Facade
💬
Behavioral

How objects communicate

Strategy, Observer, Command

---

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.

Client Factory create(type) ProductA ProductB ProductC
Real Example: Payment Processing
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"
When to use Factory: When you have multiple related classes and the client shouldn't know which one to instantiate.

---

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.

❌ Without Singleton

Multiple database connections opened. Inconsistent config values. Log files scattered.

✓ With Singleton

One shared connection. Single source of truth. Centralized logging.

Real Example: Configuration Manager
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"
⚠️ Singleton Warning: Singletons can make testing harder and hide dependencies. Use sparingly for truly global resources.

---

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.

Context IStrategy StrategyA StrategyB
Real Example: Pricing Strategies
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
When to use Strategy: Sorting algorithms, compression methods, payment processing, validation rules, pricing models.

---

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.

subscribe() Subject notify() Email SMS Push
Real Example: Order Notifications
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
When to use Observer: Event systems, notifications, UI updates, real-time feeds, stock price alerts.

---

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.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is the main purpose of the Factory pattern?

2

When should you use the Singleton pattern?

3

The Strategy pattern helps you:

4

Which pattern would you use for a notification system where multiple services need to react to events?

SOLID Principles