Skip to main content

Module 33 - Design Patterns

Design patterns are reusable solutions to common software design problems. Learn the most important patterns in Python.


1. Singleton Pattern

Ensure a class has only one instance.

class Singleton:
_instance = None

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

# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True - same instance

Using Decorator

def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance

@singleton
class Database:
def __init__(self):
print("Database initialized")

db1 = Database() # Prints: Database initialized
db2 = Database() # No print - returns existing instance
print(db1 is db2) # True

2. Factory Pattern

Create objects without specifying exact class.

from abc import ABC, abstractmethod

class Animal(ABC):
@abstractmethod
def speak(self):
pass

class Dog(Animal):
def speak(self):
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError(f"Unknown animal type: {animal_type}")

# Usage
dog = AnimalFactory.create_animal("dog")
print(dog.speak()) # Woof!

cat = AnimalFactory.create_animal("cat")
print(cat.speak()) # Meow!

3. Observer Pattern

Notify multiple objects about state changes.

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

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(self._state)

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

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

def update(self, state):
print(f"{self.name} received update: {state}")

# Usage
subject = Subject()

observer1 = Observer("Observer 1")
observer2 = Observer("Observer 2")

subject.attach(observer1)
subject.attach(observer2)

subject.set_state("New State")
# Observer 1 received update: New State
# Observer 2 received update: New State

4. Strategy Pattern

Define a family of algorithms and make them interchangeable.

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount):
pass

class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
return f"Paid ${amount} with credit card"

class PayPalPayment(PaymentStrategy):
def pay(self, amount):
return f"Paid ${amount} with PayPal"

class ShoppingCart:
def __init__(self, payment_strategy: PaymentStrategy):
self.payment_strategy = payment_strategy
self.total = 0

def add_item(self, price):
self.total += price

def checkout(self):
return self.payment_strategy.pay(self.total)

# Usage
cart = ShoppingCart(CreditCardPayment())
cart.add_item(50)
cart.add_item(30)
print(cart.checkout()) # Paid $80 with credit card

cart2 = ShoppingCart(PayPalPayment())
cart2.add_item(100)
print(cart2.checkout()) # Paid $100 with PayPal

5. Decorator Pattern

Add new functionality to objects dynamically.

class Coffee:
def cost(self):
return 5

def description(self):
return "Simple coffee"

class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee

def cost(self):
return self._coffee.cost() + 2

def description(self):
return self._coffee.description() + ", milk"

class SugarDecorator:
def __init__(self, coffee):
self._coffee = coffee

def cost(self):
return self._coffee.cost() + 1

def description(self):
return self._coffee.description() + ", sugar"

# Usage
coffee = Coffee()
print(f"{coffee.description()}: ${coffee.cost()}")

coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost()}")

coffee_with_milk_and_sugar = SugarDecorator(coffee_with_milk)
print(f"{coffee_with_milk_and_sugar.description()}: ${coffee_with_milk_and_sugar.cost()}")

6. Builder Pattern

Construct complex objects step by step.

class Pizza:
def __init__(self):
self.size = None
self.cheese = False
self.pepperoni = False
self.mushrooms = False

def __str__(self):
return f"Pizza: {self.size}, cheese={self.cheese}, pepperoni={self.pepperoni}, mushrooms={self.mushrooms}"

class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()

def set_size(self, size):
self.pizza.size = size
return self

def add_cheese(self):
self.pizza.cheese = True
return self

def add_pepperoni(self):
self.pizza.pepperoni = True
return self

def add_mushrooms(self):
self.pizza.mushrooms = True
return self

def build(self):
return self.pizza

# Usage
pizza = (PizzaBuilder()
.set_size("Large")
.add_cheese()
.add_pepperoni()
.build())

print(pizza)

7. Repository Pattern

Abstract data access logic.

from abc import ABC, abstractmethod

class Repository(ABC):
@abstractmethod
def add(self, entity):
pass

@abstractmethod
def get(self, id):
pass

@abstractmethod
def get_all(self):
pass

class UserRepository(Repository):
def __init__(self):
self.users = {}

def add(self, user):
self.users[user['id']] = user

def get(self, id):
return self.users.get(id)

def get_all(self):
return list(self.users.values())

# Usage
repo = UserRepository()
repo.add({'id': 1, 'name': 'Alice'})
repo.add({'id': 2, 'name': 'Bob'})

user = repo.get(1)
print(user) # {'id': 1, 'name': 'Alice'}

all_users = repo.get_all()
print(all_users)

8. Dependency Injection

Inject dependencies rather than creating them.

class Database:
def query(self, sql):
return f"Executing: {sql}"

class UserService:
def __init__(self, database: Database):
self.db = database

def get_user(self, user_id):
return self.db.query(f"SELECT * FROM users WHERE id={user_id}")

# Usage
db = Database()
service = UserService(db) # Inject dependency
result = service.get_user(1)
print(result)

Summary

✅ Singleton: One instance per class
✅ Factory: Create objects without specifying class
✅ Observer: Notify subscribers of changes
✅ Strategy: Interchangeable algorithms
✅ Decorator: Add functionality dynamically
✅ Builder: Construct complex objects step-by-step


Next Steps

In Module 34, you'll learn:

  • Python best practices
  • PEP 8 style guide
  • Code quality tools
  • Writing maintainable code

Practice Exercises

  1. Implement a Singleton database connection manager
  2. Create a Factory for different notification types (email, SMS, push)
  3. Build an Observer pattern for a stock price tracker
  4. Implement Strategy pattern for different sorting algorithms
  5. Create a Builder for constructing HTTP requests :::