Single Responsibility Principle
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
Section titled “The Single Responsibility Principle”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.
Understanding the Principle
Section titled “Understanding the Principle”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.
Example 1: User Management System
Section titled “Example 1: User Management System”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.
Violating SRP (Bad Approach)
Section titled “Violating SRP (Bad Approach)”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()public class User { private String username; private String email; private String password;
public User(String username, String email, String password) { this.username = username; this.email = email; this.password = password; }
// Responsibility 1: Validation public boolean validate() { if (username == null || username.isEmpty()) { throw new IllegalArgumentException("Username is required"); } if (email == null || !email.contains("@")) { throw new IllegalArgumentException("Invalid email"); } return true; }
// Responsibility 2: Database persistence public void saveToDatabase() { // Database connection and save logic System.out.println("Saving " + username + " to database..."); }
// Responsibility 3: Email notification public void sendWelcomeEmail() { // Email sending logic System.out.println("Sending welcome email to " + email + "..."); }
// Responsibility 4: Security public void hashPassword() { // Password hashing logic try { java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(password.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b)); } this.password = sb.toString(); } catch (java.security.NoSuchAlgorithmException e) { throw new RuntimeException(e); } }}Following SRP (Good Approach)
Section titled “Following SRP (Good Approach)”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()
# Usageuser = 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)// Single responsibility: Represent user datapublic class User { private String username; private String email; private String password;
public User(String username, String email, String password) { this.username = username; this.email = email; this.password = password; }
// Getters and setters public String getUsername() { return username; } public String getEmail() { return email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }}
// Single responsibility: Validate user datapublic class UserValidator { public boolean validate(User user) { if (user.getUsername() == null || user.getUsername().isEmpty()) { throw new IllegalArgumentException("Username is required"); } if (user.getEmail() == null || !user.getEmail().contains("@")) { throw new IllegalArgumentException("Invalid email"); } return true; }}
// Single responsibility: Handle database operationspublic class UserRepository { public void save(User user) { System.out.println("Saving " + user.getUsername() + " to database..."); // Database save logic }}
// Single responsibility: Handle email notificationspublic class EmailService { public void sendWelcomeEmail(String email) { System.out.println("Sending welcome email to " + email + "..."); // Email sending logic }}
// Single responsibility: Handle password securitypublic class PasswordHasher { public String hash(String password) { try { java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(password.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : hash) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (java.security.NoSuchAlgorithmException e) { throw new RuntimeException(e); } }}
// Usagepublic class Main { public static void main(String[] args) { User user = new User("john_doe", "john@example.com", "secret123"); UserValidator validator = new UserValidator(); PasswordHasher hasher = new PasswordHasher();
if (validator.validate(user)) { user.setPassword(hasher.hash(user.getPassword())); UserRepository repository = new UserRepository(); repository.save(user); EmailService emailService = new EmailService(); emailService.sendWelcomeEmail(user.getEmail()); } }}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
Example 2: API Request Handler
Section titled “Example 2: API Request Handler”Another common violation occurs when a single class handles HTTP requests, business logic, and data formatting.
Violating SRP (Bad Approach)
Section titled “Violating SRP (Bad Approach)”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"}import java.util.Map;import java.util.HashMap;
public class APIHandler { private Object dbConnection; // Database connection
// Responsibility 1: Handle HTTP request // Responsibility 2: Business logic // Responsibility 3: Format response // Responsibility 4: Database operations public Map<String, Object> handleRequest(Map<String, Object> requestData) { // Parse request Integer userId = (Integer) requestData.get("user_id");
// Business logic: Fetch user from database Map<String, Object> user = fetchUser(userId); if (user == null) { Map<String, Object> error = new HashMap<>(); error.put("error", "User not found"); return error; }
// Format JSON response Map<String, Object> response = new HashMap<>(); response.put("status", "success");
Map<String, Object> data = new HashMap<>(); data.put("id", user.get("id")); data.put("name", user.get("name")); data.put("email", user.get("email")); response.put("data", data);
return response; }
// Responsibility 4: Database operations private Map<String, Object> fetchUser(int userId) { // Database query logic Map<String, Object> user = new HashMap<>(); user.put("id", userId); user.put("name", "John"); user.put("email", "john@example.com"); return user; }}Following SRP (Good Approach)
Section titled “Following SRP (Good Approach)”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))import java.util.Map;import java.util.HashMap;
// Single responsibility: Business logicpublic class UserService { private UserRepository repository;
public UserService(UserRepository repository) { this.repository = repository; }
public Map<String, Object> getUser(int userId) { Map<String, Object> user = repository.findById(userId); if (user == null) { throw new IllegalArgumentException("User not found"); } return user; }}
// Single responsibility: Data accesspublic class UserRepository { public Map<String, Object> findById(int userId) { // Database query logic Map<String, Object> user = new HashMap<>(); user.put("id", userId); user.put("name", "John"); user.put("email", "john@example.com"); return user; }}
// Single responsibility: Response formattingpublic class ResponseFormatter { public Map<String, Object> formatSuccess(Map<String, Object> data) { Map<String, Object> response = new HashMap<>(); response.put("status", "success"); response.put("data", data); return response; }
public Map<String, Object> formatError(String error) { Map<String, Object> response = new HashMap<>(); response.put("status", "error"); response.put("message", error); return response; }}
// Single responsibility: HTTP request handlingpublic class APIHandler { private UserService userService; private ResponseFormatter formatter;
public APIHandler() { this.userService = new UserService(new UserRepository()); this.formatter = new ResponseFormatter(); }
public Map<String, Object> handleRequest(Map<String, Object> requestData) { try { Integer userId = (Integer) requestData.get("user_id"); Map<String, Object> user = userService.getUser(userId); return formatter.formatSuccess(user); } catch (IllegalArgumentException e) { return formatter.formatError(e.getMessage()); } }}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
Example 3: Report Generation System
Section titled “Example 3: Report Generation System”A report system that processes data, generates reports, and handles file operations violates SRP.
Violating SRP (Bad Approach)
Section titled “Violating SRP (Bad Approach)”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}")import java.util.List;import java.util.ArrayList;import java.util.Map;import java.io.FileWriter;import java.io.IOException;
public class Report { private List<Map<String, Object>> data;
public Report(List<Map<String, Object>> data) { this.data = data; }
// Responsibility 1: Data processing public List<Map<String, Object>> processData() { List<Map<String, Object>> processed = new ArrayList<>(); for (Map<String, Object> item : data) { if (item.get("active") != null && (Boolean) item.get("active")) { processed.add(item); } } return processed; }
// Responsibility 2: Report generation public String generateReport() { List<Map<String, Object>> processed = processData(); return "Report: " + processed.size() + " items"; }
// Responsibility 3: File operations public void saveToFile(String filename) { String report = generateReport(); try (FileWriter writer = new FileWriter(filename)) { writer.write(report); } catch (IOException e) { throw new RuntimeException(e); } }
// Responsibility 4: Email notification public void sendEmail(String recipient) { String report = generateReport(); System.out.println("Sending report to " + recipient + ": " + report); }}Following SRP (Good Approach)
Section titled “Following SRP (Good Approach)”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}")
# Usagedata = [{"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")import java.util.List;import java.util.ArrayList;import java.util.Map;import java.io.FileWriter;import java.io.IOException;
// Single responsibility: Process and clean datapublic class DataProcessor { private List<Map<String, Object>> data;
public DataProcessor(List<Map<String, Object>> data) { this.data = data; }
public List<Map<String, Object>> process() { List<Map<String, Object>> processed = new ArrayList<>(); for (Map<String, Object> item : data) { if (item.get("active") != null && (Boolean) item.get("active")) { processed.add(item); } } return processed; }}
// Single responsibility: Generate report contentpublic class ReportGenerator { private List<Map<String, Object>> processedData;
public ReportGenerator(List<Map<String, Object>> processedData) { this.processedData = processedData; }
public String generate() { return "Report: " + processedData.size() + " items"; }}
// Single responsibility: Handle file operationspublic class FileManager { public void save(String content, String filename) { try (FileWriter writer = new FileWriter(filename)) { writer.write(content); } catch (IOException e) { throw new RuntimeException(e); } }}
// Single responsibility: Handle email notificationspublic class EmailNotifier { public void send(String content, String recipient) { System.out.println("Sending report to " + recipient + ": " + content); }}
// Usagepublic class Main { public static void main(String[] args) { List<Map<String, Object>> data = new ArrayList<>(); Map<String, Object> item1 = new HashMap<>(); item1.put("id", 1); item1.put("active", true); data.add(item1);
Map<String, Object> item2 = new HashMap<>(); item2.put("id", 2); item2.put("active", false); data.add(item2);
DataProcessor processor = new DataProcessor(data); List<Map<String, Object>> processed = processor.process();
ReportGenerator generator = new ReportGenerator(processed); String reportContent = generator.generate();
FileManager fileManager = new FileManager(); fileManager.save(reportContent, "report.txt");
EmailNotifier emailNotifier = new EmailNotifier(); emailNotifier.send(reportContent, "manager@company.com"); }}Benefits of Following SRP
Section titled “Benefits of Following SRP”Identifying Violations
Section titled “Identifying Violations”Key Takeaways
Section titled “Key Takeaways”Remember: SRP is about cohesion - keeping related things together and unrelated things apart.