Docs

README

🏗️ 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?

  1. Common vocabulary - "Let's use a Factory here" is clearer than explaining the entire concept
  2. Proven solutions - These patterns have been tested by thousands of developers
  3. Better architecture - Leads to more maintainable, flexible code
  4. 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

  1. Don't force patterns - Use when they solve a real problem
  2. Keep it simple - Python's dynamic nature often makes patterns unnecessary
  3. Premature abstraction - Add patterns when complexity demands it
  4. Over-engineering - Sometimes a simple function is enough

📋 Pattern Summary

PatternPurposePython Alternative
SingletonSingle instanceModule-level variable
FactoryObject creationDict of classes/functions
BuilderComplex object constructionNamed parameters, dataclass
AdapterInterface compatibilityDuck typing often enough
DecoratorAdd behavior@decorator syntax
FacadeSimplify complex systemsModule/class API
ObserverEvent notificationCallbacks, signals
StrategyInterchangeable algorithmsFirst-class functions
CommandEncapsulate actionsClosures, callables

🎯 Next Steps

After understanding design patterns, proceed to 25_type_hints to learn about adding static typing to Python for better code quality!

README - Python Tutorial | DeepML