design patterns
🏗️ Design Patterns in Python
📌 What You'll Learn
- •What design patterns are and why they matter
- •Creational patterns (Singleton, Factory, Builder)
- •Structural patterns (Adapter, Decorator, Facade)
- •Behavioral patterns (Observer, Strategy, Command)
- •Python-specific implementations
- •When to use each pattern
🔍 What are Design Patterns?
Design patterns are reusable solutions to commonly occurring problems in software design. They're not code you copy directly, but templates for solving problems that can be applied in many situations.
Why Learn Design Patterns?
- •Common vocabulary - "Let's use a Factory here" is clearer than explaining the entire concept
- •Proven solutions - These patterns have been tested by thousands of developers
- •Better architecture - Leads to more maintainable, flexible code
- •Interview preparation - Common topic in software engineering interviews
Python's Advantage
Python's dynamic nature and first-class functions make many patterns simpler than in static languages like Java or C++.
🏭 Creational Patterns
Patterns that deal with object creation mechanisms.
Singleton
Ensures a class has only one instance and provides global access to it.
class Singleton:
"""Classic Singleton implementation."""
_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
# Pythonic Singleton - use a module!
# config.py
class _Config:
def __init__(self):
self.debug = False
self.database_url = ""
config = _Config() # Single instance
# other_file.py
from config import config # Always the same instance
When to use: Database connections, configuration managers, logging.
Factory Pattern
Creates objects without specifying the exact class of object to be created.
from abc import ABC, abstractmethod
# Abstract product
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
pass
# Concrete products
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
class Bird(Animal):
def speak(self) -> str:
return "Tweet!"
# Factory
class AnimalFactory:
@staticmethod
def create_animal(animal_type: str) -> Animal:
animals = {
"dog": Dog,
"cat": Cat,
"bird": Bird
}
if animal_type not in animals:
raise ValueError(f"Unknown animal: {animal_type}")
return animals[animal_type]()
# Usage
factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
print(dog.speak()) # Woof!
print(cat.speak()) # Meow!
When to use: When you need to create objects based on some condition, when the creation logic is complex, or when you want to decouple object creation from usage.
Builder Pattern
Constructs complex objects step by step, separating construction from representation.
class Pizza:
def __init__(self):
self.size = None
self.cheese = False
self.pepperoni = False
self.mushrooms = False
self.onions = False
def __str__(self):
toppings = []
if self.cheese: toppings.append("cheese")
if self.pepperoni: toppings.append("pepperoni")
if self.mushrooms: toppings.append("mushrooms")
if self.onions: toppings.append("onions")
return f"{self.size} pizza with {', '.join(toppings)}"
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_size(self, size: str) -> 'PizzaBuilder':
self.pizza.size = size
return self # Enable method chaining
def add_cheese(self) -> 'PizzaBuilder':
self.pizza.cheese = True
return self
def add_pepperoni(self) -> 'PizzaBuilder':
self.pizza.pepperoni = True
return self
def add_mushrooms(self) -> 'PizzaBuilder':
self.pizza.mushrooms = True
return self
def add_onions(self) -> 'PizzaBuilder':
self.pizza.onions = True
return self
def build(self) -> Pizza:
return self.pizza
# Usage - fluent interface
pizza = (PizzaBuilder()
.set_size("large")
.add_cheese()
.add_pepperoni()
.add_mushrooms()
.build())
print(pizza) # large pizza with cheese, pepperoni, mushrooms
When to use: When creating complex objects with many optional parameters, when you want to create different representations of an object.
🔗 Structural Patterns
Patterns that deal with object composition and relationships.
Adapter Pattern
Allows incompatible interfaces to work together by wrapping an object.
# Old interface (third-party library)
class OldPaymentSystem:
def make_payment(self, amount_in_cents: int) -> bool:
print(f"Processing ${amount_in_cents / 100:.2f}")
return True
# New interface we want
class PaymentProcessor:
def pay(self, amount_dollars: float) -> bool:
raise NotImplementedError
# Adapter
class PaymentAdapter(PaymentProcessor):
def __init__(self, old_system: OldPaymentSystem):
self.old_system = old_system
def pay(self, amount_dollars: float) -> bool:
# Convert dollars to cents
amount_cents = int(amount_dollars * 100)
return self.old_system.make_payment(amount_cents)
# Usage
old_system = OldPaymentSystem()
adapter = PaymentAdapter(old_system)
adapter.pay(19.99) # Processing $19.99
When to use: When you need to use an existing class but its interface doesn't match what you need, when integrating third-party libraries.
Decorator Pattern
Adds behavior to objects dynamically without modifying their class.
from abc import ABC, abstractmethod
# Component interface
class Coffee(ABC):
@abstractmethod
def cost(self) -> float:
pass
@abstractmethod
def description(self) -> str:
pass
# Concrete component
class SimpleCoffee(Coffee):
def cost(self) -> float:
return 2.00
def description(self) -> str:
return "Simple coffee"
# Decorator base class
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
# Concrete decorators
class Milk(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.50
def description(self) -> str:
return self._coffee.description() + ", milk"
class Sugar(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.25
def description(self) -> str:
return self._coffee.description() + ", sugar"
class Whip(CoffeeDecorator):
def cost(self) -> float:
return self._coffee.cost() + 0.75
def description(self) -> str:
return self._coffee.description() + ", whipped cream"
# Usage - stack decorators
coffee = SimpleCoffee()
coffee = Milk(coffee)
coffee = Sugar(coffee)
coffee = Whip(coffee)
print(f"{coffee.description()}: ${coffee.cost():.2f}")
# Simple coffee, milk, sugar, whipped cream: $3.50
# Note: Python has built-in decorator syntax (@decorator)
# which serves a similar purpose for functions!
When to use: When you need to add responsibilities to objects dynamically, when extension by subclassing is impractical.
Facade Pattern
Provides a simplified interface to a complex subsystem.
# Complex subsystem classes
class VideoFile:
def __init__(self, filename: str):
self.filename = filename
class CodecFactory:
def extract(self, file: VideoFile) -> str:
return f"codec for {file.filename}"
class AudioMixer:
def fix(self, audio) -> str:
return f"fixed {audio}"
class VideoCompressor:
def compress(self, video, codec) -> str:
return f"compressed {video} with {codec}"
# Facade - simple interface
class VideoConverter:
"""Simplified interface to the video conversion subsystem."""
def __init__(self):
self._codec_factory = CodecFactory()
self._audio_mixer = AudioMixer()
self._compressor = VideoCompressor()
def convert(self, filename: str, format: str) -> str:
"""Simple method that hides all complexity."""
file = VideoFile(filename)
codec = self._codec_factory.extract(file)
audio = self._audio_mixer.fix("audio track")
result = self._compressor.compress(filename, codec)
return f"Converted {filename} to {format}: {result}"
# Usage - simple!
converter = VideoConverter()
result = converter.convert("video.mp4", "avi")
print(result)
When to use: When you need to provide a simple interface to a complex subsystem, when you want to layer your subsystems.
🎭 Behavioral Patterns
Patterns that deal with object interaction and responsibility.
Observer Pattern
Defines a one-to-many dependency where when one object changes state, all dependents are notified.
from abc import ABC, abstractmethod
from typing import List
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, message: str) -> None:
pass
# Subject (Observable)
class NewsPublisher:
def __init__(self):
self._observers: List[Observer] = []
self._latest_news: str = ""
def subscribe(self, observer: Observer) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify_all(self) -> None:
for observer in self._observers:
observer.update(self._latest_news)
def publish_news(self, news: str) -> None:
self._latest_news = news
print(f"Published: {news}")
self.notify_all()
# Concrete observers
class EmailSubscriber(Observer):
def __init__(self, email: str):
self.email = email
def update(self, message: str) -> None:
print(f"Email to {self.email}: {message}")
class SMSSubscriber(Observer):
def __init__(self, phone: str):
self.phone = phone
def update(self, message: str) -> None:
print(f"SMS to {self.phone}: {message}")
# Usage
publisher = NewsPublisher()
email_sub = EmailSubscriber("user@example.com")
sms_sub = SMSSubscriber("+1234567890")
publisher.subscribe(email_sub)
publisher.subscribe(sms_sub)
publisher.publish_news("Breaking: Python 4.0 Released!")
# All subscribers receive notification
When to use: Event handling systems, implementing distributed event handling, when an abstraction has two aspects dependent on each other.
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
from abc import ABC, abstractmethod
from typing import List
# Strategy interface
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: List[int]) -> List[int]:
pass
# Concrete strategies
class BubbleSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
data = data.copy()
n = len(data)
for i in range(n):
for j in range(0, n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
print("Using Bubble Sort")
return data
class QuickSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
print("Using Quick Sort")
return self.sort(left) + middle + self.sort(right)
# Context
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def sort(self, data: List[int]) -> List[int]:
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSort())
print(sorter.sort(data))
sorter.set_strategy(QuickSort())
print(sorter.sort(data))
# Pythonic way - just pass functions!
def sort_data(data: List[int], algorithm) -> List[int]:
return algorithm(data)
result = sort_data(data, sorted) # Use built-in
When to use: When you have multiple algorithms for a task and want to switch between them, when you want to avoid conditional statements for selecting algorithms.
Command Pattern
Encapsulates a request as an object, allowing parameterization and queuing.
from abc import ABC, abstractmethod
from typing import List
# Command interface
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# Receiver
class Light:
def __init__(self, location: str):
self.location = location
self.is_on = False
def turn_on(self) -> None:
self.is_on = True
print(f"{self.location} light is ON")
def turn_off(self) -> None:
self.is_on = False
print(f"{self.location} light is OFF")
# Concrete commands
class LightOnCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self) -> None:
self.light.turn_on()
def undo(self) -> None:
self.light.turn_off()
class LightOffCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self) -> None:
self.light.turn_off()
def undo(self) -> None:
self.light.turn_on()
# Invoker with history
class RemoteControl:
def __init__(self):
self.history: List[Command] = []
def execute_command(self, command: Command) -> None:
command.execute()
self.history.append(command)
def undo_last(self) -> None:
if self.history:
command = self.history.pop()
command.undo()
else:
print("Nothing to undo")
# Usage
living_room = Light("Living Room")
living_on = LightOnCommand(living_room)
living_off = LightOffCommand(living_room)
remote = RemoteControl()
remote.execute_command(living_on) # Living Room light is ON
remote.execute_command(living_off) # Living Room light is OFF
remote.undo_last() # Living Room light is ON
When to use: Implementing undo/redo, queuing requests, logging operations, supporting transactions.
🐍 Python-Specific Notes
Patterns Built into Python
# Iterator Pattern - built-in
for item in my_list:
pass
# Decorator Pattern - @ syntax
@my_decorator
def my_function():
pass
# Singleton - modules are singletons
# Just put shared state in a module
# Factory - dictionary of callables
factories = {
"type1": Type1,
"type2": Type2,
}
obj = factories["type1"]()
# Strategy - first-class functions
def process(data, algorithm):
return algorithm(data)
process(data, sorted)
process(data, reversed)
When NOT to Use Patterns
- •Don't force patterns - Use when they solve a real problem
- •Keep it simple - Python's dynamic nature often makes patterns unnecessary
- •Premature abstraction - Add patterns when complexity demands it
- •Over-engineering - Sometimes a simple function is enough
📋 Pattern Summary
| Pattern | Purpose | Python Alternative |
|---|---|---|
| Singleton | Single instance | Module-level variable |
| Factory | Object creation | Dict of classes/functions |
| Builder | Complex object construction | Named parameters, dataclass |
| Adapter | Interface compatibility | Duck typing often enough |
| Decorator | Add behavior | @decorator syntax |
| Facade | Simplify complex systems | Module/class API |
| Observer | Event notification | Callbacks, signals |
| Strategy | Interchangeable algorithms | First-class functions |
| Command | Encapsulate actions | Closures, callables |
🎯 Next Steps
After understanding design patterns, proceed to 25_type_hints to learn about adding static typing to Python for better code quality!