- Introduction
- What is the Single Responsibility Principle?
- Understanding "Responsibility"
- Why SRP Matters
- Common Violations and Code Smells
- Basic Examples
- Intermediate Examples
- Advanced Examples
- Real-World Applications
- Best Practices
- Common Pitfalls
- Exercises
- Summary
Welcome to the comprehensive tutorial on the Single Responsibility Principle (SRP) - the first and arguably most important principle in the SOLID design principles. Whether you're a beginner just starting with object-oriented programming or an experienced developer looking to refine your design skills, this tutorial will guide you through understanding and applying SRP effectively.
- 🎯 Core Concept: What SRP means and why it exists
- 🔍 Identification: How to spot SRP violations in code
- 🛠️ Application: How to refactor code to follow SRP
- 💡 Best Practices: Professional techniques for maintaining SRP
- 🚀 Real Examples: Practical scenarios from beginner to expert level
- Basic understanding of classes and objects
- Familiarity with Python syntax
- Understanding of methods and attributes
"A class should have only one reason to change." - Robert C. Martin
The Single Responsibility Principle states that a class should have only one job or responsibility. This means that a class should only have one reason to be modified.
Think of SRP like organizing your home:
- Kitchen → Cooking and food preparation
- Bedroom → Sleeping and rest
- Office → Work and study
- Bathroom → Personal hygiene
Each room has a single, clear purpose. You wouldn't cook in your bedroom or sleep in your kitchen because it would be confusing and inefficient. The same principle applies to classes in programming.
A class adheres to SRP when:
- It has one primary responsibility
- It has one reason to change
- All its methods and attributes support that single responsibility
A responsibility is a reason for change. To identify responsibilities, ask yourself:
- "What does this class do?" - List all the actions
- "Why would this class need to change?" - List all the reasons
- "Who would request changes to this class?" - Identify stakeholders
| Responsibility Type | Examples | Typical Changes |
|---|---|---|
| Data Management | Store, retrieve, validate data | Database schema changes, validation rules |
| Business Logic | Calculate, process, transform | Business rule changes, algorithm updates |
| User Interface | Display, format, present | UI design changes, layout modifications |
| Communication | Send emails, API calls, logging | Protocol changes, service endpoints |
| Configuration | Settings, preferences, options | Configuration format, default values |
If you can describe a class with "AND" instead of a single verb, it likely violates SRP:
- ❌ "This class manages users AND sends emails AND validates data"
- ✅ "This class manages users"
# ❌ Hard to maintain - multiple responsibilities
class UserManager:
def save_user(self, user): pass
def send_welcome_email(self, user): pass
def validate_email(self, email): pass
def generate_report(self, users): pass
# ✅ Easy to maintain - single responsibility
class UserRepository:
def save_user(self, user): pass
def get_user(self, user_id): pass- Focused tests: Each class has a clear testing scope
- Isolated testing: Changes in one area don't break unrelated tests
- Simpler mocking: Fewer dependencies to mock
- Clear purpose: Anyone can understand what the class does
- Predictable behavior: Methods relate to the class's main responsibility
- Better naming: Class names accurately reflect their purpose
- Independent changes: Modify one responsibility without affecting others
- Flexible design: Easy to swap implementations
- Parallel development: Teams can work on different responsibilities
- Focused functionality: Classes can be reused in different contexts
- Smaller interfaces: Easier to integrate with other systems
- Modular design: Components can be combined in various ways
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_pay(self):
# Business logic responsibility
return self.salary * 12
def save_to_database(self):
# Data persistence responsibility
# Database connection code...
pass
def generate_report(self):
# Reporting responsibility
return f"Employee: {self.name}, Annual Pay: {self.calculate_pay()}"Problems with this approach:
- 🚫 Database changes affect the Employee class
- 🚫 Report format changes affect the Employee class
- 🚫 Pay calculation changes affect the Employee class
- 🚫 Hard to test each responsibility independently
- 🚫 Multiple teams might need to modify the same class
class Employee:
"""Responsible for: Employee data representation"""
def __init__(self, name, salary):
self.name = name
self.salary = salary
class PayCalculator:
"""Responsible for: Pay calculation logic"""
def calculate_annual_pay(self, employee):
return employee.salary * 12
class EmployeeRepository:
"""Responsible for: Employee data persistence"""
def save(self, employee):
# Database operations
pass
class EmployeeReportGenerator:
"""Responsible for: Employee reporting"""
def __init__(self, pay_calculator):
self.pay_calculator = pay_calculator
def generate_report(self, employee):
annual_pay = self.pay_calculator.calculate_annual_pay(employee)
return f"Employee: {employee.name}, Annual Pay: {annual_pay}"Benefits of this approach:
- ✅ Independent changes: Each class can evolve separately
- ✅ Easy testing: Test each responsibility in isolation
- ✅ Team collaboration: Different teams can work on different classes
- ✅ Reusability: PayCalculator can be used for different employee types
- ✅ Clear ownership: Each class has a clear purpose and owner
- Large Classes (>200-300 lines)
- Many Methods (>10-15 methods)
- Multiple Import Statements from different domains
- Complex Constructor with many parameters
- Mixed Abstraction Levels in the same class
- Frequent Changes to the same class for different reasons
# ❌ Violates SRP - Too many responsibilities
class UserManager:
def __init__(self):
self.database = Database()
self.email_service = EmailService()
self.logger = Logger()
self.validator = Validator()
# User data management
def create_user(self, user_data): pass
def update_user(self, user_id, data): pass
def delete_user(self, user_id): pass
# Authentication
def login(self, username, password): pass
def logout(self, user_id): pass
def reset_password(self, email): pass
# Email operations
def send_welcome_email(self, user): pass
def send_notification(self, user, message): pass
# Reporting
def generate_user_report(self): pass
def export_users_csv(self): pass
# Validation
def validate_email(self, email): pass
def validate_password(self, password): pass# ❌ Violates SRP - Mixing high-level and low-level operations
class OrderProcessor:
def process_order(self, order):
# High-level business logic
if self.validate_order(order):
# Low-level database operations
connection = sqlite3.connect('orders.db')
cursor = connection.cursor()
cursor.execute("INSERT INTO orders...", order.data)
connection.commit()
# Low-level email operations
smtp_server = smtplib.SMTP('smtp.gmail.com', 587)
smtp_server.starttls()
smtp_server.login(username, password)
# ... email sending code# ❌ Violates SRP - Changes for different stakeholders
class Invoice:
def calculate_total(self):
# Finance team might change this
pass
def print_invoice(self):
# UI/UX team might change this
pass
def save_to_database(self):
# Database team might change this
pass
def send_by_email(self):
# IT/Infrastructure team might change this
pass# ✅ Separate responsibilities into different classes
class User:
"""Data representation only"""
def __init__(self, username, email):
self.username = username
self.email = email
class UserRepository:
"""Data persistence only"""
def save(self, user): pass
def find_by_id(self, user_id): pass
class UserAuthenticator:
"""Authentication only"""
def login(self, username, password): pass
def logout(self, user): pass
class EmailService:
"""Email operations only"""
def send_welcome_email(self, user): pass# ✅ Compose services for complex operations
class UserService:
def __init__(self, repository, authenticator, email_service):
self.repository = repository
self.authenticator = authenticator
self.email_service = email_service
def register_user(self, user_data):
user = User(user_data['username'], user_data['email'])
self.repository.save(user)
self.email_service.send_welcome_email(user)
return userclass User:
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = password
def save_to_file(self):
"""Responsibility: File I/O"""
with open('users.txt', 'a') as f:
f.write(f"{self.username},{self.email},{self.password}\n")
def send_welcome_email(self):
"""Responsibility: Email communication"""
print(f"Sending welcome email to {self.email}")
def validate_email(self):
"""Responsibility: Data validation"""
return '@' in self.email and '.' in self.email
def hash_password(self):
"""Responsibility: Security/Encryption"""
import hashlib
return hashlib.md5(self.password.encode()).hexdigest()Problems:
- User class has 4 different responsibilities
- Changes to file format affect User class
- Changes to email service affect User class
- Changes to validation rules affect User class
- Hard to test each responsibility independently
class User:
"""Responsibility: User data representation"""
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = password
class UserRepository:
"""Responsibility: User data persistence"""
def save_to_file(self, user):
with open('users.txt', 'a') as f:
f.write(f"{user.username},{user.email},{user.password}\n")
def load_from_file(self):
users = []
try:
with open('users.txt', 'r') as f:
for line in f:
username, email, password = line.strip().split(',')
users.append(User(username, email, password))
except FileNotFoundError:
pass
return users
class EmailService:
"""Responsibility: Email communication"""
def send_welcome_email(self, user):
print(f"Sending welcome email to {user.email}")
# In real implementation: SMTP, templates, etc.
class UserValidator:
"""Responsibility: User data validation"""
def validate_email(self, email):
return '@' in email and '.' in email
def validate_password(self, password):
return len(password) >= 8
class PasswordHasher:
"""Responsibility: Password security"""
def hash_password(self, password):
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
# Usage example
class UserService:
"""Orchestrates user operations using single-responsibility classes"""
def __init__(self):
self.repository = UserRepository()
self.email_service = EmailService()
self.validator = UserValidator()
self.hasher = PasswordHasher()
def register_user(self, username, email, password):
# Validate input
if not self.validator.validate_email(email):
raise ValueError("Invalid email")
if not self.validator.validate_password(password):
raise ValueError("Password too weak")
# Create user with hashed password
hashed_password = self.hasher.hash_password(password)
user = User(username, email, hashed_password)
# Save and notify
self.repository.save_to_file(user)
self.email_service.send_welcome_email(user)
return userBenefits:
- ✅ Each class has a single, clear responsibility
- ✅ Easy to test each component independently
- ✅ Changes to file format only affect UserRepository
- ✅ Changes to email service only affect EmailService
- ✅ Easy to swap implementations (file → database, email → SMS)
- ✅ Code is more readable and maintainable
class Order:
def __init__(self, customer_id, items):
self.customer_id = customer_id
self.items = items
self.status = "pending"
self.total = 0
def calculate_total(self):
"""Responsibility: Business logic - pricing"""
self.total = sum(item['price'] * item['quantity'] for item in self.items)
# Apply discounts
if self.total > 100:
self.total *= 0.9 # 10% discount
return self.total
def validate_order(self):
"""Responsibility: Business logic - validation"""
if not self.items:
return False
for item in self.items:
if item['quantity'] <= 0:
return False
return True
def save_to_database(self):
"""Responsibility: Data persistence"""
import sqlite3
conn = sqlite3.connect('orders.db')
cursor = conn.cursor()
cursor.execute("""
INSERT INTO orders (customer_id, total, status)
VALUES (?, ?, ?)
""", (self.customer_id, self.total, self.status))
conn.commit()
conn.close()
def send_confirmation_email(self):
"""Responsibility: Communication"""
print(f"Sending order confirmation email to customer {self.customer_id}")
# Email implementation...
def generate_invoice_pdf(self):
"""Responsibility: Document generation"""
print(f"Generating PDF invoice for order {self.customer_id}")
# PDF generation logic...
def process_payment(self, payment_method):
"""Responsibility: Payment processing"""
if payment_method == "credit_card":
print("Processing credit card payment...")
elif payment_method == "paypal":
print("Processing PayPal payment...")
return True
def update_inventory(self):
"""Responsibility: Inventory management"""
for item in self.items:
print(f"Reducing inventory for {item['name']} by {item['quantity']}")# Data Models
class Order:
"""Responsibility: Order data representation"""
def __init__(self, customer_id, items):
self.customer_id = customer_id
self.items = items
self.status = "pending"
self.total = 0
self.created_at = datetime.now()
class OrderItem:
"""Responsibility: Order item data representation"""
def __init__(self, product_id, name, price, quantity):
self.product_id = product_id
self.name = name
self.price = price
self.quantity = quantity
# Business Logic
class PricingCalculator:
"""Responsibility: Price calculations and discounts"""
def calculate_order_total(self, order):
subtotal = sum(item.price * item.quantity for item in order.items)
return self.apply_discounts(subtotal, order)
def apply_discounts(self, subtotal, order):
if subtotal > 100:
return subtotal * 0.9 # 10% discount
return subtotal
class OrderValidator:
"""Responsibility: Order validation logic"""
def validate_order(self, order):
if not order.items:
raise ValueError("Order must contain at least one item")
for item in order.items:
if item.quantity <= 0:
raise ValueError(f"Invalid quantity for {item.name}")
return True
# Data Persistence
class OrderRepository:
"""Responsibility: Order data persistence"""
def save(self, order):
import sqlite3
conn = sqlite3.connect('orders.db')
cursor = conn.cursor()
cursor.execute("""
INSERT INTO orders (customer_id, total, status, created_at)
VALUES (?, ?, ?, ?)
""", (order.customer_id, order.total, order.status, order.created_at))
order.id = cursor.lastrowid
conn.commit()
conn.close()
def find_by_id(self, order_id):
# Implementation to retrieve order
pass
# Communication
class NotificationService:
"""Responsibility: Customer notifications"""
def send_order_confirmation(self, order, customer_email):
print(f"Sending order confirmation email to {customer_email}")
# Email implementation...
# Document Generation
class InvoiceGenerator:
"""Responsibility: Invoice document creation"""
def generate_pdf(self, order):
print(f"Generating PDF invoice for order {order.id}")
# PDF generation logic...
# Payment Processing
class PaymentProcessor:
"""Responsibility: Payment handling"""
def process_payment(self, order, payment_method, payment_details):
if payment_method == "credit_card":
return self._process_credit_card(payment_details)
elif payment_method == "paypal":
return self._process_paypal(payment_details)
else:
raise ValueError("Unsupported payment method")
def _process_credit_card(self, details):
print("Processing credit card payment...")
return {"status": "success", "transaction_id": "cc_123"}
def _process_paypal(self, details):
print("Processing PayPal payment...")
return {"status": "success", "transaction_id": "pp_456"}
# Inventory Management
class InventoryManager:
"""Responsibility: Inventory updates"""
def reserve_items(self, order):
for item in order.items:
print(f"Reserving {item.quantity} units of {item.name}")
def release_items(self, order):
for item in order.items:
print(f"Releasing {item.quantity} units of {item.name}")
# Orchestration Service
class OrderService:
"""Responsibility: Orchestrating order processing workflow"""
def __init__(self):
self.validator = OrderValidator()
self.pricing_calculator = PricingCalculator()
self.repository = OrderRepository()
self.notification_service = NotificationService()
self.invoice_generator = InvoiceGenerator()
self.payment_processor = PaymentProcessor()
self.inventory_manager = InventoryManager()
def process_order(self, order, payment_method, payment_details, customer_email):
try:
# Validate order
self.validator.validate_order(order)
# Calculate pricing
order.total = self.pricing_calculator.calculate_order_total(order)
# Reserve inventory
self.inventory_manager.reserve_items(order)
# Process payment
payment_result = self.payment_processor.process_payment(
order, payment_method, payment_details
)
if payment_result["status"] == "success":
# Save order
order.status = "confirmed"
self.repository.save(order)
# Send notifications
self.notification_service.send_order_confirmation(order, customer_email)
# Generate invoice
self.invoice_generator.generate_pdf(order)
return order
else:
# Release inventory on payment failure
self.inventory_manager.release_items(order)
raise Exception("Payment failed")
except Exception as e:
order.status = "failed"
self.inventory_manager.release_items(order)
raise eKey Improvements:
- ✅ 8 separate classes each with a single responsibility
- ✅ Easy testing: Each component can be tested independently
- ✅ Flexible payment: Easy to add new payment methods
- ✅ Swappable storage: Can change from SQLite to PostgreSQL
- ✅ Independent changes: Pricing changes don't affect email logic
- ✅ Clear ownership: Each team can own specific components
# Domain Models
class Article:
"""Responsibility: Article data representation"""
def __init__(self, title, content, author_id, category_id):
self.id = None
self.title = title
self.content = content
self.author_id = author_id
self.category_id = category_id
self.status = "draft"
self.created_at = datetime.now()
self.updated_at = datetime.now()
self.view_count = 0
class Author:
"""Responsibility: Author data representation"""
def __init__(self, name, email, bio=""):
self.id = None
self.name = name
self.email = email
self.bio = bio
# Content Processing
class ContentProcessor:
"""Responsibility: Content formatting and processing"""
def process_markdown(self, content):
# Convert markdown to HTML
return content.replace("**", "<strong>").replace("**", "</strong>")
def extract_summary(self, content, max_length=200):
if len(content) <= max_length:
return content
return content[:max_length] + "..."
def extract_tags(self, content):
# Extract hashtags from content
import re
return re.findall(r'#(\w+)', content)
class ContentValidator:
"""Responsibility: Content validation rules"""
def validate_article(self, article):
errors = []
if not article.title or len(article.title.strip()) < 5:
errors.append("Title must be at least 5 characters")
if not article.content or len(article.content.strip()) < 50:
errors.append("Content must be at least 50 characters")
if self._contains_prohibited_words(article.content):
errors.append("Content contains prohibited words")
return errors
def _contains_prohibited_words(self, content):
prohibited = ["spam", "fake", "scam"]
return any(word in content.lower() for word in prohibited)
# Data Access Layer
class ArticleRepository:
"""Responsibility: Article data persistence"""
def save(self, article):
# Database save logic
pass
def find_by_id(self, article_id):
# Database retrieval logic
pass
def find_by_author(self, author_id):
# Find articles by author
pass
def find_published(self, limit=10, offset=0):
# Find published articles with pagination
pass
class AuthorRepository:
"""Responsibility: Author data persistence"""
def save(self, author):
pass
def find_by_email(self, email):
pass
# Search and Analytics
class SearchEngine:
"""Responsibility: Article search functionality"""
def search_articles(self, query, filters=None):
# Implement search logic (could use Elasticsearch, etc.)
pass
def get_related_articles(self, article, limit=5):
# Find related articles based on tags, category, etc.
pass
class AnalyticsTracker:
"""Responsibility: Usage analytics and metrics"""
def track_article_view(self, article_id, user_id=None):
# Track article views
pass
def get_popular_articles(self, time_period="week"):
# Get most viewed articles
pass
def get_author_stats(self, author_id):
# Get author performance metrics
pass
# Caching Layer
class CacheManager:
"""Responsibility: Content caching"""
def get_cached_article(self, article_id):
# Get article from cache (Redis, Memcached, etc.)
pass
def cache_article(self, article):
# Cache article for faster access
pass
def invalidate_cache(self, article_id):
# Remove from cache when updated
pass
# Notification System
class NotificationService:
"""Responsibility: User notifications"""
def notify_new_article(self, article, subscribers):
# Notify subscribers of new article
pass
def notify_article_published(self, article):
# Notify author when article is published
pass
# Publishing Workflow
class PublishingWorkflow:
"""Responsibility: Article publishing process"""
def __init__(self):
self.states = ["draft", "review", "approved", "published", "archived"]
def can_transition(self, from_state, to_state):
transitions = {
"draft": ["review"],
"review": ["draft", "approved"],
"approved": ["published"],
"published": ["archived"]
}
return to_state in transitions.get(from_state, [])
def transition_article(self, article, new_state, user_role):
if not self.can_transition(article.status, new_state):
raise ValueError(f"Cannot transition from {article.status} to {new_state}")
if new_state == "approved" and user_role != "editor":
raise PermissionError("Only editors can approve articles")
article.status = new_state
article.updated_at = datetime.now()
# Main Service Layer
class ArticleService:
"""Responsibility: Orchestrating article operations"""
def __init__(self):
self.article_repo = ArticleRepository()
self.author_repo = AuthorRepository()
self.content_processor = ContentProcessor()
self.validator = ContentValidator()
self.search_engine = SearchEngine()
self.analytics = AnalyticsTracker()
self.cache = CacheManager()
self.notifications = NotificationService()
self.workflow = PublishingWorkflow()
def create_article(self, title, content, author_id, category_id):
# Create new article
article = Article(title, content, author_id, category_id)
# Validate content
errors = self.validator.validate_article(article)
if errors:
raise ValueError(f"Validation failed: {', '.join(errors)}")
# Process content
article.content = self.content_processor.process_markdown(content)
# Save article
self.article_repo.save(article)
return article
def publish_article(self, article_id, user_role):
article = self.article_repo.find_by_id(article_id)
# Use workflow to manage state transitions
self.workflow.transition_article(article, "published", user_role)
# Save updated article
self.article_repo.save(article)
# Cache published article
self.cache.cache_article(article)
# Send notifications
self.notifications.notify_article_published(article)
return article
def get_article(self, article_id, user_id=None):
# Try cache first
article = self.cache.get_cached_article(article_id)
if not article:
article = self.article_repo.find_by_id(article_id)
if article and article.status == "published":
self.cache.cache_article(article)
# Track view
if article and user_id:
self.analytics.track_article_view(article_id, user_id)
return articleAdvanced Benefits:
- ✅ Microservice-ready: Each class could become a separate service
- ✅ Testable architecture: Mock any component for testing
- ✅ Scalable design: Cache, search, and analytics can scale independently
- ✅ Maintainable workflow: Publishing logic is isolated and configurable
- ✅ Performance optimized: Caching and analytics don't affect core logic
# ❌ Violation: Account class doing everything
class BankAccount:
def deposit(self, amount): pass
def withdraw(self, amount): pass
def send_sms_notification(self): pass
def calculate_interest(self): pass
def generate_statement(self): pass
def validate_transaction(self): pass
# ✅ SRP Applied: Separate responsibilities
class Account:
"""Data representation"""
pass
class TransactionProcessor:
"""Transaction operations"""
pass
class NotificationService:
"""Customer communications"""
pass
class InterestCalculator:
"""Interest calculations"""
pass
class StatementGenerator:
"""Document generation"""
pass
class TransactionValidator:
"""Business rule validation"""
pass# Separate responsibilities for different stakeholders
class Course:
"""Course data - Content team"""
pass
class EnrollmentManager:
"""Enrollment logic - Academic team"""
pass
class ProgressTracker:
"""Learning analytics - Data team"""
pass
class CertificateGenerator:
"""Certification - Compliance team"""
pass
class PaymentProcessor:
"""Billing - Finance team"""
passclass Patient:
"""Patient data - Medical records team"""
pass
class AppointmentScheduler:
"""Scheduling - Operations team"""
pass
class BillingCalculator:
"""Insurance/billing - Finance team"""
pass
class MedicalReportGenerator:
"""Reports - Clinical team"""
pass
class NotificationService:
"""Communications - IT team"""
pass# Models (Data representation)
class User(models.Model):
username = models.CharField(max_length=100)
email = models.EmailField()
# Services (Business logic)
class UserRegistrationService:
def __init__(self):
self.validator = UserValidator()
self.email_service = EmailService()
self.repository = UserRepository()
# Views (HTTP handling)
class UserRegistrationView:
def post(self, request):
service = UserRegistrationService()
return service.register_user(request.data)
# Serializers (Data transformation)
class UserSerializer:
def serialize(self, user):
return {"id": user.id, "username": user.username}# Separate endpoints for different responsibilities
class UserController:
"""User CRUD operations"""
def get_user(self, user_id): pass
def update_user(self, user_id, data): pass
class AuthController:
"""Authentication operations"""
def login(self, credentials): pass
def logout(self, token): pass
class ProfileController:
"""Profile management"""
def update_profile(self, user_id, profile_data): pass
def upload_avatar(self, user_id, image): pass# ✅ Clear, single-purpose names
class UserValidator: # Validates users
class EmailSender: # Sends emails
class PaymentProcessor: # Processes payments
class ReportGenerator: # Generates reports
# ❌ Vague or multi-purpose names
class UserManager: # What does it manage?
class DataHandler: # What kind of data?
class SystemUtility: # Too genericclass OrderValidator:
"""All methods should support the validation responsibility"""
def validate_order(self, order):
"""Main validation method"""
pass
def validate_items(self, items):
"""Helper validation method"""
pass
def validate_customer(self, customer):
"""Helper validation method"""
pass
# ❌ Don't add unrelated methods
# def send_email(self, order): # This belongs in EmailService
# def calculate_total(self, order): # This belongs in PricingCalculatorclass OrderService:
def __init__(self, validator, repository, email_service):
self.validator = validator
self.repository = repository
self.email_service = email_service
def process_order(self, order):
if self.validator.validate_order(order):
self.repository.save(order)
self.email_service.send_confirmation(order)class ServiceFactory:
@staticmethod
def create_order_service():
validator = OrderValidator()
repository = OrderRepository()
email_service = EmailService()
return OrderService(validator, repository, email_service)import unittest
from unittest.mock import Mock
class TestOrderValidator(unittest.TestCase):
def setUp(self):
self.validator = OrderValidator()
def test_valid_order_passes_validation(self):
order = Order(customer_id=1, items=[{"name": "item", "quantity": 1}])
result = self.validator.validate_order(order)
self.assertTrue(result)
def test_empty_order_fails_validation(self):
order = Order(customer_id=1, items=[])
with self.assertRaises(ValueError):
self.validator.validate_order(order)
class TestOrderService(unittest.TestCase):
def setUp(self):
self.mock_validator = Mock()
self.mock_repository = Mock()
self.mock_email_service = Mock()
self.service = OrderService(
self.mock_validator,
self.mock_repository,
self.mock_email_service
)
def test_process_order_with_valid_order(self):
order = Order(customer_id=1, items=[{"name": "item", "quantity": 1}])
self.mock_validator.validate_order.return_value = True
result = self.service.process_order(order)
self.mock_validator.validate_order.assert_called_once_with(order)
self.mock_repository.save.assert_called_once_with(order)
self.mock_email_service.send_confirmation.assert_called_once_with(order)class DatabaseConfig:
"""Responsibility: Database configuration"""
def __init__(self):
self.host = os.getenv('DB_HOST', 'localhost')
self.port = os.getenv('DB_PORT', 5432)
self.database = os.getenv('DB_NAME', 'myapp')
class EmailConfig:
"""Responsibility: Email configuration"""
def __init__(self):
self.smtp_host = os.getenv('SMTP_HOST')
self.smtp_port = os.getenv('SMTP_PORT', 587)
self.username = os.getenv('SMTP_USERNAME')
class AppConfig:
"""Responsibility: Application configuration orchestration"""
def __init__(self):
self.database = DatabaseConfig()
self.email = EmailConfig()
self.debug = os.getenv('DEBUG', 'False').lower() == 'true'class ValidationError(Exception):
"""Raised by validators"""
pass
class PersistenceError(Exception):
"""Raised by repositories"""
pass
class CommunicationError(Exception):
"""Raised by notification services"""
pass
# Each service handles its own error types
class OrderValidator:
def validate_order(self, order):
if not order.items:
raise ValidationError("Order must contain items")
class OrderRepository:
def save(self, order):
try:
# Database save logic
pass
except DatabaseError as e:
raise PersistenceError(f"Failed to save order: {e}")# Don't create a class for every single method
class StringUppercaser:
def uppercase(self, text):
return text.upper()
class StringLowercaser:
def lowercase(self, text):
return text.lower()
# ✅ Group related functionality appropriately
class StringFormatter:
def uppercase(self, text):
return text.upper()
def lowercase(self, text):
return text.lower()
def title_case(self, text):
return text.title()# Don't create interfaces for everything immediately
class IUserValidator:
def validate(self, user): pass
class IUserRepository:
def save(self, user): pass
# ✅ Start simple, add abstractions when needed
class UserValidator:
def validate(self, user): pass
class UserRepository:
def save(self, user): pass# Technical responsibility (how) vs Business responsibility (what)
class UserService:
def save_user_to_mysql(self, user): # ❌ Technical detail leaked
pass
def save_user_via_http_api(self, user): # ❌ Technical detail leaked
pass
# ✅ Focus on business responsibility
class UserService:
def save_user(self, user): # ✅ Business operation
pass
class MySQLUserRepository: # ✅ Technical implementation separate
def save(self, user):
pass# For small applications, this might be overkill:
class SimpleCalculator:
def add(self, a, b):
return a + b
class AdditionValidator:
def validate_numbers(self, a, b):
return isinstance(a, (int, float)) and isinstance(b, (int, float))
class AdditionLogger:
def log_operation(self, a, b, result):
print(f"{a} + {b} = {result}")
# For small apps, this is often sufficient:
class Calculator:
def add(self, a, b):
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise ValueError("Arguments must be numbers")
result = a + b
print(f"{a} + {b} = {result}")
return resultclass UserService:
def __init__(self):
self.order_service = OrderService() # ❌ Circular dependency
def get_user_orders(self, user_id):
return self.order_service.get_orders_by_user(user_id)
class OrderService:
def __init__(self):
self.user_service = UserService() # ❌ Circular dependency
def get_order_user(self, order_id):
order = self.get_order(order_id)
return self.user_service.get_user(order.user_id)class UserRepository:
def find_by_id(self, user_id): pass
class OrderRepository:
def find_by_user_id(self, user_id): pass
def find_by_id(self, order_id): pass
class UserService:
def __init__(self, user_repo, order_repo):
self.user_repo = user_repo
self.order_repo = order_repo
def get_user_with_orders(self, user_id):
user = self.user_repo.find_by_id(user_id)
orders = self.order_repo.find_by_user_id(user_id)
return {"user": user, "orders": orders}Now it's time to apply what you've learned! Work through these exercises to master the Single Responsibility Principle.
File: 01-basic-single-responsibility.py
Problem: You have a User class that handles user information, authentication, and email notifications. Refactor it to follow SRP.
Your Task:
- Identify the different responsibilities in the current code
- Create separate classes for each responsibility
- Ensure each class has a single, clear purpose
- Test your refactored code
Learning Goals:
- Recognize SRP violations
- Practice extracting classes
- Understand single responsibility benefits
File: 02-advanced-single-responsibility.py
Problem: You have an Order class that handles order processing, payment, inventory, and notifications. This is a complex real-world scenario.
Your Task:
- Analyze the multiple responsibilities
- Design a proper class structure
- Implement proper dependency injection
- Create a service layer to orchestrate operations
Learning Goals:
- Handle complex business logic
- Design service architectures
- Manage dependencies properly
- Apply SRP in real-world scenarios
Before looking at the solutions, ask yourself:
- Identification: Can you list all the responsibilities in the original class?
- Separation: What would be the name and purpose of each new class?
- Dependencies: How would these classes interact with each other?
- Testing: How would you test each class independently?
- Benefits: What specific benefits does your refactoring provide?
When reviewing your solution:
- Single Purpose: Each class has one clear responsibility
- Clear Naming: Class names clearly indicate their purpose
- Cohesive Methods: All methods in a class support its responsibility
- Loose Coupling: Classes don't depend on implementation details
- Easy Testing: Each class can be tested independently
- Future Changes: Changes to one responsibility don't affect others
Ready for more? Try these additional challenges:
Create a library system with these requirements:
- Book management (add, remove, search)
- Member management (register, update, deactivate)
- Borrowing system (checkout, return, renewals)
- Fine calculation (overdue fees, damage fees)
- Notification system (due date reminders, overdue notices)
- Report generation (popular books, member activity)
Focus: Design this system following SRP from the start.
Design a social media platform with:
- User profiles and authentication
- Post creation and management
- Friend/follower relationships
- News feed generation
- Notification system
- Content moderation
- Analytics and metrics
Focus: Handle complex interactions while maintaining SRP.
Take an existing e-commerce codebase (or create a monolithic one) and refactor it to follow SRP. Include:
- Product catalog
- Shopping cart
- Order processing
- Payment handling
- Inventory management
- Customer service
- Analytics
Focus: Practice refactoring existing code to follow SRP.
- Definition: A class should have only one reason to change
- Purpose: Each class should have a single, well-defined responsibility
- Goal: Create focused, maintainable, and testable code
- Identify Responsibilities: Ask "What does this class do?" and "Why would it change?"
- Separate Concerns: Extract different responsibilities into separate classes
- Use Composition: Combine single-responsibility classes to create complex behavior
- Name Clearly: Use descriptive names that reflect the single responsibility
- ✅ Easier Maintenance: Changes are localized to specific responsibilities
- ✅ Better Testing: Each responsibility can be tested independently
- ✅ Improved Readability: Code purpose is clear and predictable
- ✅ Reduced Coupling: Classes are less dependent on each other
- ✅ Enhanced Reusability: Focused classes can be reused in different contexts
- Data Models: Represent data structure only
- Repositories: Handle data persistence
- Services: Implement business logic
- Validators: Enforce business rules
- Processors: Transform or manipulate data
- Notifiers: Handle communications
- Classes with "AND" in their description
- Large classes (>200-300 lines)
- Many methods (>10-15 methods)
- Multiple import statements from different domains
- Frequent changes for different reasons
- Mixed abstraction levels
- Clear, single-purpose class names
- Cohesive method groups
- Single reason to change
- Easy to test independently
- Clear dependencies
- Focused documentation
- Recognize obvious SRP violations
- Extract simple responsibilities (validation, formatting)
- Practice with basic examples
- Focus on clear naming
- Handle complex business scenarios
- Design service layers
- Manage dependencies properly
- Apply SRP in web applications
- Design microservice-ready architectures
- Handle cross-cutting concerns
- Balance SRP with performance
- Mentor others in SRP application
- Code Reviews: Look for SRP violations in pull requests
- New Features: Design new classes following SRP from the start
- Refactoring: Gradually extract responsibilities from large classes
- Architecture: Use SRP to guide system design decisions
- Coding Standards: Include SRP guidelines in your team's standards
- Design Reviews: Discuss responsibilities during design sessions
- Training: Share SRP knowledge with team members
- Metrics: Track class sizes and complexity as SRP indicators
- Practice: Work through the exercises multiple times
- Apply: Use SRP in your current projects
- Study: Look at well-designed open-source projects
- Teach: Explain SRP to others to deepen your understanding
- Open/Closed Principle: Build on SRP for extensible design
- Dependency Injection: Manage SRP class dependencies
- Design Patterns: Apply patterns that support SRP
- Clean Architecture: Use SRP as a foundation for clean architecture
The Single Responsibility Principle is more than just a coding guideline—it's a way of thinking about software design. When you consistently apply SRP:
- Your code becomes more professional and easier to work with
- Your team becomes more productive with clearer responsibilities
- Your applications become more maintainable and adaptable to change
- Your skills as a developer improve through disciplined design thinking
Remember: Perfect is the enemy of good. Start applying SRP gradually, learn from experience, and continuously improve your design skills.
- "Clean Code" by Robert C. Martin
- "Clean Architecture" by Robert C. Martin
- "Refactoring" by Martin Fowler
- Work through the provided exercises
- Contribute to open-source projects
- Code review sessions with peers
- Design pattern practice problems
🎉 Congratulations! You've completed the comprehensive Single Responsibility Principle tutorial. You're now equipped with the knowledge and skills to write more maintainable, testable, and professional code.
Ready for the next challenge? Continue with the Open/Closed Principle to further enhance your SOLID design skills!
Happy coding! 🚀