Design Patterns Essentials
Complete guide to essential software design patterns with practical examples in multiple languages.
What are Design Patterns?
Design patterns are reusable solutions to common problems in software design. They represent best practices evolved over time by experienced developers.
Key Benefits:
Pattern Categories:
Creational Patterns
Singleton Pattern
Purpose: Ensure a class has only one instance and provide global access to it.
Use Cases:
Implementation:
# ========== Python Implementation ==========
class DatabaseConnection:
"""Singleton database connection"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
"""Initialize connection (only called once)"""
self.connection = "Database connected"
print("Database connection established")
def query(self, sql):
"""Execute query"""
return f"Executing: {sql}"
# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True - same instance
print(db1.query("SELECT * FROM users"))
// ========== JavaScript Implementation ==========
class ConfigManager {
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this.config = {};
ConfigManager.instance = this;
}
set(key, value) {
this.config[key] = value;
}
get(key) {
return this.config[key];
}
}
// Usage
const config1 = new ConfigManager();
const config2 = new ConfigManager();
console.log(config1 === config2); // true
config1.set('apiUrl', 'https://api.example.com');
console.log(config2.get('apiUrl')); // https://api.example.com
When to Use:
Factory Pattern
Purpose: Create objects without specifying exact class to create.
Use Cases:
Implementation:
# ========== Abstract Product ==========
from abc import ABC, abstractmethod
class Transport(ABC):
"""Abstract transport interface"""
@abstractmethod
def deliver(self):
pass
# ========== Concrete Products ==========
class Truck(Transport):
def deliver(self):
return "Delivering by land in a truck"
class Ship(Transport):
def deliver(self):
return "Delivering by sea in a ship"
class Airplane(Transport):
def deliver(self):
return "Delivering by air in an airplane"
# ========== Factory ==========
class TransportFactory:
"""Factory to create transport objects"""
@staticmethod
def create_transport(transport_type: str) -> Transport:
"""
Create transport based on type
Args:
transport_type: 'truck', 'ship', or 'airplane'
Returns:
Transport object
"""
transports = {
'truck': Truck,
'ship': Ship,
'airplane': Airplane
}
transport_class = transports.get(transport_type.lower())
if not transport_class:
raise ValueError(f"Unknown transport type: {transport_type}")
return transport_class()
# ========== Usage ==========
# Create transports without knowing specific classes
truck = TransportFactory.create_transport('truck')
ship = TransportFactory.create_transport('ship')
airplane = TransportFactory.create_transport('airplane')
print(truck.deliver()) # Delivering by land in a truck
print(ship.deliver()) # Delivering by sea in a ship
print(airplane.deliver()) # Delivering by air in an airplane
When to Use:
Builder Pattern
Purpose: Construct complex objects step by step.
Use Cases:
Implementation:
# ========== Product ==========
class Computer:
"""Complex product with many parts"""
def __init__(self):
self.cpu = None
self.ram = None
self.storage = None
self.gpu = None
self.os = None
def __str__(self):
return (f"Computer: CPU={self.cpu}, RAM={self.ram}GB, "
f"Storage={self.storage}, GPU={self.gpu}, OS={self.os}")
# ========== Builder ==========
class ComputerBuilder:
"""Builder for constructing Computer objects"""
def __init__(self):
self.computer = Computer()
def set_cpu(self, cpu):
"""Set CPU (required)"""
self.computer.cpu = cpu
return self # Return self for method chaining
def set_ram(self, ram):
"""Set RAM in GB"""
self.computer.ram = ram
return self
def set_storage(self, storage):
"""Set storage type and size"""
self.computer.storage = storage
return self
def set_gpu(self, gpu):
"""Set GPU (optional)"""
self.computer.gpu = gpu
return self
def set_os(self, os):
"""Set operating system"""
self.computer.os = os
return self
def build(self):
"""Return the constructed computer"""
# Validate required fields
if not self.computer.cpu or not self.computer.ram:
raise ValueError("CPU and RAM are required")
return self.computer
# ========== Usage ==========
# Build a gaming computer
gaming_pc = (ComputerBuilder()
.set_cpu("Intel i9")
.set_ram(32)
.set_storage("1TB NVMe SSD")
.set_gpu("RTX 4090")
.set_os("Windows 11")
.build())
print(gaming_pc)
# Build a basic office computer
office_pc = (ComputerBuilder()
.set_cpu("Intel i5")
.set_ram(16)
.set_storage("512GB SSD")
.set_os("Windows 11")
.build())
print(office_pc)
When to Use:
Structural Patterns
Adapter Pattern
Purpose: Convert interface of a class into another interface clients expect.
Use Cases:
Implementation:
# ========== Existing Interface (Target) ==========
class MediaPlayer:
"""Interface that client expects"""
def play(self, audio_type, filename):
pass
# ========== Incompatible Interface (Adaptee) ==========
class AdvancedMediaPlayer:
"""Third-party player with different interface"""
def play_mp4(self, filename):
print(f"Playing MP4 file: {filename}")
def play_mkv(self, filename):
print(f"Playing MKV file: {filename}")
# ========== Adapter ==========
class MediaAdapter(MediaPlayer):
"""Adapter to make AdvancedMediaPlayer compatible"""
def __init__(self):
self.advanced_player = AdvancedMediaPlayer()
def play(self, audio_type, filename):
"""Convert interface"""
if audio_type == 'mp4':
self.advanced_player.play_mp4(filename)
elif audio_type == 'mkv':
self.advanced_player.play_mkv(filename)
else:
print(f"Unsupported format: {audio_type}")
# ========== Client ==========
class AudioPlayer(MediaPlayer):
"""Client using the adapter"""
def __init__(self):
self.media_adapter = None
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 ['mp4', 'mkv']:
self.media_adapter = MediaAdapter()
self.media_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('mp4', 'video.mp4') # Playing MP4 file: video.mp4
player.play('mkv', 'movie.mkv') # Playing MKV file: movie.mkv
When to Use:
Decorator Pattern
Purpose: Add new functionality to objects dynamically.
Use Cases:
Implementation:
# ========== Component Interface ==========
from abc import ABC, abstractmethod
class Coffee(ABC):
"""Base coffee interface"""
@abstractmethod
def cost(self):
pass
@abstractmethod
def description(self):
pass
# ========== Concrete Component ==========
class SimpleCoffee(Coffee):
"""Basic coffee"""
def cost(self):
return 5.0
def description(self):
return "Simple Coffee"
# ========== Decorator Base ==========
class CoffeeDecorator(Coffee):
"""Base decorator"""
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost()
def description(self):
return self._coffee.description()
# ========== Concrete Decorators ==========
class MilkDecorator(CoffeeDecorator):
"""Add milk"""
def cost(self):
return self._coffee.cost() + 1.5
def description(self):
return self._coffee.description() + ", Milk"
class SugarDecorator(CoffeeDecorator):
"""Add sugar"""
def cost(self):
return self._coffee.cost() + 0.5
def description(self):
return self._coffee.description() + ", Sugar"
class WhipDecorator(CoffeeDecorator):
"""Add whipped cream"""
def cost(self):
return self._coffee.cost() + 2.0
def description(self):
return self._coffee.description() + ", Whipped Cream"
# ========== Usage ==========
# Start with simple coffee
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost()}")
# Output: Simple Coffee: $5.0
# Add milk
coffee = MilkDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Output: Simple Coffee, Milk: $6.5
# Add sugar
coffee = SugarDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Output: Simple Coffee, Milk, Sugar: $7.0
# Add whipped cream
coffee = WhipDecorator(coffee)
print(f"{coffee.description()}: ${coffee.cost()}")
# Output: Simple Coffee, Milk, Sugar, Whipped Cream: $9.0
# Create fancy coffee in one go
fancy_coffee = WhipDecorator(
SugarDecorator(
MilkDecorator(
SimpleCoffee()
)
)
)
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost()}")
When to Use:
Facade Pattern
Purpose: Provide simplified interface to complex subsystem.
Use Cases:
Implementation:
# ========== Complex Subsystem ==========
class CPU:
"""Complex component"""
def freeze(self):
print("CPU: Freezing...")
def jump(self, position):
print(f"CPU: Jumping to {position}")
def execute(self):
print("CPU: Executing...")
class Memory:
"""Complex component"""
def load(self, position, data):
print(f"Memory: Loading {data} at {position}")
class HardDrive:
"""Complex component"""
def read(self, sector, size):
print(f"HardDrive: Reading {size} bytes from sector {sector}")
return "boot_data"
# ========== Facade ==========
class ComputerFacade:
"""
Simple interface to complex computer subsystem
Hides complexity of CPU, Memory, and HardDrive
"""
def __init__(self):
self.cpu = CPU()
self.memory = Memory()
self.hard_drive = HardDrive()
def start(self):
"""
Simple one-method interface to start computer
Internally coordinates complex boot sequence
"""
print("=== Starting Computer ===")
self.cpu.freeze()
boot_data = self.hard_drive.read(sector=0, size=1024)
self.memory.load(position=0x00, data=boot_data)
self.cpu.jump(position=0x00)
self.cpu.execute()
print("=== Computer Started ===\n")
# ========== Usage ==========
# Without Facade (complex)
# cpu = CPU()
# memory = Memory()
# hdd = HardDrive()
# cpu.freeze()
# boot_data = hdd.read(0, 1024)
# memory.load(0x00, boot_data)
# cpu.jump(0x00)
# cpu.execute()
# With Facade (simple!)
computer = ComputerFacade()
computer.start() # One simple call!
When to Use:
Behavioral Patterns
Observer Pattern
Purpose: Define one-to-many dependency between objects.
Use Cases:
Implementation:
# ========== Subject (Observable) ==========
class Subject:
"""Observable object that notifies observers"""
def __init__(self):
self._observers = []
self._state = None
def attach(self, observer):
"""Add observer"""
if observer not in self._observers:
self._observers.append(observer)
print(f"Attached: {observer.__class__.__name__}")
def detach(self, observer):
"""Remove observer"""
self._observers.remove(observer)
print(f"Detached: {observer.__class__.__name__}")
def notify(self):
"""Notify all observers of state change"""
print("Notifying observers...")
for observer in self._observers:
observer.update(self)
@property
def state(self):
return self._state
@state.setter
def state(self, value):
"""When state changes, notify observers"""
print(f"State changed to: {value}")
self._state = value
self.notify()
# ========== Observer Interface ==========
class Observer:
"""Observer that reacts to subject changes"""
def update(self, subject):
pass
# ========== Concrete Observers ==========
class EmailNotifier(Observer):
"""Observer that sends email notifications"""
def update(self, subject):
print(f" 📧 Email: Sending notification - State is {subject.state}")
class SMSNotifier(Observer):
"""Observer that sends SMS notifications"""
def update(self, subject):
print(f" 📱 SMS: Sending text - State is {subject.state}")
class LoggerObserver(Observer):
"""Observer that logs state changes"""
def update(self, subject):
print(f" 📝 Logger: Recording state change to {subject.state}")
# ========== Usage ==========
# Create subject
stock_price = Subject()
# Create observers
email = EmailNotifier()
sms = SMSNotifier()
logger = LoggerObserver()
# Subscribe observers
stock_price.attach(email)
stock_price.attach(sms)
stock_price.attach(logger)
# Change state - all observers notified
stock_price.state = 100
# Output:
# State changed to: 100
# Notifying observers...
# 📧 Email: Sending notification - State is 100
# 📱 SMS: Sending text - State is 100
# 📝 Logger: Recording state change to 100
print()
# Change state again
stock_price.state = 150
print()
# Unsubscribe one observer
stock_price.detach(sms)
# Change state - only remaining observers notified
stock_price.state = 200
When to Use:
Strategy Pattern
Purpose: Define family of algorithms, encapsulate each, make them interchangeable.
Use Cases:
Implementation:
# ========== Strategy Interface ==========
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
"""Abstract payment strategy"""
@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} using 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} using PayPal account {self.email}")
class CryptoPayment(PaymentStrategy):
"""Pay with cryptocurrency"""
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def pay(self, amount):
print(f"Paid ${amount} using Crypto wallet {self.wallet_address[:10]}...")
# ========== Context ==========
class ShoppingCart:
"""Context that uses payment strategy"""
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 calculate_total(self):
"""Calculate total price"""
return sum(item['price'] for item in self.items)
def set_payment_strategy(self, strategy: PaymentStrategy):
"""Set payment method (strategy)"""
self.payment_strategy = strategy
def checkout(self):
"""Process payment using selected strategy"""
if not self.payment_strategy:
print("Please select a payment method")
return
total = self.calculate_total()
print(f"Total: ${total}")
self.payment_strategy.pay(total)
# ========== Usage ==========
# Create shopping cart
cart = ShoppingCart()
cart.add_item("Laptop", 999)
cart.add_item("Mouse", 25)
cart.add_item("Keyboard", 75)
print("=== Checkout with Credit Card ===")
cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123"))
cart.checkout()
# Output:
# Total: $1099
# Paid $1099 using Credit Card ending in 3456
print("\n=== Checkout with PayPal ===")
cart.set_payment_strategy(PayPalPayment("user@example.com"))
cart.checkout()
# Output:
# Total: $1099
# Paid $1099 using PayPal account user@example.com
print("\n=== Checkout with Crypto ===")
cart.set_payment_strategy(CryptoPayment("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"))
cart.checkout()
# Output:
# Total: $1099
# Paid $1099 using Crypto wallet 0x742d35Cc...
When to Use:
Command Pattern
Purpose: Encapsulate request as an object.
Use Cases:
Implementation:
# ========== Command Interface ==========
from abc import ABC, abstractmethod
class Command(ABC):
"""Abstract command"""
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# ========== Receiver ==========
class Light:
"""Receiver - actual object that performs action"""
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:
"""Invoker - triggers commands"""
def __init__(self):
self.commands = {}
self.history = []
def set_command(self, button, command: Command):
"""Assign command to button"""
self.commands[button] = command
def press_button(self, button):
"""Execute command"""
if button in self.commands:
command = self.commands[button]
command.execute()
self.history.append(command)
else:
print(f"No command assigned to button {button}")
def undo(self):
"""Undo last command"""
if self.history:
command = self.history.pop()
command.undo()
else:
print("Nothing to undo")
# ========== Usage ==========
# Create receivers
living_room_light = Light("Living Room")
bedroom_light = Light("Bedroom")
# Create commands
living_room_on = LightOnCommand(living_room_light)
living_room_off = LightOffCommand(living_room_light)
bedroom_on = LightOnCommand(bedroom_light)
bedroom_off = LightOffCommand(bedroom_light)
# Create invoker (remote control)
remote = RemoteControl()
# Assign commands to buttons
remote.set_command('1', living_room_on)
remote.set_command('2', living_room_off)
remote.set_command('3', bedroom_on)
remote.set_command('4', bedroom_off)
# Use remote
print("=== Using Remote Control ===")
remote.press_button('1') # Living Room light is ON
remote.press_button('3') # Bedroom light is ON
remote.press_button('2') # Living Room light is OFF
print("\n=== Undo Operations ===")
remote.undo() # Living Room light is ON (undo last OFF)
remote.undo() # Bedroom light is OFF (undo bedroom ON)
remote.undo() # Living Room light is OFF (undo living room ON)
When to Use:
Real-World Examples
Example 1: E-commerce System
Combining multiple patterns:
# ========== Singleton: Configuration ==========
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.settings = {
'tax_rate': 0.08,
'shipping_cost': 10.0
}
return cls._instance
# ========== Factory: Product Creation ==========
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
class ProductFactory:
@staticmethod
def create_product(product_type, name, price):
products = {
'physical': lambda: PhysicalProduct(name, price),
'digital': lambda: DigitalProduct(name, price)
}
return products[product_type]()
class PhysicalProduct(Product):
def calculate_shipping(self):
return Config().settings['shipping_cost']
class DigitalProduct(Product):
def calculate_shipping(self):
return 0 # No shipping for digital products
# ========== Strategy: Discount Calculation ==========
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, amount):
pass
class NoDiscount(DiscountStrategy):
def calculate(self, amount):
return amount
class PercentageDiscount(DiscountStrategy):
def __init__(self, percentage):
self.percentage = percentage
def calculate(self, amount):
return amount * (1 - self.percentage / 100)
# ========== Observer: Order Notifications ==========
class Order:
def __init__(self):
self._observers = []
self._status = 'pending'
def attach(self, observer):
self._observers.append(observer)
def set_status(self, status):
self._status = status
for observer in self._observers:
observer.update(status)
class EmailNotification:
def update(self, status):
print(f"Email: Order status changed to {status}")
# ========== Complete Flow ==========
# Create products
laptop = ProductFactory.create_product('physical', 'Laptop', 1000)
ebook = ProductFactory.create_product('digital', 'E-Book', 20)
# Apply discount
discount = PercentageDiscount(10)
final_price = discount.calculate(laptop.price)
# Create order and attach observer
order = Order()
order.attach(EmailNotification())
order.set_status('confirmed') # Email: Order status changed to confirmed
Example 2: Logging System
# ========== Singleton: Logger ==========
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.logs = []
return cls._instance
def log(self, message, level='INFO'):
log_entry = f"[{level}] {message}"
self.logs.append(log_entry)
print(log_entry)
# ========== Decorator: Log Formatting ==========
class LogDecorator:
def __init__(self, logger):
self.logger = logger
def log(self, message, level='INFO'):
self.logger.log(message, level)
class TimestampDecorator(LogDecorator):
def log(self, message, level='INFO'):
from datetime import datetime
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
message = f"{timestamp} - {message}"
self.logger.log(message, level)
# Usage
logger = Logger()
decorated_logger = TimestampDecorator(logger)
decorated_logger.log("Application started")
# Output: [INFO] 2024-04-20 10:30:00 - Application started
Best Practices
When to Use Design Patterns
✅ DO Use When:
❌ DON'T Use When:
Common Pitfalls
1. Pattern Overuse# ❌ Bad: Using pattern for simple task
class SingletonStringFormatter:
_instance = None
def format(self, s):
return s.upper()
# ✅ Good: Simple function
def format_string(s):
return s.upper()
2. Ignoring Language Features
# ❌ Bad: Implementing Singleton in Python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# ✅ Good: Use module (Python modules are singletons)
# config.py
settings = {'key': 'value'}
# Import and use
from config import settings
3. Not Considering Alternatives
Pattern Selection Guide
By Problem Type
Object Creation:
Object Structure:
Object Behavior:
Resources
Quick Reference Card
# ========== Creational Patterns ==========
Singleton - One instance only
Factory - Create objects without specifying class
Builder - Construct complex objects step by step
Prototype - Clone existing objects
# ========== Structural Patterns ==========
Adapter - Convert interface to another
Decorator - Add features dynamically
Facade - Simplify complex subsystem
Proxy - Control access to object
# ========== Behavioral Patterns ==========
Observer - Notify multiple objects of changes
Strategy - Swap algorithms at runtime
Command - Encapsulate request as object
Template - Define algorithm skeleton
Next Steps
---
Last Updated: 2026-04-20 Difficulty: Intermediate Recommended: Understand OOP before studying patterns