- Introduction to Design Patterns
- Understanding Pattern Categories
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
- Advanced Pattern Combinations
- Real-World Applications
- Best Practices
- Common Pitfalls
- Exercises and Practice
Design Patterns are proven solutions to recurring problems in software design. They represent best practices evolved over time by experienced developers and provide a common vocabulary for discussing design solutions.
🎯 Key Benefits:
- Proven Solutions: Time-tested approaches to common problems
- Communication: Common vocabulary for developers
- Reusability: Solutions that can be applied across different contexts
- Maintainability: Well-structured, understandable code
- Flexibility: Designs that can adapt to changing requirements
Think of design patterns like architectural blueprints:
- Blueprints provide proven solutions for building structures
- Patterns provide proven solutions for software structures
- Both can be adapted to specific needs
- Both promote consistency and quality
- Both help communicate complex ideas simply
The most famous design patterns come from the book "Design Patterns: Elements of Reusable Object-Oriented Software" by the Gang of Four (Gamma, Helm, Johnson, and Vlissides). They identified 23 fundamental patterns.
Design patterns are typically organized into three main categories:
Purpose: Deal with object creation mechanisms Focus: How objects are created, composed, and represented Examples: Singleton, Factory, Builder, Prototype
# Example: Factory Pattern
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError("Unknown animal type")Purpose: Deal with object composition and relationships Focus: How classes and objects are composed to form larger structures Examples: Adapter, Decorator, Facade, Proxy
# Example: Decorator Pattern
class Coffee:
def cost(self):
return 5
class MilkDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 2Purpose: Deal with communication between objects and assignment of responsibilities Focus: How objects interact and distribute responsibilities Examples: Observer, Strategy, Command, State
# Example: Observer Pattern
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self, message):
for observer in self._observers:
observer.update(message)| Category | Purpose | When to Use | Common Examples |
|---|---|---|---|
| Creational | Object creation | Need flexible object creation | Singleton, Factory |
| Structural | Object composition | Need to organize classes/objects | Adapter, Decorator |
| Behavioral | Object interaction | Need to define communication | Observer, Strategy |
Creational patterns abstract the instantiation process and help make systems independent of how objects are created, composed, and represented.
Intent: Ensure a class has only one instance and provide global access to it.
When to Use:
- Need exactly one instance (database connection, logger)
- Global access point required
- Instance creation is expensive
class Singleton:
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._initialized:
self.value = 0
self._initialized = True
def increment(self):
self.value += 1
return self.value
# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True - same instance
s1.increment()
print(s2.value) # 1 - shared stateIntent: Create objects without specifying their exact classes.
When to Use:
- Object creation logic is complex
- Need to decouple object creation from usage
- Want to centralize object creation
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class Bird(Animal):
def make_sound(self):
return "Tweet!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
animals = {
'dog': Dog,
'cat': Cat,
'bird': Bird
}
animal_class = animals.get(animal_type.lower())
if animal_class:
return animal_class()
else:
raise ValueError(f"Unknown animal type: {animal_type}")
# Usage
factory = AnimalFactory()
dog = factory.create_animal("dog")
cat = factory.create_animal("cat")
print(dog.make_sound()) # Woof!
print(cat.make_sound()) # Meow!Intent: Construct complex objects step by step.
When to Use:
- Object construction is complex
- Want to create different representations
- Need to construct objects with many optional parameters
class Computer:
def __init__(self):
self.cpu = None
self.memory = None
self.storage = None
self.graphics = None
def __str__(self):
return f"Computer(CPU: {self.cpu}, Memory: {self.memory}, Storage: {self.storage}, Graphics: {self.graphics})"
class ComputerBuilder:
def __init__(self):
self.computer = Computer()
def set_cpu(self, cpu):
self.computer.cpu = cpu
return self # Return self for method chaining
def set_memory(self, memory):
self.computer.memory = memory
return self
def set_storage(self, storage):
self.computer.storage = storage
return self
def set_graphics(self, graphics):
self.computer.graphics = graphics
return self
def build(self):
return self.computer
class ComputerDirector:
@staticmethod
def build_gaming_computer():
return (ComputerBuilder()
.set_cpu("Intel i9")
.set_memory("32GB DDR4")
.set_storage("1TB NVMe SSD")
.set_graphics("RTX 4080")
.build())
@staticmethod
def build_office_computer():
return (ComputerBuilder()
.set_cpu("Intel i5")
.set_memory("16GB DDR4")
.set_storage("512GB SSD")
.set_graphics("Integrated")
.build())
# Usage
gaming_pc = ComputerDirector.build_gaming_computer()
office_pc = ComputerDirector.build_office_computer()
print(gaming_pc)
print(office_pc)Intent: Create objects by cloning existing instances.
When to Use:
- Object creation is expensive
- Need to create objects similar to existing ones
- Want to avoid subclasses of creator
import copy
from abc import ABC, abstractmethod
class Prototype(ABC):
@abstractmethod
def clone(self):
pass
class Document(Prototype):
def __init__(self, title, content, formatting):
self.title = title
self.content = content
self.formatting = formatting
def clone(self):
# Deep copy to ensure complete independence
return copy.deepcopy(self)
def __str__(self):
return f"Document(title='{self.title}', content='{self.content[:20]}...', formatting={self.formatting})"
class DocumentManager:
def __init__(self):
self._prototypes = {}
def register_prototype(self, name, prototype):
self._prototypes[name] = prototype
def create_document(self, prototype_name, title, content):
if prototype_name in self._prototypes:
# Clone the prototype and customize
document = self._prototypes[prototype_name].clone()
document.title = title
document.content = content
return document
else:
raise ValueError(f"Unknown prototype: {prototype_name}")
# Usage
manager = DocumentManager()
# Register prototypes
report_template = Document("Template", "", {"font": "Arial", "size": 12, "margins": "1in"})
letter_template = Document("Template", "", {"font": "Times", "size": 11, "margins": "1.5in"})
manager.register_prototype("report", report_template)
manager.register_prototype("letter", letter_template)
# Create documents from prototypes
quarterly_report = manager.create_document("report", "Q1 Report", "Sales increased by 15%...")
cover_letter = manager.create_document("letter", "Cover Letter", "Dear Hiring Manager...")
print(quarterly_report)
print(cover_letter)Structural patterns deal with object composition, helping to ensure that if one part changes, the entire structure doesn't need to change.
Intent: Allow incompatible interfaces to work together.
When to Use:
- Need to use existing class with incompatible interface
- Want to create reusable class that cooperates with unrelated classes
- Need to use several existing subclasses but can't adapt their interface
# Target interface that client expects
class MediaPlayer:
def play(self, audio_type, filename):
pass
# Existing classes with different interfaces
class Mp3Player:
def play_mp3(self, filename):
print(f"Playing MP3 file: {filename}")
class Mp4Player:
def play_mp4(self, filename):
print(f"Playing MP4 file: {filename}")
class VlcPlayer:
def play_vlc(self, filename):
print(f"Playing VLC file: {filename}")
# Adapter to make incompatible interfaces work
class MediaAdapter:
def __init__(self, audio_type):
if audio_type == "mp4":
self.player = Mp4Player()
elif audio_type == "vlc":
self.player = VlcPlayer()
def play(self, audio_type, filename):
if audio_type == "mp4":
self.player.play_mp4(filename)
elif audio_type == "vlc":
self.player.play_vlc(filename)
# Client class that uses the adapter
class AudioPlayer(MediaPlayer):
def play(self, audio_type, filename):
if audio_type == "mp3":
# Built-in support for MP3
mp3_player = Mp3Player()
mp3_player.play_mp3(filename)
elif audio_type in ["mp4", "vlc"]:
# Use adapter for other formats
adapter = MediaAdapter(audio_type)
adapter.play(audio_type, filename)
else:
print(f"Invalid media. {audio_type} format not supported")
# Usage
player = AudioPlayer()
player.play("mp3", "song.mp3")
player.play("mp4", "video.mp4")
player.play("vlc", "movie.vlc")
player.play("avi", "video.avi") # Not supportedIntent: Add new functionality to objects dynamically without altering their structure.
When to Use:
- Want to add responsibilities to objects dynamically
- Extension by subclassing is impractical
- Need to add functionality that can be withdrawn
from abc import ABC, abstractmethod
# Component interface
class Coffee(ABC):
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
# Concrete component
class SimpleCoffee(Coffee):
def cost(self):
return 5.0
def description(self):
return "Simple coffee"
# Base decorator
class CoffeeDecorator(Coffee):
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost()
def description(self):
return self._coffee.description()
# Concrete decorators
class MilkDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 2.0
def description(self):
return self._coffee.description() + ", milk"
class SugarDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 1.0
def description(self):
return self._coffee.description() + ", sugar"
class WhipDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 3.0
def description(self):
return self._coffee.description() + ", whipped cream"
class VanillaDecorator(CoffeeDecorator):
def cost(self):
return self._coffee.cost() + 1.5
def description(self):
return self._coffee.description() + ", vanilla"
# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost()}")
# Add milk
coffee = MilkDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Add sugar
coffee = SugarDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Add whipped cream
coffee = WhipDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Create a complex coffee in one go
fancy_coffee = VanillaDecorator(
WhipDecorator(
SugarDecorator(
MilkDecorator(
SimpleCoffee()
)
)
)
)
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost()}")Intent: Provide a unified interface to a set of interfaces in a subsystem.
When to Use:
- Want to provide simple interface to complex subsystem
- Need to decouple clients from subsystem components
- Want to layer your subsystems
# Complex subsystem classes
class CPU:
def freeze(self):
print("CPU: Freezing processor")
def jump(self, position):
print(f"CPU: Jumping to position {position}")
def execute(self):
print("CPU: Executing instructions")
class Memory:
def load(self, position, data):
print(f"Memory: Loading data '{data}' at position {position}")
class HardDrive:
def read(self, lba, size):
print(f"HardDrive: Reading {size} bytes from LBA {lba}")
return f"Boot data from sector {lba}"
class GPU:
def render(self):
print("GPU: Rendering graphics")
class SoundCard:
def play_sound(self, sound):
print(f"SoundCard: Playing {sound}")
# Facade class
class ComputerFacade:
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.hard_drive = HardDrive()
self.gpu = GPU()
self.sound_card = SoundCard()
def start_computer(self):
print("=== Starting Computer ===")
self.cpu.freeze()
boot_data = self.hard_drive.read(0, 1024)
self.memory.load(0, boot_data)
self.cpu.jump(0)
self.cpu.execute()
self.gpu.render()
self.sound_card.play_sound("startup.wav")
print("=== Computer Started Successfully ===")
def shutdown_computer(self):
print("=== Shutting Down Computer ===")
self.sound_card.play_sound("shutdown.wav")
print("=== Computer Shut Down ===")
def restart_computer(self):
print("=== Restarting Computer ===")
self.shutdown_computer()
self.start_computer()
# Usage
computer = ComputerFacade()
computer.start_computer()
print()
computer.restart_computer()
print()
computer.shutdown_computer()Intent: Provide a placeholder or surrogate for another object to control access to it.
When to Use:
- Need lazy initialization (virtual proxy)
- Need access control (protection proxy)
- Need local representation of remote object (remote proxy)
- Need to add functionality when accessing object
from abc import ABC, abstractmethod
import time
# Subject interface
class Image(ABC):
@abstractmethod
def display(self):
pass
# Real subject
class RealImage(Image):
def __init__(self, filename):
self.filename = filename
self._load_from_disk()
def _load_from_disk(self):
print(f"Loading image from disk: {self.filename}")
# Simulate expensive loading operation
time.sleep(1)
def display(self):
print(f"Displaying image: {self.filename}")
# Proxy
class ImageProxy(Image):
def __init__(self, filename):
self.filename = filename
self._real_image = None
def display(self):
if self._real_image is None:
# Lazy loading - create real object only when needed
self._real_image = RealImage(self.filename)
self._real_image.display()
# Protection Proxy
class ProtectedImageProxy(Image):
def __init__(self, filename, user_role):
self.filename = filename
self.user_role = user_role
self._real_image = None
def display(self):
if self._has_access():
if self._real_image is None:
self._real_image = RealImage(self.filename)
self._real_image.display()
else:
print(f"Access denied: {self.user_role} cannot view {self.filename}")
def _has_access(self):
# Simple access control logic
restricted_files = ["classified.jpg", "secret.png"]
if self.filename in restricted_files:
return self.user_role == "admin"
return True
# Caching Proxy
class CachingImageProxy(Image):
_cache = {}
def __init__(self, filename):
self.filename = filename
def display(self):
if self.filename not in self._cache:
print(f"Cache miss for {self.filename}")
self._cache[self.filename] = RealImage(self.filename)
else:
print(f"Cache hit for {self.filename}")
self._cache[self.filename].display()
# Usage
print("=== Virtual Proxy (Lazy Loading) ===")
proxy_image = ImageProxy("photo.jpg")
print("Proxy created, but image not loaded yet")
proxy_image.display() # Now image is loaded
proxy_image.display() # Uses already loaded image
print("\n=== Protection Proxy ===")
admin_image = ProtectedImageProxy("classified.jpg", "admin")
user_image = ProtectedImageProxy("classified.jpg", "user")
admin_image.display() # Access granted
user_image.display() # Access denied
print("\n=== Caching Proxy ===")
cache_proxy1 = CachingImageProxy("cached_photo.jpg")
cache_proxy2 = CachingImageProxy("cached_photo.jpg")
cache_proxy1.display() # Cache miss - loads image
cache_proxy2.display() # Cache hit - uses cached imageBehavioral patterns focus on communication between objects and the assignment of responsibilities.
Intent: Define a one-to-many dependency between objects so that when one object changes state, all dependents are notified.
When to Use:
- Need to notify multiple objects about state changes
- Want loose coupling between subject and observers
- Need to add/remove observers dynamically
from abc import ABC, abstractmethod
from typing import List
# Observer interface
class Observer(ABC):
@abstractmethod
def update(self, subject):
pass
# Subject interface
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer):
pass
@abstractmethod
def detach(self, observer: Observer):
pass
@abstractmethod
def notify(self):
pass
# Concrete subject
class WeatherStation(Subject):
def __init__(self):
self._observers: List[Observer] = []
self._temperature = 0
self._humidity = 0
self._pressure = 0
def attach(self, observer: Observer):
if observer not in self._observers:
self._observers.append(observer)
print(f"Observer {observer.__class__.__name__} attached")
def detach(self, observer: Observer):
if observer in self._observers:
self._observers.remove(observer)
print(f"Observer {observer.__class__.__name__} detached")
def notify(self):
print("Notifying all observers...")
for observer in self._observers:
observer.update(self)
def set_measurements(self, temperature, humidity, pressure):
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.notify()
@property
def temperature(self):
return self._temperature
@property
def humidity(self):
return self._humidity
@property
def pressure(self):
return self._pressure
# Concrete observers
class CurrentConditionsDisplay(Observer):
def update(self, subject: WeatherStation):
print(f"Current Conditions: {subject.temperature}°F, {subject.humidity}% humidity")
class StatisticsDisplay(Observer):
def __init__(self):
self._temperatures = []
def update(self, subject: WeatherStation):
self._temperatures.append(subject.temperature)
avg_temp = sum(self._temperatures) / len(self._temperatures)
print(f"Statistics: Avg temp: {avg_temp:.1f}°F, Min: {min(self._temperatures)}°F, Max: {max(self._temperatures)}°F")
class ForecastDisplay(Observer):
def update(self, subject: WeatherStation):
if subject.pressure > 30.20:
forecast = "Improving weather on the way!"
elif subject.pressure < 29.20:
forecast = "Watch out for cooler, rainy weather"
else:
forecast = "More of the same"
print(f"Forecast: {forecast}")
# Usage
weather_station = WeatherStation()
# Create and attach observers
current_display = CurrentConditionsDisplay()
statistics_display = StatisticsDisplay()
forecast_display = ForecastDisplay()
weather_station.attach(current_display)
weather_station.attach(statistics_display)
weather_station.attach(forecast_display)
# Update weather data
print("=== Weather Update 1 ===")
weather_station.set_measurements(80, 65, 30.4)
print("\n=== Weather Update 2 ===")
weather_station.set_measurements(82, 70, 29.2)
print("\n=== Detaching Statistics Display ===")
weather_station.detach(statistics_display)
print("\n=== Weather Update 3 ===")
weather_station.set_measurements(78, 90, 29.2)Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
When to Use:
- Have multiple ways to perform a task
- Want to switch algorithms at runtime
- Need to eliminate conditional statements
from abc import ABC, abstractmethod
# Strategy interface
class SortingStrategy(ABC):
@abstractmethod
def sort(self, data):
pass
@abstractmethod
def get_name(self):
pass
# Concrete strategies
class BubbleSort(SortingStrategy):
def sort(self, data):
arr = data.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def get_name(self):
return "Bubble Sort"
class QuickSort(SortingStrategy):
def sort(self, data):
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]
return self.sort(left) + middle + self.sort(right)
def get_name(self):
return "Quick Sort"
class MergeSort(SortingStrategy):
def sort(self, data):
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def get_name(self):
return "Merge Sort"
# Context class
class Sorter:
def __init__(self, strategy: SortingStrategy = None):
self._strategy = strategy
def set_strategy(self, strategy: SortingStrategy):
self._strategy = strategy
def sort_data(self, data):
if self._strategy is None:
raise ValueError("No sorting strategy set")
print(f"Using {self._strategy.get_name()}")
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
print(f"Original data: {data}")
sorter = Sorter()
# Use different strategies
strategies = [BubbleSort(), QuickSort(), MergeSort()]
for strategy in strategies:
sorter.set_strategy(strategy)
sorted_data = sorter.sort_data(data)
print(f"Sorted with {strategy.get_name()}: {sorted_data}")
print()Intent: Encapsulate a request as an object, allowing you to parameterize clients with different requests, queue operations, and support undo.
When to Use:
- Need to parameterize objects with operations
- Want to queue, log, or support undo operations
- Need to support macro recording
from abc import ABC, abstractmethod
# Command interface
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Receiver classes
class Light:
def __init__(self, location):
self.location = location
self.is_on = False
def turn_on(self):
self.is_on = True
print(f"{self.location} light is ON")
def turn_off(self):
self.is_on = False
print(f"{self.location} light is OFF")
class Fan:
def __init__(self, location):
self.location = location
self.speed = 0 # 0=off, 1=low, 2=medium, 3=high
def turn_on(self, speed=1):
self.speed = speed
speeds = ["OFF", "LOW", "MEDIUM", "HIGH"]
print(f"{self.location} fan is {speeds[speed]}")
def turn_off(self):
self.speed = 0
print(f"{self.location} fan is OFF")
# Concrete commands
class LightOnCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_on()
def undo(self):
self.light.turn_off()
class LightOffCommand(Command):
def __init__(self, light):
self.light = light
def execute(self):
self.light.turn_off()
def undo(self):
self.light.turn_on()
class FanOnCommand(Command):
def __init__(self, fan, speed=1):
self.fan = fan
self.speed = speed
self.previous_speed = 0
def execute(self):
self.previous_speed = self.fan.speed
self.fan.turn_on(self.speed)
def undo(self):
if self.previous_speed == 0:
self.fan.turn_off()
else:
self.fan.turn_on(self.previous_speed)
class FanOffCommand(Command):
def __init__(self, fan):
self.fan = fan
self.previous_speed = 0
def execute(self):
self.previous_speed = self.fan.speed
self.fan.turn_off()
def undo(self):
if self.previous_speed > 0:
self.fan.turn_on(self.previous_speed)
# Null Object pattern for empty slots
class NoCommand(Command):
def execute(self):
pass
def undo(self):
pass
# Macro command
class MacroCommand(Command):
def __init__(self, commands):
self.commands = commands
def execute(self):
for command in self.commands:
command.execute()
def undo(self):
# Undo in reverse order
for command in reversed(self.commands):
command.undo()
# Invoker
class RemoteControl:
def __init__(self):
self.on_commands = [NoCommand()] * 7
self.off_commands = [NoCommand()] * 7
self.undo_command = NoCommand()
def set_command(self, slot, on_command, off_command):
self.on_commands[slot] = on_command
self.off_commands[slot] = off_command
def on_button_pressed(self, slot):
self.on_commands[slot].execute()
self.undo_command = self.on_commands[slot]
def off_button_pressed(self, slot):
self.off_commands[slot].execute()
self.undo_command = self.off_commands[slot]
def undo_button_pressed(self):
self.undo_command.undo()
# Usage
remote = RemoteControl()
# Create devices
living_room_light = Light("Living Room")
kitchen_light = Light("Kitchen")
bedroom_fan = Fan("Bedroom")
# Create commands
living_room_light_on = LightOnCommand(living_room_light)
living_room_light_off = LightOffCommand(living_room_light)
kitchen_light_on = LightOnCommand(kitchen_light)
kitchen_light_off = LightOffCommand(kitchen_light)
bedroom_fan_on = FanOnCommand(bedroom_fan, 2)
bedroom_fan_off = FanOffCommand(bedroom_fan)
# Set up remote
remote.set_command(0, living_room_light_on, living_room_light_off)
remote.set_command(1, kitchen_light_on, kitchen_light_off)
remote.set_command(2, bedroom_fan_on, bedroom_fan_off)
# Create macro command for "party mode"
party_on = MacroCommand([living_room_light_on, kitchen_light_on, bedroom_fan_on])
party_off = MacroCommand([living_room_light_off, kitchen_light_off, bedroom_fan_off])
remote.set_command(6, party_on, party_off)
# Test the remote
print("=== Testing Remote Control ===")
remote.on_button_pressed(0) # Living room light on
remote.off_button_pressed(0) # Living room light off
remote.undo_button_pressed() # Undo (light back on)
print("\n=== Party Mode ===")
remote.on_button_pressed(6) # Party mode on
remote.undo_button_pressed() # Undo party modeReal-world applications often combine multiple design patterns to solve complex problems.
Combines Observer, Strategy, and Composite patterns:
from abc import ABC, abstractmethod
from typing import List
# Model (Subject in Observer pattern)
class Model:
def __init__(self):
self._observers: List['Observer'] = []
self._data = {}
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)
def set_data(self, key, value):
self._data[key] = value
self.notify()
def get_data(self, key):
return self._data.get(key)
def get_all_data(self):
return self._data.copy()
# View (Observer pattern)
class View(ABC):
@abstractmethod
def update(self, model):
pass
@abstractmethod
def render(self):
pass
class ConsoleView(View):
def __init__(self, name):
self.name = name
self.model_data = {}
def update(self, model):
self.model_data = model.get_all_data()
self.render()
def render(self):
print(f"=== {self.name} View ===")
for key, value in self.model_data.items():
print(f"{key}: {value}")
print()
class WebView(View):
def __init__(self):
self.model_data = {}
def update(self, model):
self.model_data = model.get_all_data()
self.render()
def render(self):
print("=== Web View (HTML) ===")
print("<html><body>")
for key, value in self.model_data.items():
print(f"<p><strong>{key}:</strong> {value}</p>")
print("</body></html>")
print()
# Controller (Strategy pattern for different actions)
class Controller:
def __init__(self, model):
self.model = model
def update_user_name(self, name):
self.model.set_data("user_name", name)
def update_user_email(self, email):
self.model.set_data("user_email", email)
def update_user_age(self, age):
self.model.set_data("user_age", age)
# Usage
model = Model()
console_view = ConsoleView("Console")
web_view = WebView()
model.attach(console_view)
model.attach(web_view)
controller = Controller(model)
# Update data through controller
controller.update_user_name("Alice")
controller.update_user_email("alice@example.com")
controller.update_user_age(30)Combines Factory, Strategy, Observer, and Registry patterns:
from abc import ABC, abstractmethod
from typing import Dict, List, Any
# Plugin interface (Strategy pattern)
class Plugin(ABC):
@property
@abstractmethod
def name(self) -> str:
pass
@abstractmethod
def execute(self, data: Any) -> Any:
pass
# Plugin Registry (Registry pattern)
class PluginRegistry:
_plugins: Dict[str, type] = {}
@classmethod
def register(cls, plugin_class):
"""Decorator to register plugins"""
cls._plugins[plugin_class.__name__] = plugin_class
return plugin_class
@classmethod
def get_plugin(cls, name: str):
return cls._plugins.get(name)
@classmethod
def list_plugins(cls):
return list(cls._plugins.keys())
# Plugin Factory (Factory pattern)
class PluginFactory:
@staticmethod
def create_plugin(plugin_name: str, **kwargs):
plugin_class = PluginRegistry.get_plugin(plugin_name)
if plugin_class:
return plugin_class(**kwargs)
raise ValueError(f"Unknown plugin: {plugin_name}")
# Event system (Observer pattern)
class EventManager:
def __init__(self):
self._listeners: Dict[str, List[callable]] = {}
def subscribe(self, event_name: str, callback: callable):
if event_name not in self._listeners:
self._listeners[event_name] = []
self._listeners[event_name].append(callback)
def emit(self, event_name: str, data: Any = None):
if event_name in self._listeners:
for callback in self._listeners[event_name]:
callback(data)
# Plugin Manager (Facade pattern)
class PluginManager:
def __init__(self):
self.event_manager = EventManager()
self.loaded_plugins: Dict[str, Plugin] = {}
def load_plugin(self, plugin_name: str, **kwargs):
try:
plugin = PluginFactory.create_plugin(plugin_name, **kwargs)
self.loaded_plugins[plugin.name] = plugin
self.event_manager.emit("plugin_loaded", plugin.name)
return plugin
except ValueError as e:
self.event_manager.emit("plugin_error", str(e))
raise
def execute_plugin(self, plugin_name: str, data: Any):
if plugin_name in self.loaded_plugins:
result = self.loaded_plugins[plugin_name].execute(data)
self.event_manager.emit("plugin_executed", {
"plugin": plugin_name,
"result": result
})
return result
raise ValueError(f"Plugin {plugin_name} not loaded")
# Concrete plugins
@PluginRegistry.register
class DataValidatorPlugin(Plugin):
def __init__(self, rules=None):
self.rules = rules or []
@property
def name(self):
return "DataValidator"
def execute(self, data):
print(f"Validating data with {len(self.rules)} rules")
# Simulate validation
return {"valid": True, "data": data}
@PluginRegistry.register
class DataTransformerPlugin(Plugin):
def __init__(self, transformation_type="uppercase"):
self.transformation_type = transformation_type
@property
def name(self):
return "DataTransformer"
def execute(self, data):
if self.transformation_type == "uppercase" and isinstance(data, str):
return data.upper()
return data
@PluginRegistry.register
class DataLoggerPlugin(Plugin):
@property
def name(self):
return "DataLogger"
def execute(self, data):
print(f"Logging data: {data}")
return data
# Usage
manager = PluginManager()
# Subscribe to events
manager.event_manager.subscribe("plugin_loaded",
lambda name: print(f"Plugin loaded: {name}"))
manager.event_manager.subscribe("plugin_executed",
lambda data: print(f"Plugin executed: {data['plugin']} -> {data['result']}"))
# Load and use plugins
print("Available plugins:", PluginRegistry.list_plugins())
validator = manager.load_plugin("DataValidatorPlugin", rules=["required", "email"])
transformer = manager.load_plugin("DataTransformerPlugin", transformation_type="uppercase")
logger = manager.load_plugin("DataLoggerPlugin")
# Execute plugins
test_data = "hello world"
result1 = manager.execute_plugin("DataValidator", test_data)
result2 = manager.execute_plugin("DataTransformer", test_data)
result3 = manager.execute_plugin("DataLogger", result2)from abc import ABC, abstractmethod
from enum import Enum
from typing import List, Dict
# State pattern for order states
class OrderState(Enum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self.state = OrderState.PENDING
self.items = []
self.total = 0.0
def add_item(self, item, quantity):
self.items.append({"item": item, "quantity": quantity})
self.total += item.price * quantity
def confirm(self):
if self.state == OrderState.PENDING:
self.state = OrderState.CONFIRMED
return True
return False
def ship(self):
if self.state == OrderState.CONFIRMED:
self.state = OrderState.SHIPPED
return True
return False
def deliver(self):
if self.state == OrderState.SHIPPED:
self.state = OrderState.DELIVERED
return True
return False
# Strategy pattern for payment methods
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> bool:
pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def pay(self, amount: float) -> bool:
print(f"Paid ${amount} using Credit Card ending in {self.card_number[-4:]}")
return True
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> bool:
print(f"Paid ${amount} using PayPal account {self.email}")
return True
class BankTransferPayment(PaymentStrategy):
def __init__(self, account_number: str):
self.account_number = account_number
def pay(self, amount: float) -> bool:
print(f"Paid ${amount} using Bank Transfer from account {self.account_number}")
return True
# Observer pattern for notifications
class OrderObserver(ABC):
@abstractmethod
def notify(self, order: Order, event: str):
pass
class EmailNotification(OrderObserver):
def notify(self, order: Order, event: str):
print(f"Email: Order {order.order_id} has been {event}")
class SMSNotification(OrderObserver):
def notify(self, order: Order, event: str):
print(f"SMS: Order {order.order_id} status: {event}")
class PushNotification(OrderObserver):
def notify(self, order: Order, event: str):
print(f"Push: Your order {order.order_id} is now {event}")
# Factory pattern for creating different product types
class Product:
def __init__(self, name: str, price: float, category: str):
self.name = name
self.price = price
self.category = category
class ProductFactory:
@staticmethod
def create_product(product_type: str, name: str, price: float, **kwargs):
if product_type == "electronics":
return Product(name, price, "Electronics")
elif product_type == "clothing":
return Product(name, price, "Clothing")
elif product_type == "books":
return Product(name, price, "Books")
else:
return Product(name, price, "General")
# Main e-commerce system (Facade pattern)
class ECommerceSystem:
def __init__(self):
self.orders: Dict[str, Order] = {}
self.observers: List[OrderObserver] = []
self.payment_strategy: PaymentStrategy = None
def add_observer(self, observer: OrderObserver):
self.observers.append(observer)
def set_payment_strategy(self, strategy: PaymentStrategy):
self.payment_strategy = strategy
def create_order(self, order_id: str) -> Order:
order = Order(order_id)
self.orders[order_id] = order
self._notify_observers(order, "created")
return order
def process_payment(self, order_id: str) -> bool:
order = self.orders.get(order_id)
if order and self.payment_strategy:
if self.payment_strategy.pay(order.total):
order.confirm()
self._notify_observers(order, "confirmed")
return True
return False
def ship_order(self, order_id: str):
order = self.orders.get(order_id)
if order and order.ship():
self._notify_observers(order, "shipped")
def deliver_order(self, order_id: str):
order = self.orders.get(order_id)
if order and order.deliver():
self._notify_observers(order, "delivered")
def _notify_observers(self, order: Order, event: str):
for observer in self.observers:
observer.notify(order, event)
# Usage
ecommerce = ECommerceSystem()
# Add notification observers
ecommerce.add_observer(EmailNotification())
ecommerce.add_observer(SMSNotification())
ecommerce.add_observer(PushNotification())
# Create products
laptop = ProductFactory.create_product("electronics", "Gaming Laptop", 1200.0)
shirt = ProductFactory.create_product("clothing", "Cotton Shirt", 25.0)
# Create and process order
order = ecommerce.create_order("ORD-001")
order.add_item(laptop, 1)
order.add_item(shirt, 2)
print(f"Order total: ${order.total}")
# Set payment method and process payment
ecommerce.set_payment_strategy(CreditCardPayment("1234-5678-9012-3456"))
ecommerce.process_payment("ORD-001")
# Ship and deliver
ecommerce.ship_order("ORD-001")
ecommerce.deliver_order("ORD-001")✅ Good Pattern Selection:
# Use Observer when you need to notify multiple objects
class EventManager:
def __init__(self):
self._listeners = []
def subscribe(self, listener):
self._listeners.append(listener)
def notify(self, event):
for listener in self._listeners:
listener.handle(event)
# Use Strategy when you have multiple algorithms
class DataProcessor:
def __init__(self, strategy):
self._strategy = strategy
def process(self, data):
return self._strategy.process(data)❌ Poor Pattern Selection:
# Don't use Singleton for everything
class DatabaseSingleton: # Overkill for simple config
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Don't use Observer for simple callbacks
class SimpleCalculator: # Observer is overkill here
def __init__(self):
self._observers = []
def add(self, a, b):
result = a + b
self._notify_observers(result) # Unnecessary complexity
return result✅ Simple Implementation:
class Command:
def __init__(self, action, *args):
self.action = action
self.args = args
def execute(self):
return self.action(*self.args)
# Usage
def print_message(msg):
print(msg)
command = Command(print_message, "Hello, World!")
command.execute()❌ Over-engineered Implementation:
class AbstractCommandFactoryBuilder: # Too many patterns
def create_command_factory(self):
return CommandFactory()
class CommandFactory:
def create_command(self, command_type):
if command_type == "print":
return PrintCommandBuilder().build()
# ... more complexityclass WeatherStation:
"""
Weather monitoring system using Observer pattern.
This class implements the Subject role in the Observer pattern,
allowing multiple display elements to be notified when weather
measurements change.
Design Patterns Used:
- Observer: For notifying displays of weather changes
- Strategy: For different measurement algorithms (future)
"""
def __init__(self):
self._observers = []
self._temperature = 0
self._humidity = 0
def attach(self, observer):
"""Add an observer (Display element)"""
self._observers.append(observer)
def notify(self):
"""Notify all observers of state change"""
for observer in self._observers:
observer.update(self)❌ Bad:
# Using patterns where simple solutions would work
class SingletonFactoryObserverCommand: # Too many patterns!
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def create_observer_command(self, observer_type):
# Unnecessary complexity for simple task
pass✅ Good:
# Simple solution for simple problem
class MessageSender:
def send(self, message, recipients):
for recipient in recipients:
print(f"Sending '{message}' to {recipient}")❌ Bad:
# Using Singleton for stateless utility class
class MathUtilsSingleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def add(self, a, b):
return a + b # No state needed!✅ Good:
# Simple static methods or functions
class MathUtils:
@staticmethod
def add(a, b):
return a + b
# Or even simpler
def add(a, b):
return a + b❌ Bad:
# Java-style Observer in Python
class Observer:
def update(self, subject):
pass
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def notify(self):
for observer in self._observers:
observer.update(self)✅ Good:
# Pythonic approach with callbacks
class EventEmitter:
def __init__(self):
self._callbacks = []
def on(self, callback):
self._callbacks.append(callback)
def emit(self, *args, **kwargs):
for callback in self._callbacks:
callback(*args, **kwargs)
# Usage
emitter = EventEmitter()
emitter.on(lambda data: print(f"Received: {data}"))
emitter.emit("Hello!")-
Observer Weather Station
- Create a weather station that notifies multiple displays
- Implement temperature, humidity, and pressure sensors
- Add different display types (current conditions, statistics, forecast)
-
Strategy Calculator
- Create a calculator that can switch between different operation strategies
- Implement addition, subtraction, multiplication, division strategies
- Allow runtime strategy switching
-
Command Text Editor
- Build a simple text editor with undo/redo functionality
- Implement commands for insert, delete, replace operations
- Add macro recording capabilities
-
Decorator Coffee Shop
- Create a coffee ordering system using decorator pattern
- Base coffee types with various add-ons (milk, sugar, whip, etc.)
- Calculate total cost with all decorations
-
Factory Game Characters
- Design a game character creation system
- Different character types (warrior, mage, archer) with unique abilities
- Use abstract factory for different character families
-
Proxy Image Gallery
- Build an image gallery with lazy loading
- Implement virtual proxy for expensive image loading
- Add caching and access control proxies
-
MVC Web Framework
- Create a mini web framework using MVC pattern
- Combine Observer, Strategy, and Template Method patterns
- Support multiple view formats (HTML, JSON, XML)
-
Plugin Architecture
- Design an extensible plugin system
- Combine Factory, Registry, Observer, and Strategy patterns
- Support dynamic plugin loading and configuration
Design patterns are powerful tools that help you:
- Solve common problems with proven solutions
- Communicate effectively with other developers
- Write maintainable code that's easy to understand and modify
- Create flexible designs that can adapt to changing requirements
- Avoid reinventing the wheel by using established best practices
- Understand the problem first: Choose patterns based on actual needs, not trends
- Start simple: Don't over-engineer solutions with unnecessary patterns
- Learn the intent: Understand why patterns exist, not just how to implement them
- Practice regularly: Apply patterns in real projects to gain experience
- Combine wisely: Real applications often use multiple patterns together
| Problem Type | Recommended Patterns | When to Use |
|---|---|---|
| Object Creation | Singleton, Factory, Builder | Complex creation logic |
| Object Structure | Adapter, Decorator, Facade | Interface compatibility |
| Object Behavior | Observer, Strategy, Command | Dynamic behavior changes |
| System Architecture | MVC, Plugin, Layered | Large, complex systems |
- Practice implementing patterns from scratch
- Study real-world frameworks to see patterns in action
- Experiment with pattern combinations
- Focus on solving real problems, not just learning patterns
Remember: Patterns are tools, not goals. Use them when they solve real problems and make your code better, not just because you can.