Design Patterns
Introduction
Design patterns are reusable solutions to recurring problems in software design. This article covers the SOLID principles and the most commonly used patterns from the GoF (Gang of Four) classic.
1. SOLID Principles
| Principle | Full Name | Meaning |
|---|---|---|
| S | Single Responsibility | A class should have only one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for their base types |
| I | Interface Segregation | Interfaces should be small and specific |
| D | Dependency Inversion | Depend on abstractions, not concrete implementations |
Example: Dependency Inversion
# Violating DIP
class MySQLDatabase:
def query(self, sql): ...
class UserService:
def __init__(self):
self.db = MySQLDatabase() # direct dependency on concrete implementation
# Following DIP
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def query(self, sql): ...
class MySQLDatabase(Database):
def query(self, sql): ...
class PostgresDatabase(Database):
def query(self, sql): ...
class UserService:
def __init__(self, db: Database): # depends on abstraction
self.db = db
2. Creational Patterns
2.1 Singleton
Ensures a class has only one instance.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Thread-safe version
import threading
class ThreadSafeSingleton:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None: # double-checked locking
cls._instance = super().__new__(cls)
return cls._instance
Use cases: database connection pools, loggers, configuration management.
2.2 Factory
Delegates object creation to factory methods.
class Animal(ABC):
@abstractmethod
def speak(self): ...
class Dog(Animal):
def speak(self): return "Woof!"
class Cat(Animal):
def speak(self): return "Meow!"
class AnimalFactory:
@staticmethod
def create(animal_type: str) -> Animal:
factories = {
"dog": Dog,
"cat": Cat,
}
if animal_type not in factories:
raise ValueError(f"Unknown animal: {animal_type}")
return factories[animal_type]()
animal = AnimalFactory.create("dog")
print(animal.speak()) # Woof!
Use cases: creating different types of objects based on configuration or input.
2.3 Builder
Constructs complex objects step by step.
class QueryBuilder:
def __init__(self):
self._table = None
self._conditions = []
self._order_by = None
self._limit = None
def table(self, name):
self._table = name
return self
def where(self, condition):
self._conditions.append(condition)
return self
def order(self, field):
self._order_by = field
return self
def limit(self, n):
self._limit = n
return self
def build(self):
sql = f"SELECT * FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
query = (QueryBuilder()
.table("users")
.where("age > 18")
.where("active = 1")
.order("created_at DESC")
.limit(10)
.build())
# SELECT * FROM users WHERE age > 18 AND active = 1 ORDER BY created_at DESC LIMIT 10
Use cases: building complex configurations, SQL queries, HTTP requests.
2.4 Prototype
Creates new objects by cloning existing ones.
import copy
class Prototype:
def clone(self):
return copy.deepcopy(self)
class GameUnit(Prototype):
def __init__(self, name, hp, attack):
self.name = name
self.hp = hp
self.attack = attack
template = GameUnit("Warrior", 100, 15)
unit1 = template.clone()
unit1.name = "Warrior_1"
3. Structural Patterns
3.1 Adapter
Converts one interface into another that clients expect.
# Existing legacy interface
class OldPaymentSystem:
def make_payment(self, amount_in_cents):
print(f"Paid {amount_in_cents} cents")
# New interface
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount_in_dollars): ...
# Adapter
class PaymentAdapter(PaymentProcessor):
def __init__(self, old_system: OldPaymentSystem):
self.old_system = old_system
def pay(self, amount_in_dollars):
self.old_system.make_payment(int(amount_in_dollars * 100))
adapter = PaymentAdapter(OldPaymentSystem())
adapter.pay(9.99) # Paid 999 cents
3.2 Decorator
Dynamically adds responsibilities to objects.
class DataSource(ABC):
@abstractmethod
def write(self, data: str): ...
@abstractmethod
def read(self) -> str: ...
class FileDataSource(DataSource):
def __init__(self, filename):
self.filename = filename
def write(self, data):
with open(self.filename, 'w') as f:
f.write(data)
def read(self):
with open(self.filename) as f:
return f.read()
class EncryptionDecorator(DataSource):
def __init__(self, source: DataSource):
self.source = source
def write(self, data):
encrypted = data[::-1] # simplified "encryption"
self.source.write(encrypted)
def read(self):
return self.source.read()[::-1]
class CompressionDecorator(DataSource):
def __init__(self, source: DataSource):
self.source = source
def write(self, data):
import zlib
self.source.write(zlib.compress(data.encode()).hex())
def read(self):
import zlib
return zlib.decompress(bytes.fromhex(self.source.read())).decode()
# Composing decorators
source = EncryptionDecorator(CompressionDecorator(FileDataSource("data.txt")))
source.write("Hello, World!")
3.3 Proxy
class ImageProxy:
"""Lazy-loading proxy"""
def __init__(self, filename):
self.filename = filename
self._real_image = None
def display(self):
if self._real_image is None:
print(f"Loading {self.filename}...")
self._real_image = self._load_image()
self._real_image.display()
def _load_image(self):
return RealImage(self.filename)
3.4 Facade
Provides a simple interface to a complex subsystem.
class VideoConverter:
"""Facade: hides the complex video conversion subsystem"""
def convert(self, filename, format):
file = VideoFile(filename)
codec = CodecFactory.extract(file)
if format == "mp4":
result = MPEG4Compressor().compress(codec)
elif format == "avi":
result = AVICompressor().compress(codec)
return result
# The client only needs:
converter = VideoConverter()
mp4 = converter.convert("video.ogg", "mp4")
4. Behavioral Patterns
4.1 Observer
Automatically notifies dependents when an object's state changes.
class EventEmitter:
def __init__(self):
self._listeners = {}
def on(self, event, callback):
self._listeners.setdefault(event, []).append(callback)
def emit(self, event, *args, **kwargs):
for callback in self._listeners.get(event, []):
callback(*args, **kwargs)
# Usage
emitter = EventEmitter()
emitter.on("user_created", lambda user: print(f"Welcome {user}!"))
emitter.on("user_created", lambda user: send_email(user))
emitter.emit("user_created", "Alice")
4.2 Strategy
Defines a family of algorithms that are interchangeable.
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
class QuickSort(SortStrategy):
def sort(self, data):
if len(data) <= 1: return data
pivot = data[0]
left = [x for x in data[1:] if x <= pivot]
right = [x for x in data[1:] if x > pivot]
return self.sort(left) + [pivot] + self.sort(right)
class MergeSort(SortStrategy):
def sort(self, data):
# merge sort implementation
...
class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def sort(self, data):
return self.strategy.sort(data)
# Switch strategy at runtime
sorter = Sorter(QuickSort())
result = sorter.sort([3, 1, 4, 1, 5])
4.3 Command
Encapsulates requests as objects, supporting undo/redo.
class Command(ABC):
@abstractmethod
def execute(self): ...
@abstractmethod
def undo(self): ...
class InsertTextCommand(Command):
def __init__(self, editor, text, position):
self.editor = editor
self.text = text
self.position = position
def execute(self):
self.editor.insert(self.position, self.text)
def undo(self):
self.editor.delete(self.position, len(self.text))
class CommandHistory:
def __init__(self):
self._history = []
def execute(self, command):
command.execute()
self._history.append(command)
def undo(self):
if self._history:
command = self._history.pop()
command.undo()
4.4 Iterator
class BinaryTree:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def __iter__(self):
"""In-order traversal iterator"""
if self.left:
yield from self.left
yield self.value
if self.right:
yield from self.right
tree = BinaryTree(2, BinaryTree(1), BinaryTree(3))
for val in tree:
print(val) # 1, 2, 3
4.5 State
class State(ABC):
@abstractmethod
def handle(self, context): ...
class IdleState(State):
def handle(self, context):
print("Starting...")
context.state = RunningState()
class RunningState(State):
def handle(self, context):
print("Pausing...")
context.state = PausedState()
class PausedState(State):
def handle(self, context):
print("Resuming...")
context.state = RunningState()
class Player:
def __init__(self):
self.state = IdleState()
def press_button(self):
self.state.handle(self)
5. Anti-Patterns
| Anti-Pattern | Description | Improvement |
|---|---|---|
| God Object | One class takes on too many responsibilities | Split into multiple classes (SRP) |
| Spaghetti Code | Chaotic structure, lacks modularity | Introduce clear modules/layers |
| Golden Hammer | Using the same technology for every problem | Choose appropriate solutions per problem |
| Premature Optimization | Optimizing unimportant parts too early | Profile first, then optimize bottlenecks |
| Copy-Paste | Extensive duplicated code | Extract common methods/base classes |
| Magic Numbers | Hard-coded numbers in code | Use named constants |
| Singleton Abuse | Overuse of singletons | Dependency injection |
6. Design Pattern Selection Guide
Need to create objects?
├── One type, control instance count → Singleton
├── Multiple types, create based on conditions → Factory
├── Complex object construction process → Builder
└── Copy from a template → Prototype
Need to compose/wrap objects?
├── Incompatible interfaces → Adapter
├── Dynamically add functionality → Decorator
├── Control access → Proxy
└── Simplify complex interfaces → Facade
Need to manage inter-object interactions?
├── One-to-many notification → Observer
├── Swappable algorithms → Strategy
├── Support undo → Command
├── State-driven behavior → State
└── Uniform traversal → Iterator
Relations to Other Topics
- See Software Engineering Overview for the chapter-level role of design patterns
- See System Design for the relationship between local object structure and system-level boundaries
- See Full-Stack Development for how patterns appear in service layers and web applications
- See Testing and Quality Assurance for how good design reduces testing and regression cost
References
- "Design Patterns: Elements of Reusable Object-Oriented Software" - GoF
- "Head First Design Patterns" - Freeman & Robson
- "Clean Architecture" - Robert C. Martin
- Refactoring Guru: https://refactoring.guru/design-patterns