Design Patterns
Learning Objectives
By the end of this reading, you will be able to:
- Understand the purpose and benefits of design patterns
- Implement creational patterns (Singleton, Factory, Builder)
- Apply structural patterns (Adapter, Decorator, Facade)
- Use behavioral patterns (Observer, Strategy, Command)
- Recognize when to apply specific patterns
- Avoid common anti-patterns
Introduction
Design patterns are reusable solutions to commonly occurring problems in software design. They represent best practices evolved over time by experienced developers. Design patterns are not finished code you can copy; they're templates for solving problems in various contexts.
Why Design Patterns?
- Proven Solutions: Battle-tested approaches to common problems
- Common Vocabulary: Shared language for discussing design
- Best Practices: Codified wisdom from expert developers
- Flexibility: Create more maintainable and extensible code
- Efficiency: Avoid reinventing the wheel
Pattern Categories
Design patterns are typically organized into three categories:
- Creational: Object creation mechanisms
- Structural: Object composition and relationships
- Behavioral: Object collaboration and responsibility
Creational Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
class Singleton:
"""
Classic Singleton implementation.
Use when:
- Exactly one instance of a class is needed
- The instance needs to be accessible from multiple parts of the application
- Examples: configuration manager, logging, database connection pool
"""
_instance = None
_initialized = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# Ensure initialization only happens once
if not Singleton._initialized:
self.config = {}
Singleton._initialized = True
def set_config(self, key, value):
"""Set a configuration value."""
self.config[key] = value
def get_config(self, key):
"""Get a configuration value."""
return self.config.get(key)
# Usage
config1 = Singleton()
config1.set_config("database", "postgresql")
config2 = Singleton()
print(config2.get_config("database")) # postgresql
# Both are the same instance
assert config1 is config2 # True
Thread-Safe Singleton
import threading
class ThreadSafeSingleton:
"""Thread-safe Singleton using lock."""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
# Double-checked locking
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Modern Python approach using metaclass
class SingletonMeta(type):
"""Metaclass for creating Singleton classes."""
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
"""Database connection pool (Singleton)."""
def __init__(self):
self.connection_string = None
self.pool = []
def connect(self, connection_string):
"""Establish database connection."""
if not self.connection_string:
self.connection_string = connection_string
print(f"Connected to {connection_string}")
# Usage
db1 = DatabaseConnection()
db1.connect("postgresql://localhost/mydb")
db2 = DatabaseConnection()
# Same instance, already connected
assert db1 is db2
Factory Pattern
Defines an interface for creating objects, but lets subclasses decide which class to instantiate.
from abc import ABC, abstractmethod
# Product interface
class Vehicle(ABC):
"""Abstract base class for vehicles."""
@abstractmethod
def drive(self):
"""Drive the vehicle."""
pass
@abstractmethod
def get_specs(self):
"""Get vehicle specifications."""
pass
# Concrete products
class Car(Vehicle):
"""Car implementation."""
def __init__(self, model):
self.model = model
self.wheels = 4
def drive(self):
return f"Driving {self.model} car on road"
def get_specs(self):
return {"type": "car", "model": self.model, "wheels": self.wheels}
class Motorcycle(Vehicle):
"""Motorcycle implementation."""
def __init__(self, model):
self.model = model
self.wheels = 2
def drive(self):
return f"Riding {self.model} motorcycle"
def get_specs(self):
return {"type": "motorcycle", "model": self.model, "wheels": self.wheels}
class Truck(Vehicle):
"""Truck implementation."""
def __init__(self, model, capacity):
self.model = model
self.capacity = capacity
self.wheels = 6
def drive(self):
return f"Driving {self.model} truck carrying {self.capacity}kg"
def get_specs(self):
return {
"type": "truck",
"model": self.model,
"wheels": self.wheels,
"capacity": self.capacity
}
# Factory
class VehicleFactory:
"""
Factory for creating vehicles.
Use when:
- Class can't anticipate the type of objects it needs to create
- You want to centralize object creation logic
- You need to add new types without changing client code
"""
@staticmethod
def create_vehicle(vehicle_type, **kwargs):
"""
Create a vehicle based on type.
Args:
vehicle_type: 'car', 'motorcycle', or 'truck'
**kwargs: Arguments specific to vehicle type
Returns:
Vehicle instance
"""
vehicles = {
'car': Car,
'motorcycle': Motorcycle,
'truck': Truck
}
vehicle_class = vehicles.get(vehicle_type.lower())
if not vehicle_class:
raise ValueError(f"Unknown vehicle type: {vehicle_type}")
return vehicle_class(**kwargs)
# Usage
factory = VehicleFactory()
car = factory.create_vehicle('car', model='Tesla Model 3')
print(car.drive()) # Driving Tesla Model 3 car on road
motorcycle = factory.create_vehicle('motorcycle', model='Harley Davidson')
print(motorcycle.drive()) # Riding Harley Davidson motorcycle
truck = factory.create_vehicle('truck', model='Ford F-150', capacity=1000)
print(truck.drive()) # Driving Ford F-150 truck carrying 1000kg
Abstract Factory Pattern
Creates families of related objects without specifying their concrete classes.
# Abstract products
class Button(ABC):
@abstractmethod
def render(self):
pass
class Checkbox(ABC):
@abstractmethod
def render(self):
pass
# Concrete products - Windows
class WindowsButton(Button):
def render(self):
return "Rendering Windows-style button"
class WindowsCheckbox(Checkbox):
def render(self):
return "Rendering Windows-style checkbox"
# Concrete products - Mac
class MacButton(Button):
def render(self):
return "Rendering Mac-style button"
class MacCheckbox(Checkbox):
def render(self):
return "Rendering Mac-style checkbox"
# Abstract factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete factories
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self):
return MacButton()
def create_checkbox(self):
return MacCheckbox()
# Client code
class Application:
"""Application that uses GUI factory."""
def __init__(self, factory: GUIFactory):
self.factory = factory
self.button = factory.create_button()
self.checkbox = factory.create_checkbox()
def render(self):
"""Render UI elements."""
return {
'button': self.button.render(),
'checkbox': self.checkbox.render()
}
# Usage
import platform
# Choose factory based on platform
if platform.system() == "Windows":
factory = WindowsFactory()
else:
factory = MacFactory()
app = Application(factory)
print(app.render())
Builder Pattern
Separates the construction of a complex object from its representation.
class Pizza:
"""Complex object to build."""
def __init__(self):
self.size = None
self.cheese = False
self.pepperoni = False
self.mushrooms = False
self.olives = False
self.crust_type = None
def __str__(self):
toppings = []
if self.cheese:
toppings.append("cheese")
if self.pepperoni:
toppings.append("pepperoni")
if self.mushrooms:
toppings.append("mushrooms")
if self.olives:
toppings.append("olives")
return f"{self.size} pizza with {self.crust_type} crust and {', '.join(toppings)}"
class PizzaBuilder:
"""
Builder for constructing Pizza objects.
Use when:
- Object construction is complex with many optional parameters
- You want to create different representations of an object
- Construction process must allow different representations
"""
def __init__(self):
self.pizza = Pizza()
def set_size(self, size):
"""Set pizza size."""
self.pizza.size = size
return self # Return self for method chaining
def add_cheese(self):
"""Add cheese topping."""
self.pizza.cheese = True
return self
def add_pepperoni(self):
"""Add pepperoni topping."""
self.pizza.pepperoni = True
return self
def add_mushrooms(self):
"""Add mushroom topping."""
self.pizza.mushrooms = True
return self
def add_olives(self):
"""Add olive topping."""
self.pizza.olives = True
return self
def set_crust(self, crust_type):
"""Set crust type."""
self.pizza.crust_type = crust_type
return self
def build(self):
"""Build and return the pizza."""
if not self.pizza.size or not self.pizza.crust_type:
raise ValueError("Size and crust type are required")
return self.pizza
# Usage - Method chaining
pizza = (PizzaBuilder()
.set_size("large")
.set_crust("thin")
.add_cheese()
.add_pepperoni()
.add_mushrooms()
.build())
print(pizza)
# large pizza with thin crust and cheese, pepperoni, mushrooms
# Director - encapsulates common construction logic
class PizzaDirector:
"""Director for building predefined pizza types."""
@staticmethod
def margherita():
"""Build a Margherita pizza."""
return (PizzaBuilder()
.set_size("medium")
.set_crust("regular")
.add_cheese()
.build())
@staticmethod
def pepperoni_feast():
"""Build a Pepperoni Feast pizza."""
return (PizzaBuilder()
.set_size("large")
.set_crust("thick")
.add_cheese()
.add_pepperoni()
.build())
# Usage with director
margherita = PizzaDirector.margherita()
print(margherita)
Structural Patterns
Structural patterns deal with object composition, creating relationships between objects to form larger structures.
Adapter Pattern
Converts the interface of a class into another interface clients expect.
# Target interface
class MediaPlayer(ABC):
"""Interface expected by client."""
@abstractmethod
def play(self, audio_type, filename):
pass
# Adaptee - incompatible interface
class AdvancedMediaPlayer(ABC):
"""Advanced media player with different interface."""
@abstractmethod
def play_vlc(self, filename):
pass
@abstractmethod
def play_mp4(self, filename):
pass
# Concrete adaptees
class VlcPlayer(AdvancedMediaPlayer):
def play_vlc(self, filename):
print(f"Playing VLC file: {filename}")
def play_mp4(self, filename):
pass
class Mp4Player(AdvancedMediaPlayer):
def play_vlc(self, filename):
pass
def play_mp4(self, filename):
print(f"Playing MP4 file: {filename}")
# Adapter
class MediaAdapter(MediaPlayer):
"""
Adapter that makes AdvancedMediaPlayer compatible with MediaPlayer.
Use when:
- You want to use an existing class with an incompatible interface
- You need to create a reusable class that cooperates with unrelated classes
- You need to use several existing subclasses but can't adapt by subclassing
"""
def __init__(self, audio_type):
if audio_type == "vlc":
self.advanced_player = VlcPlayer()
elif audio_type == "mp4":
self.advanced_player = Mp4Player()
def play(self, audio_type, filename):
if audio_type == "vlc":
self.advanced_player.play_vlc(filename)
elif audio_type == "mp4":
self.advanced_player.play_mp4(filename)
# Concrete target
class AudioPlayer(MediaPlayer):
"""Media player that can play different formats using adapter."""
def play(self, audio_type, filename):
# Built-in support for mp3
if audio_type == "mp3":
print(f"Playing MP3 file: {filename}")
# Use adapter for other formats
elif audio_type in ["vlc", "mp4"]:
adapter = MediaAdapter(audio_type)
adapter.play(audio_type, filename)
else:
print(f"Invalid format: {audio_type}")
# Usage
player = AudioPlayer()
player.play("mp3", "song.mp3") # Playing MP3 file: song.mp3
player.play("vlc", "movie.vlc") # Playing VLC file: movie.vlc
player.play("mp4", "video.mp4") # Playing MP4 file: video.mp4
Decorator Pattern
Attaches additional responsibilities to an object dynamically.
# Component interface
class Coffee(ABC):
"""Base coffee interface."""
@abstractmethod
def get_cost(self):
pass
@abstractmethod
def get_description(self):
pass
# Concrete component
class SimpleCoffee(Coffee):
"""Basic coffee without additions."""
def get_cost(self):
return 2.0
def get_description(self):
return "Simple coffee"
# Decorator base class
class CoffeeDecorator(Coffee):
"""
Base decorator for coffee additions.
Use when:
- You need to add responsibilities to objects dynamically
- Extension by subclassing is impractical
- You want to add responsibilities that can be withdrawn
"""
def __init__(self, coffee: Coffee):
self._coffee = coffee
def get_cost(self):
return self._coffee.get_cost()
def get_description(self):
return self._coffee.get_description()
# Concrete decorators
class Milk(CoffeeDecorator):
"""Add milk to coffee."""
def get_cost(self):
return self._coffee.get_cost() + 0.5
def get_description(self):
return self._coffee.get_description() + ", milk"
class Sugar(CoffeeDecorator):
"""Add sugar to coffee."""
def get_cost(self):
return self._coffee.get_cost() + 0.2
def get_description(self):
return self._coffee.get_description() + ", sugar"
class WhippedCream(CoffeeDecorator):
"""Add whipped cream to coffee."""
def get_cost(self):
return self._coffee.get_cost() + 0.7
def get_description(self):
return self._coffee.get_description() + ", whipped cream"
# Usage - wrapping decorators
coffee = SimpleCoffee()
print(f"{coffee.get_description()} = ${coffee.get_cost()}")
# Simple coffee = $2.0
coffee_with_milk = Milk(coffee)
print(f"{coffee_with_milk.get_description()} = ${coffee_with_milk.get_cost()}")
# Simple coffee, milk = $2.5
fancy_coffee = WhippedCream(Sugar(Milk(SimpleCoffee())))
print(f"{fancy_coffee.get_description()} = ${fancy_coffee.get_cost()}")
# Simple coffee, milk, sugar, whipped cream = $3.4
Facade Pattern
Provides a unified interface to a set of interfaces in a subsystem.
# Complex subsystem classes
class CPU:
"""CPU component."""
def freeze(self):
print("CPU: Freezing processor")
def jump(self, position):
print(f"CPU: Jumping to {position}")
def execute(self):
print("CPU: Executing instructions")
class Memory:
"""Memory component."""
def load(self, position, data):
print(f"Memory: Loading data at {position}")
class HardDrive:
"""Hard drive component."""
def read(self, lba, size):
print(f"HardDrive: Reading {size} bytes from sector {lba}")
return b"boot data"
# Facade
class ComputerFacade:
"""
Simplified interface for starting a computer.
Use when:
- You want to provide a simple interface to a complex subsystem
- There are many dependencies between clients and implementation classes
- You want to layer your subsystems
"""
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.hard_drive = HardDrive()
def start(self):
"""Simplified method to start computer."""
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()
print("Computer started successfully")
# Usage
computer = ComputerFacade()
computer.start()
# Much simpler than calling each subsystem method individually
Proxy Pattern
Provides a surrogate or placeholder for another object to control access.
# Subject interface
class Image(ABC):
"""Image interface."""
@abstractmethod
def display(self):
pass
# Real subject
class RealImage(Image):
"""Actual image that loads from disk."""
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 operation
import time
time.sleep(1)
def display(self):
print(f"Displaying {self.filename}")
# Proxy
class ImageProxy(Image):
"""
Proxy that delays loading until needed.
Use when:
- You need a more sophisticated reference to an object
- Remote proxy: represents object in different address space
- Virtual proxy: creates expensive objects on demand
- Protection proxy: controls access to the original object
"""
def __init__(self, filename):
self.filename = filename
self._real_image = None
def display(self):
# Lazy loading - only load when needed
if self._real_image is None:
self._real_image = RealImage(self.filename)
self._real_image.display()
# Usage
images = [
ImageProxy("photo1.jpg"),
ImageProxy("photo2.jpg"),
ImageProxy("photo3.jpg")
]
print("Images created (but not loaded)")
# Only load when accessed
images[0].display() # Loads and displays photo1.jpg
images[0].display() # Just displays (already loaded)
Behavioral Patterns
Behavioral patterns focus on communication between objects and the assignment of responsibilities.
Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.
# Subject
class Subject:
"""
Subject that maintains list of observers.
Use when:
- Change to one object requires changing others
- An object should notify other objects without knowing who they are
- Examples: Event handling, MVC, publish-subscribe systems
"""
def __init__(self):
self._observers = []
self._state = None
def attach(self, observer):
"""Register an observer."""
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer):
"""Unregister an observer."""
self._observers.remove(observer)
def notify(self):
"""Notify all observers of state change."""
for observer in self._observers:
observer.update(self)
def set_state(self, state):
"""Change state and notify observers."""
self._state = state
self.notify()
def get_state(self):
"""Get current state."""
return self._state
# Observer interface
class Observer(ABC):
"""Observer interface."""
@abstractmethod
def update(self, subject):
pass
# Concrete observers
class BinaryObserver(Observer):
"""Displays state in binary."""
def update(self, subject):
state = subject.get_state()
if state is not None:
print(f"Binary: {bin(state)}")
class HexObserver(Observer):
"""Displays state in hexadecimal."""
def update(self, subject):
state = subject.get_state()
if state is not None:
print(f"Hex: {hex(state)}")
class OctalObserver(Observer):
"""Displays state in octal."""
def update(self, subject):
state = subject.get_state()
if state is not None:
print(f"Octal: {oct(state)}")
# Usage
subject = Subject()
# Register observers
binary_obs = BinaryObserver()
hex_obs = HexObserver()
octal_obs = OctalObserver()
subject.attach(binary_obs)
subject.attach(hex_obs)
subject.attach(octal_obs)
# Change state - all observers notified
print("Setting state to 15:")
subject.set_state(15)
# Binary: 0b1111
# Hex: 0xf
# Octal: 0o17
print("\nSetting state to 10:")
subject.set_state(10)
# Binary: 0b1010
# Hex: 0xa
# Octal: 0o12
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
# Strategy interface
class PaymentStrategy(ABC):
"""Payment method interface."""
@abstractmethod
def pay(self, amount):
pass
# Concrete strategies
class CreditCardPayment(PaymentStrategy):
"""Pay with credit card."""
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount):
print(f"Paid ${amount} with credit card ending in {self.card_number[-4:]}")
class PayPalPayment(PaymentStrategy):
"""Pay with PayPal."""
def __init__(self, email):
self.email = email
def pay(self, amount):
print(f"Paid ${amount} via PayPal account {self.email}")
class BitcoinPayment(PaymentStrategy):
"""Pay with Bitcoin."""
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def pay(self, amount):
print(f"Paid ${amount} with Bitcoin to {self.wallet_address[:10]}...")
# Context
class ShoppingCart:
"""
Shopping cart that can use different payment strategies.
Use when:
- Many related classes differ only in their behavior
- You need different variants of an algorithm
- A class defines many behaviors with multiple conditional statements
"""
def __init__(self):
self.items = []
self.payment_strategy = None
def add_item(self, item, price):
"""Add item to cart."""
self.items.append({'item': item, 'price': price})
def set_payment_strategy(self, strategy: PaymentStrategy):
"""Set payment method."""
self.payment_strategy = strategy
def checkout(self):
"""Process payment using selected strategy."""
total = sum(item['price'] for item in self.items)
if self.payment_strategy:
self.payment_strategy.pay(total)
else:
raise ValueError("No payment method selected")
# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
# Pay with credit card
cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123"))
cart.checkout() # Paid $1029.98 with credit card ending in 3456
# Change strategy and pay with PayPal
cart.set_payment_strategy(PayPalPayment("user@example.com"))
cart.checkout() # Paid $1029.98 via PayPal account user@example.com
Command Pattern
Encapsulates a request as an object, thereby letting you parameterize clients with different requests.
# Command interface
class Command(ABC):
"""Command interface."""
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# Receiver
class Light:
"""Light that can be turned on/off."""
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")
# Concrete commands
class LightOnCommand(Command):
"""Command to turn light on."""
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.turn_on()
def undo(self):
self.light.turn_off()
class LightOffCommand(Command):
"""Command to turn light off."""
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.turn_off()
def undo(self):
self.light.turn_on()
# Invoker
class RemoteControl:
"""
Remote control that executes commands.
Use when:
- You want to parameterize objects with operations
- You want to queue operations, schedule their execution, or execute remotely
- You want to support undo/redo
- You want to log changes
"""
def __init__(self):
self.command = None
self.history = []
def set_command(self, command: Command):
"""Set current command."""
self.command = command
def press_button(self):
"""Execute current command."""
if self.command:
self.command.execute()
self.history.append(self.command)
def press_undo(self):
"""Undo last command."""
if self.history:
command = self.history.pop()
command.undo()
# Usage
living_room_light = Light("Living Room")
bedroom_light = Light("Bedroom")
# Create commands
lr_on = LightOnCommand(living_room_light)
lr_off = LightOffCommand(living_room_light)
br_on = LightOnCommand(bedroom_light)
remote = RemoteControl()
# Turn on living room light
remote.set_command(lr_on)
remote.press_button() # Living Room light is ON
# Turn on bedroom light
remote.set_command(br_on)
remote.press_button() # Bedroom light is ON
# Undo last command
remote.press_undo() # Bedroom light is OFF
Template Method Pattern
Defines the skeleton of an algorithm, deferring some steps to subclasses.
from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""
Template for data processing workflow.
Use when:
- You want to let subclasses override specific steps of an algorithm
- Common behavior should be factored and localized in a common class
"""
def process(self):
"""Template method defining the algorithm skeleton."""
self.read_data()
self.validate_data()
self.transform_data()
self.save_data()
self.send_notification()
@abstractmethod
def read_data(self):
"""Read data from source (must be implemented by subclass)."""
pass
def validate_data(self):
"""Validate data (can be overridden)."""
print("Validating data using default validation")
@abstractmethod
def transform_data(self):
"""Transform data (must be implemented by subclass)."""
pass
@abstractmethod
def save_data(self):
"""Save processed data (must be implemented by subclass)."""
pass
def send_notification(self):
"""Send notification (optional hook)."""
print("Processing complete")
# Concrete implementations
class CSVDataProcessor(DataProcessor):
"""Process CSV files."""
def read_data(self):
print("Reading data from CSV file")
def transform_data(self):
print("Transforming CSV data to JSON")
def save_data(self):
print("Saving to database")
class XMLDataProcessor(DataProcessor):
"""Process XML files."""
def read_data(self):
print("Reading data from XML file")
def validate_data(self):
# Override default validation
print("Validating XML against schema")
def transform_data(self):
print("Transforming XML data to JSON")
def save_data(self):
print("Saving to cloud storage")
def send_notification(self):
# Override notification
print("Sending email notification")
# Usage
csv_processor = CSVDataProcessor()
csv_processor.process()
# Reading data from CSV file
# Validating data using default validation
# Transforming CSV data to JSON
# Saving to database
# Processing complete
print()
xml_processor = XMLDataProcessor()
xml_processor.process()
# Reading data from XML file
# Validating XML against schema
# Transforming XML data to JSON
# Saving to cloud storage
# Sending email notification
Pattern Selection Guide
class PatternSelector:
"""Help select appropriate design pattern."""
PATTERNS = {
'creational': {
'singleton': "Need exactly one instance of a class",
'factory': "Need to create objects without specifying exact class",
'abstract_factory': "Need to create families of related objects",
'builder': "Need to construct complex objects step by step",
'prototype': "Need to create objects by cloning existing ones"
},
'structural': {
'adapter': "Need to make incompatible interfaces work together",
'decorator': "Need to add responsibilities to objects dynamically",
'facade': "Need to provide simple interface to complex subsystem",
'proxy': "Need to control access to an object",
'composite': "Need to treat individual objects and compositions uniformly",
'bridge': "Need to separate abstraction from implementation"
},
'behavioral': {
'observer': "Need to notify multiple objects of state changes",
'strategy': "Need to switch between different algorithms",
'command': "Need to encapsulate requests as objects",
'template_method': "Need to define algorithm skeleton, let subclasses override steps",
'iterator': "Need to traverse a collection without exposing its representation",
'state': "Need object to change behavior when internal state changes"
}
}
@staticmethod
def recommend(problem_description):
"""Recommend pattern based on problem description."""
# Simple keyword matching (in real implementation, use NLP)
keywords = {
'one instance': 'singleton',
'create object': 'factory',
'incompatible interface': 'adapter',
'add behavior': 'decorator',
'notify': 'observer',
'algorithm': 'strategy',
'undo': 'command',
'complex object': 'builder'
}
for keyword, pattern in keywords.items():
if keyword in problem_description.lower():
return pattern
return None
Exercises
Basic Exercises
Singleton Pattern
- Implement a Logger class as a Singleton
- Add methods for different log levels (debug, info, warning, error)
- Ensure thread safety
- Add log rotation functionality
Factory Pattern
- Create a shape factory that creates circles, squares, and triangles
- Each shape should have an area() and perimeter() method
- Add a new shape type without modifying existing code
Observer Pattern
- Implement a weather station with temperature, humidity, and pressure sensors
- Create multiple display observers (current conditions, statistics, forecast)
- Each display updates when sensor data changes
Intermediate Exercises
Decorator Pattern
- Create a text processing system with decorators
- Base component: plain text
- Decorators: uppercase, lowercase, encryption, compression
- Allow chaining multiple decorators
Strategy Pattern
- Implement a sorting context that can use different strategies
- Strategies: bubble sort, quick sort, merge sort
- Measure and compare performance
- Choose strategy based on data characteristics
Builder Pattern
- Create a SQL query builder
- Support SELECT, WHERE, JOIN, ORDER BY, LIMIT
- Validate query correctness
- Generate optimized SQL
Advanced Exercises
Combined Patterns
- Create a document editor using multiple patterns:
- Command: for undo/redo
- Decorator: for text formatting
- Observer: for auto-save
- Composite: for document structure
- Create a document editor using multiple patterns:
E-commerce System
- Design an e-commerce system using:
- Factory: for creating products
- Strategy: for pricing strategies (discounts, taxes)
- Observer: for inventory updates
- Decorator: for gift wrapping, express shipping
- Design an e-commerce system using:
Game Framework
- Build a simple game framework using:
- Factory: for creating enemies
- Strategy: for enemy AI behaviors
- Observer: for event system
- State: for game states (menu, playing, paused, game over)
- Build a simple game framework using:
Plugin Architecture
- Design a plugin system:
- Use Factory to create plugins
- Use Strategy for plugin execution
- Use Observer for plugin events
- Support dynamic plugin loading/unloading
Anti-Patterns to Avoid
"""
Common anti-patterns:
1. GOD OBJECT
- One class that does everything
- Solution: Single Responsibility Principle, decompose into smaller classes
2. SPAGHETTI CODE
- Unstructured, difficult to follow
- Solution: Proper separation of concerns, design patterns
3. LAVA FLOW
- Dead code that nobody dares remove
- Solution: Regular refactoring, code reviews
4. GOLDEN HAMMER
- Using same solution for every problem
- Solution: Learn multiple patterns, choose appropriately
5. PREMATURE OPTIMIZATION
- Optimizing before profiling
- Solution: Make it work, make it right, make it fast (in that order)
6. TIGHT COUPLING
- Classes depend too heavily on each other
- Solution: Dependency Injection, interfaces, loose coupling
7. SHOTGUN SURGERY
- One change requires many small changes everywhere
- Solution: Better encapsulation, higher cohesion
"""
Summary
Design patterns are proven solutions to common software design problems:
Creational Patterns:
- Singleton: One instance, global access
- Factory: Create objects without specifying exact class
- Builder: Construct complex objects step by step
Structural Patterns:
- Adapter: Make incompatible interfaces work together
- Decorator: Add behavior dynamically
- Facade: Simplify complex subsystems
- Proxy: Control access to objects
Behavioral Patterns:
- Observer: Notify dependents of state changes
- Strategy: Interchange algorithms
- Command: Encapsulate requests as objects
- Template Method: Define algorithm skeleton
Key principles:
- Patterns are guidelines, not rigid rules
- Choose patterns based on actual needs
- Don't over-engineer simple problems
- Combine patterns when appropriate
- Refactor to patterns as code evolves
Next Reading
Continue to 04-testing.md to learn about testing strategies including unit tests, integration tests, TDD, and the testing pyramid.