Skip to content

Single Responsibility Principle

A class should have only one reason to change.

In this chapter, we will explore the SOLID principles, a set of five design principles that help developers create maintainable and scalable software. These principles were introduced by Robert C. Martin and are widely regarded as best practices in object-oriented programming.

For starters, SOLID is an acronym that stands for:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

The Single Responsibility Principle (SRP) states that a software component (class, module, function, etc.) must have only one responsibility or one reason to change. This means that each component should be responsible for a single functionality or behavior.

If you find yourself modifying a class for different reasons, it’s a sign that the abstraction is incorrect and the class has too many responsibilities. This indicates that you need to create more objects to address the extra responsibilities.

Objects that do one thing, and just one thing, are:

  • Easier to maintain - Changes are isolated
  • Easier to understand - Clear purpose
  • Easier to test - Focused test cases
  • Less prone to bugs - Fewer side effects

We want to avoid “God Objects” that know too much or do too much.

Consider a user management system where a single class handles user data, authentication, and email notifications. This violates SRP because the class has multiple reasons to change.

Diagram
bad_user.py
class User:
def __init__(self, username: str, email: str, password: str):
self.username = username
self.email = email
self.password = password
def validate(self):
"""Responsibility 1: Validation"""
if not self.username:
raise ValueError("Username is required")
if "@" not in self.email:
raise ValueError("Invalid email")
return True
def save_to_database(self):
"""Responsibility 2: Database persistence"""
# Database connection and save logic
print(f"Saving {self.username} to database...")
def send_welcome_email(self):
"""Responsibility 3: Email notification"""
# Email sending logic
print(f"Sending welcome email to {self.email}...")
def hash_password(self):
"""Responsibility 4: Security"""
# Password hashing logic
import hashlib
self.password = hashlib.sha256(self.password.encode()).hexdigest()
Diagram
user.py
class User:
"""Single responsibility: Represent user data"""
def __init__(self, username: str, email: str, password: str):
self.username = username
self.email = email
self.password = password
class UserValidator:
"""Single responsibility: Validate user data"""
def validate(self, user: User) -> bool:
if not user.username:
raise ValueError("Username is required")
if "@" not in user.email:
raise ValueError("Invalid email")
return True
class UserRepository:
"""Single responsibility: Handle database operations"""
def save(self, user: User):
print(f"Saving {user.username} to database...")
# Database save logic
class EmailService:
"""Single responsibility: Handle email notifications"""
def send_welcome_email(self, email: str):
print(f"Sending welcome email to {email}...")
# Email sending logic
class PasswordHasher:
"""Single responsibility: Handle password security"""
def hash(self, password: str) -> str:
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
# Usage
user = User("john_doe", "john@example.com", "secret123")
validator = UserValidator()
hasher = PasswordHasher()
if validator.validate(user):
user.password = hasher.hash(user.password)
repository = UserRepository()
repository.save(user)
email_service = EmailService()
email_service.send_welcome_email(user.email)
classDiagram
    class User {
        -username: str
        -email: str
        -password: str
    }
    
    class UserValidator {
        +validate(user: User) bool
    }
    
    class UserRepository {
        +save(user: User)
    }
    
    class EmailService {
        +send_welcome_email(email: str)
    }
    
    class PasswordHasher {
        +hash(password: str) str
    }
    
    UserValidator ..> User : validates
    UserRepository ..> User : saves
    EmailService ..> User : uses email
    PasswordHasher ..> User : hashes password

Another common violation occurs when a single class handles HTTP requests, business logic, and data formatting.

bad_api_handler.py
class APIHandler:
def __init__(self):
self.db_connection = None # Database connection
def handle_request(self, request_data: dict):
"""Responsibility 1: Handle HTTP request"""
# Parse request
user_id = request_data.get("user_id")
"""Responsibility 2: Business logic"""
# Fetch user from database
user = self.fetch_user(user_id)
if not user:
return {"error": "User not found"}
"""Responsibility 3: Format response"""
# Format JSON response
return {
"status": "success",
"data": {
"id": user["id"],
"name": user["name"],
"email": user["email"]
}
}
def fetch_user(self, user_id: int):
"""Responsibility 4: Database operations"""
# Database query logic
return {"id": user_id, "name": "John", "email": "john@example.com"}
api_handler.py
class UserService:
"""Single responsibility: Business logic"""
def __init__(self, repository):
self.repository = repository
def get_user(self, user_id: int):
user = self.repository.find_by_id(user_id)
if not user:
raise ValueError("User not found")
return user
class UserRepository:
"""Single responsibility: Data access"""
def find_by_id(self, user_id: int):
# Database query logic
return {"id": user_id, "name": "John", "email": "john@example.com"}
class ResponseFormatter:
"""Single responsibility: Response formatting"""
def format_success(self, data: dict):
return {
"status": "success",
"data": data
}
def format_error(self, error: str):
return {
"status": "error",
"message": error
}
class APIHandler:
"""Single responsibility: HTTP request handling"""
def __init__(self):
self.user_service = UserService(UserRepository())
self.formatter = ResponseFormatter()
def handle_request(self, request_data: dict):
try:
user_id = request_data.get("user_id")
user = self.user_service.get_user(user_id)
return self.formatter.format_success(user)
except ValueError as e:
return self.formatter.format_error(str(e))
classDiagram
    class APIHandler {
        -user_service: UserService
        -formatter: ResponseFormatter
        +handle_request(request_data: dict)
    }
    
    class UserService {
        -repository: UserRepository
        +get_user(user_id: int)
    }
    
    class UserRepository {
        +find_by_id(user_id: int)
    }
    
    class ResponseFormatter {
        +format_success(data: dict)
        +format_error(error: str)
    }
    
    APIHandler --> UserService
    APIHandler --> ResponseFormatter
    UserService --> UserRepository

A report system that processes data, generates reports, and handles file operations violates SRP.

Diagram
bad_report.py
class Report:
def __init__(self, data):
self.data = data
def process_data(self):
"""Responsibility 1: Data processing"""
# Process and clean data
return [item for item in self.data if item.get("active")]
def generate_report(self):
"""Responsibility 2: Report generation"""
processed = self.process_data()
return f"Report: {len(processed)} items"
def save_to_file(self, filename: str):
"""Responsibility 3: File operations"""
report = self.generate_report()
with open(filename, 'w') as f:
f.write(report)
def send_email(self, recipient: str):
"""Responsibility 4: Email notification"""
report = self.generate_report()
print(f"Sending report to {recipient}: {report}")
Diagram
report.py
class DataProcessor:
"""Single responsibility: Process and clean data"""
def __init__(self, data):
self.data = data
def process(self):
return [item for item in self.data if item.get("active")]
class ReportGenerator:
"""Single responsibility: Generate report content"""
def __init__(self, processed_data):
self.processed_data = processed_data
def generate(self) -> str:
return f"Report: {len(self.processed_data)} items"
class FileManager:
"""Single responsibility: Handle file operations"""
def save(self, content: str, filename: str):
with open(filename, 'w') as f:
f.write(content)
class EmailNotifier:
"""Single responsibility: Handle email notifications"""
def send(self, content: str, recipient: str):
print(f"Sending report to {recipient}: {content}")
# Usage
data = [{"id": 1, "active": True}, {"id": 2, "active": False}]
processor = DataProcessor(data)
processed = processor.process()
generator = ReportGenerator(processed)
report_content = generator.generate()
file_manager = FileManager()
file_manager.save(report_content, "report.txt")
email_notifier = EmailNotifier()
email_notifier.send(report_content, "manager@company.com")

Remember: SRP is about cohesion - keeping related things together and unrelated things apart.