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
- Implement a Singleton database connection manager
- Create a Factory for different notification types (email, SMS, push)
- Build an Observer pattern for a stock price tracker
- Implement Strategy pattern for different sorting algorithms
- Create a Builder for constructing HTTP requests :::