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, validation, persistence, and email notifications. This violates SRP because the class has multiple reasons to change.
Violating SRP (Bad Approach)
Section titled “Violating SRP (Bad Approach)”import hashlib
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 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); } }}import * as crypto from 'crypto';
class User { username: string; email: string; password: string;
constructor(username: string, email: string, password: string) { this.username = username; this.email = email; this.password = password; }
// Responsibility 1: Validation validate(): boolean { if (!this.username) { throw new Error("Username is required"); } if (!this.email.includes("@")) { throw new Error("Invalid email"); } return true; }
// Responsibility 2: Database persistence saveToDatabase(): void { // Database connection and save logic console.log(`Saving ${this.username} to database...`); }
// Responsibility 3: Email notification sendWelcomeEmail(): void { // Email sending logic console.log(`Sending welcome email to ${this.email}...`); }
// Responsibility 4: Security hashPassword(): void { // Password hashing logic this.password = crypto .createHash('sha256') .update(this.password) .digest('hex'); }}#include <string>#include <iostream>#include <stdexcept>#include <openssl/sha.h>#include <sstream>#include <iomanip>
class User {private: std::string username; std::string email; std::string password;
public: User(const std::string& username, const std::string& email, const std::string& password) : username(username), email(email), password(password) {}
// Responsibility 1: Validation bool validate() { if (username.empty()) { throw std::invalid_argument("Username is required"); } if (email.find("@") == std::string::npos) { throw std::invalid_argument("Invalid email"); } return true; }
// Responsibility 2: Database persistence void saveToDatabase() { // Database connection and save logic std::cout << "Saving " << username << " to database..." << std::endl; }
// Responsibility 3: Email notification void sendWelcomeEmail() { // Email sending logic std::cout << "Sending welcome email to " << email << "..." << std::endl; }
// Responsibility 4: Security void hashPassword() { // Password hashing logic unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast<const unsigned char*>(password.c_str()), password.length(), hash);
std::stringstream ss; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]); } password = ss.str(); }};using System;using System.Security.Cryptography;using System.Text;
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 bool Validate() { if (string.IsNullOrEmpty(username)) { throw new ArgumentException("Username is required"); } if (string.IsNullOrEmpty(email) || !email.Contains("@")) { throw new ArgumentException("Invalid email"); } return true; }
// Responsibility 2: Database persistence public void SaveToDatabase() { // Database connection and save logic Console.WriteLine($"Saving {username} to database..."); }
// Responsibility 3: Email notification public void SendWelcomeEmail() { // Email sending logic Console.WriteLine($"Sending welcome email to {email}..."); }
// Responsibility 4: Security public void HashPassword() { // Password hashing logic using (SHA256 sha256 = SHA256.Create()) { byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); password = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } }}package main
import ( "crypto/sha256" "errors" "fmt" "strings")
// ❌ User has too many responsibilitiestype User struct { Username string Email string Password string}
func NewUser(username, email, password string) *User { return &User{Username: username, Email: email, Password: password}}
// Responsibility 1: Validationfunc (u *User) Validate() error { if u.Username == "" { return errors.New("username is required") } if u.Email == "" || !strings.Contains(u.Email, "@") { return errors.New("invalid email") } return nil}
// Responsibility 2: Database persistencefunc (u *User) SaveToDatabase() { fmt.Printf("Saving %s to database...\n", u.Username)}
// Responsibility 3: Email notificationfunc (u *User) SendWelcomeEmail() { fmt.Printf("Sending welcome email to %s...\n", u.Email)}
// Responsibility 4: Securityfunc (u *User) HashPassword() { hash := sha256.Sum256([]byte(u.Password)) u.Password = fmt.Sprintf("%x", hash)}struct User { name: String, email: String,}impl User { fn save_to_db(&self) { println!("Saving user"); } fn send_welcome_email(&self) { println!("Email to {}", self.email); } fn generate_report(&self) -> String { format!("Report for {}", self.name) }}// Problem: persistence, email, and reporting live in one type.Following SRP (Good Approach)
Section titled “Following SRP (Good Approach)”import hashlib
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: return hashlib.sha256(password.encode()).hexdigest()
# Usagevalidator = 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) { 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()); } }}import * as crypto from 'crypto';
// Single responsibility: Represent user dataclass User { constructor( public username: string, public email: string, public password: string ) {}}
// Single responsibility: Validate user dataclass UserValidator { validate(user: User): boolean { if (!user.username) { throw new Error("Username is required"); } if (!user.email.includes("@")) { throw new Error("Invalid email"); } return true; }}
// Single responsibility: Handle database operationsclass UserRepository { save(user: User): void { console.log(`Saving ${user.username} to database...`); // Database save logic }}
// Single responsibility: Handle email notificationsclass EmailService { sendWelcomeEmail(email: string): void { console.log(`Sending welcome email to ${email}...`); // Email sending logic }}
// Single responsibility: Handle password securityclass PasswordHasher { hash(password: string): string { return crypto.createHash('sha256').update(password).digest('hex'); }}
// Usageconst validator = new UserValidator();const hasher = new PasswordHasher();
if (validator.validate(user)) { user.password = hasher.hash(user.password); const repository = new UserRepository(); repository.save(user); const emailService = new EmailService(); emailService.sendWelcomeEmail(user.email);}#include <string>#include <iostream>#include <stdexcept>#include <openssl/sha.h>#include <sstream>#include <iomanip>
// Single responsibility: Represent user dataclass User {public: std::string username; std::string email; std::string password;
User(const std::string& username, const std::string& email, const std::string& password) : username(username), email(email), password(password) {}};
// Single responsibility: Validate user dataclass UserValidator {public: bool validate(const User& user) { if (user.username.empty()) { throw std::invalid_argument("Username is required"); } if (user.email.find("@") == std::string::npos) { throw std::invalid_argument("Invalid email"); } return true; }};
// Single responsibility: Handle database operationsclass UserRepository {public: void save(const User& user) { std::cout << "Saving " << user.username << " to database..." << std::endl; // Database save logic }};
// Single responsibility: Handle email notificationsclass EmailService {public: void sendWelcomeEmail(const std::string& email) { std::cout << "Sending welcome email to " << email << "..." << std::endl; // Email sending logic }};
// Single responsibility: Handle password securityclass PasswordHasher {public: std::string hash(const std::string& password) { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast<const unsigned char*>(password.c_str()), password.length(), hash);
std::stringstream ss; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]); } return ss.str(); }};
// Usageint main() { UserValidator validator; PasswordHasher hasher;
if (validator.validate(user)) { user.password = hasher.hash(user.password); UserRepository repository; repository.save(user); EmailService emailService; emailService.sendWelcomeEmail(user.email); }
return 0;}using System;using System.Security.Cryptography;using System.Text;
// Single responsibility: Represent user datapublic class User{ public string Username { get; set; } public string Email { get; set; } public string Password { get; set; }
public User(string username, string email, string password) { Username = username; Email = email; Password = password; }}
// Single responsibility: Validate user datapublic class UserValidator{ public bool Validate(User user) { if (string.IsNullOrEmpty(user.Username)) { throw new ArgumentException("Username is required"); } if (string.IsNullOrEmpty(user.Email) || !user.Email.Contains("@")) { throw new ArgumentException("Invalid email"); } return true; }}
// Single responsibility: Handle database operationspublic class UserRepository{ public void Save(User user) { Console.WriteLine($"Saving {user.Username} to database..."); // Database save logic }}
// Single responsibility: Handle email notificationspublic class EmailService{ public void SendWelcomeEmail(string email) { Console.WriteLine($"Sending welcome email to {email}..."); // Email sending logic }}
// Single responsibility: Handle password securitypublic class PasswordHasher{ public string Hash(string password) { using (SHA256 sha256 = SHA256.Create()) { byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); } }}
// Usageclass Program{ static void Main() { UserValidator validator = new UserValidator(); PasswordHasher hasher = new PasswordHasher();
if (validator.Validate(user)) { user.Password = hasher.Hash(user.Password); UserRepository repository = new UserRepository(); repository.Save(user); EmailService emailService = new EmailService(); emailService.SendWelcomeEmail(user.Email); } }}package main
import ( "crypto/sha256" "errors" "fmt" "strings")
// Single responsibility: represent user datatype User struct { Username string Email string Password string}
// Single responsibility: validate user datatype UserValidator struct{}
func (v *UserValidator) Validate(user *User) error { if user.Username == "" { return errors.New("username is required") } if user.Email == "" || !strings.Contains(user.Email, "@") { return errors.New("invalid email") } return nil}
// Single responsibility: handle database operationstype UserRepository struct{}
func (r *UserRepository) Save(user *User) { fmt.Printf("Saving %s to database...\n", user.Username)}
// Single responsibility: handle email notificationstype EmailService struct{}
func (s *EmailService) SendWelcomeEmail(email string) { fmt.Printf("Sending welcome email to %s...\n", email)}
// Single responsibility: handle password securitytype PasswordHasher struct{}
func (h *PasswordHasher) Hash(password string) string { hash := sha256.Sum256([]byte(password)) return fmt.Sprintf("%x", hash)}
func main() { validator := &UserValidator{} hasher := &PasswordHasher{}
if err := validator.Validate(user); err == nil { user.Password = hasher.Hash(user.Password) repo := &UserRepository{} repo.Save(user) emailSvc := &EmailService{} emailSvc.SendWelcomeEmail(user.Email) }}struct User { name: String, email: String,}struct UserRepository;struct EmailService;struct UserReportGenerator;impl UserRepository { fn save(&self, user: &User) { println!("Saving {}", user.name); }}impl EmailService { fn send_welcome_email(&self, user: &User) { println!("Email to {}", user.email); }}impl UserReportGenerator { fn generate(&self, user: &User) -> String { format!("Report for {}", user.name) }}// Each type has one reason to change.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, data formatting, and database operations.
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") if user_id is None: return {"error": "user_id is required"}
# 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 (returns None if user not found) if user_id <= 0: return Noneimport 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"); if (userId == null) { Map<String, Object> error = new HashMap<>(); error.put("error", "user_id is required"); return error; }
// 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 (returns null if user not found) if (userId <= 0) return null; Map<String, Object> user = new HashMap<>(); user.put("id", userId); user.put("name", "John"); return user; }}interface RequestData { user_id?: number;}
interface UserData { id: number; name: string; email: string;}
class APIHandler { private dbConnection: unknown = null; // Database connection
handleRequest(requestData: RequestData): Record<string, unknown> { // Responsibility 1: Handle HTTP request const userId = requestData.user_id; if (userId === undefined || userId === null) { return { error: "user_id is required" }; }
// Responsibility 2: Business logic const user = this.fetchUser(userId); if (!user) { return { error: "User not found" }; }
// Responsibility 3: Format response return { status: "success", data: { id: user.id, name: user.name, email: user.email, }, }; }
// Responsibility 4: Database operations private fetchUser(userId: number): UserData | null { // Database query logic (returns null if user not found) if (userId <= 0) return null; }}#include <string>#include <map>#include <optional>#include <sstream>
struct UserData { int id; std::string name; std::string email;};
class APIHandler {private: void* dbConnection = nullptr; // Database connection
// Responsibility 4: Database operations std::optional<UserData> fetchUser(int userId) { // Database query logic (returns nullopt if user not found) if (userId <= 0) return std::nullopt; }
public: // Responsibility 1: Handle HTTP request // Responsibility 2: Business logic // Responsibility 3: Format response std::map<std::string, std::string> handleRequest( const std::map<std::string, int>& requestData) { // Parse request auto it = requestData.find("user_id"); if (it == requestData.end()) { return {{"error", "user_id is required"}}; } int userId = it->second;
// Business logic: Fetch user from database auto user = fetchUser(userId); if (!user) { return {{"error", "User not found"}}; }
// Format JSON response (C++ uses string for nested data; equivalent to // {"status":"success", "data":{...}} in other languages) std::map<std::string, std::string> response; response["status"] = "success"; std::ostringstream data; data << "{\"id\":" << user->id << ",\"name\":\"" << user->name << "\",\"email\":\"" << user->email << "\"}"; response["data"] = data.str(); return response; }};using System;using System.Collections.Generic;
public class APIHandler{ private object dbConnection = null; // Database connection
// Responsibility 1: Handle HTTP request // Responsibility 2: Business logic // Responsibility 3: Format response // Responsibility 4: Database operations public Dictionary<string, object> HandleRequest( Dictionary<string, object> requestData) { // Parse request if (!requestData.ContainsKey("user_id") || requestData["user_id"] == null) { return new Dictionary<string, object> { { "error", "user_id is required" } }; } int userId = (int)requestData["user_id"];
// Business logic: Fetch user from database var user = FetchUser(userId); if (user == null) { return new Dictionary<string, object> { { "error", "User not found" } }; }
// Format JSON response return new Dictionary<string, object> { { "status", "success" }, { "data", new Dictionary<string, object> { { "id", user["id"] }, { "name", user["name"] }, { "email", user["email"] } } } }; }
// Responsibility 4: Database operations private Dictionary<string, object>? FetchUser(int userId) { // Database query logic (returns null if user not found) if (userId <= 0) return null; return new Dictionary<string, object> { { "id", userId }, { "name", "John" }, }; }}package main
// ❌ APIHandler mixes request parsing, business logic, formatting, and DBtype APIHandler struct{}
// Responsibility 1: Handle HTTP request// Responsibility 2: Business logic// Responsibility 3: Format response// Responsibility 4: Database operationsfunc (h *APIHandler) HandleRequest(requestData map[string]any) map[string]any { userID, ok := requestData["user_id"] if !ok || userID == nil { return map[string]any{"error": "user_id is required"} }
id, _ := userID.(int) user := h.fetchUser(id) if user == nil { return map[string]any{"error": "User not found"} }
return map[string]any{ "status": "success", "data": map[string]any{ "id": user["id"], "name": user["name"], "email": user["email"], }, }}
func (h *APIHandler) fetchUser(userID int) map[string]any { if userID <= 0 { return nil }}struct ApiHandler;impl ApiHandler { fn handle_request(&self, raw_body: &str) -> String { println!("Parse {raw_body}"); println!("Validate"); println!("Save"); println!("Format response"); "200 OK".to_string() }}// Too many responsibilities in one handler.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 (returns None if user not found) if user_id <= 0: return None
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") if user_id is None: return self.formatter.format_error("user_id is required") 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 (returns null if user not found) if (userId <= 0) return null; Map<String, Object> user = new HashMap<>(); user.put("id", userId); user.put("name", "John"); 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"); if (userId == null) { return formatter.formatError("user_id is required"); } Map<String, Object> user = userService.getUser(userId); return formatter.formatSuccess(user); } catch (IllegalArgumentException e) { return formatter.formatError(e.getMessage()); } }}interface UserData { id: number; name: string; email: string;}
// Single responsibility: Business logicclass UserService { constructor(private repository: UserRepository) {}
getUser(userId: number): UserData { const user = this.repository.findById(userId); if (!user) { throw new Error("User not found"); } return user; }}
// Single responsibility: Data accessclass UserRepository { findById(userId: number): UserData | null { // Database query logic (returns null if user not found) if (userId <= 0) return null; }}
// Single responsibility: Response formattingclass ResponseFormatter { formatSuccess(data: UserData): Record<string, unknown> { return { status: "success", data, }; }
formatError(error: string): Record<string, unknown> { return { status: "error", message: error, }; }}
// Single responsibility: HTTP request handlingclass APIHandler { private userService: UserService; private formatter: ResponseFormatter;
constructor() { this.userService = new UserService(new UserRepository()); this.formatter = new ResponseFormatter(); }
handleRequest(requestData: { user_id?: number }): Record<string, unknown> { try { const userId = requestData.user_id; if (userId === undefined || userId === null) { return this.formatter.formatError("user_id is required"); } const user = this.userService.getUser(userId); return this.formatter.formatSuccess(user); } catch (e) { return this.formatter.formatError((e as Error).message); } }}#include <string>#include <map>#include <optional>#include <stdexcept>
struct UserData { int id; std::string name; std::string email;};
// Single responsibility: Data accessclass UserRepository {public: std::optional<UserData> findById(int userId) { // Database query logic (returns nullopt if user not found) if (userId <= 0) return std::nullopt; }};
// Single responsibility: Business logicclass UserService { UserRepository repository;
public: UserData getUser(int userId) { auto user = repository.findById(userId); if (!user) { throw std::runtime_error("User not found"); } return *user; }};
// Single responsibility: Response formattingclass ResponseFormatter {public: std::map<std::string, std::string> formatSuccess(const UserData& data) { std::map<std::string, std::string> response; response["status"] = "success"; response["data"] = "{\"id\":" + std::to_string(data.id) + ",\"name\":\"" + data.name + "\",\"email\":\"" + data.email + "\"}"; return response; }
std::map<std::string, std::string> formatError(const std::string& error) { std::map<std::string, std::string> response; response["status"] = "error"; response["message"] = error; return response; }};
// Single responsibility: HTTP request handlingclass APIHandler { UserService userService; ResponseFormatter formatter;
public: std::map<std::string, std::string> handleRequest( const std::map<std::string, int>& requestData) { try { auto it = requestData.find("user_id"); if (it == requestData.end()) { return formatter.formatError("user_id is required"); } int userId = it->second; UserData user = userService.getUser(userId); return formatter.formatSuccess(user); } catch (const std::exception& e) { return formatter.formatError(e.what()); } }};using System;using System.Collections.Generic;
// Single responsibility: Data accesspublic class UserRepository{ public Dictionary<string, object>? FindById(int userId) { // Database query logic (returns null if user not found) if (userId <= 0) return null; return new Dictionary<string, object> { { "id", userId }, { "name", "John" }, }; }}
// Single responsibility: Business logicpublic class UserService{ private readonly UserRepository repository;
public UserService(UserRepository repository) { this.repository = repository; }
public Dictionary<string, object> GetUser(int userId) { var user = repository.FindById(userId); if (user == null) { throw new ArgumentException("User not found"); } return user; }}
// Single responsibility: Response formattingpublic class ResponseFormatter{ public Dictionary<string, object> FormatSuccess(Dictionary<string, object> data) { return new Dictionary<string, object> { { "status", "success" }, { "data", data } }; }
public Dictionary<string, object> FormatError(string error) { return new Dictionary<string, object> { { "status", "error" }, { "message", error } }; }}
// Single responsibility: HTTP request handlingpublic class APIHandler{ private readonly UserService userService; private readonly ResponseFormatter formatter;
public APIHandler() { userService = new UserService(new UserRepository()); formatter = new ResponseFormatter(); }
public Dictionary<string, object> HandleRequest( Dictionary<string, object> requestData) { try { if (!requestData.ContainsKey("user_id") || requestData["user_id"] == null) { return formatter.FormatError("user_id is required"); } int userId = (int)requestData["user_id"]; var user = userService.GetUser(userId); return formatter.FormatSuccess(user); } catch (ArgumentException e) { return formatter.FormatError(e.Message); } }}package main
import "errors"
// Single responsibility: data accesstype UserRepository struct{}
func (r *UserRepository) FindById(userID int) (map[string]any, error) { if userID <= 0 { return nil, errors.New("user not found") }}
// Single responsibility: business logictype UserService struct{ repo *UserRepository }
func NewUserService(repo *UserRepository) *UserService { return &UserService{repo: repo} }
func (s *UserService) GetUser(userID int) (map[string]any, error) { return s.repo.FindById(userID)}
// Single responsibility: response formattingtype ResponseFormatter struct{}
func (f *ResponseFormatter) FormatSuccess(data any) map[string]any { return map[string]any{"status": "success", "data": data}}
func (f *ResponseFormatter) FormatError(err string) map[string]any { return map[string]any{"status": "error", "message": err}}
// Single responsibility: HTTP request handlingtype APIHandler struct { service *UserService formatter *ResponseFormatter}
func NewAPIHandler() *APIHandler { return &APIHandler{ service: NewUserService(&UserRepository{}), formatter: &ResponseFormatter{}, }}
func (h *APIHandler) HandleRequest(requestData map[string]any) map[string]any { userID, ok := requestData["user_id"] if !ok || userID == nil { return h.formatter.FormatError("user_id is required") } id, _ := userID.(int) user, err := h.service.GetUser(id) if err != nil { return h.formatter.FormatError(err.Error()) } return h.formatter.FormatSuccess(user)}struct RequestParser;struct RequestValidator;struct UserRepository;struct ResponseFormatter;struct ApiHandler { parser: RequestParser, validator: RequestValidator, repo: UserRepository, formatter: ResponseFormatter,}impl ApiHandler { fn handle_request(&self, body: &str) -> String { let req = self.parser.parse(body); self.validator.validate(&req); self.repo.save(&req); self.formatter.ok() }}impl RequestParser { fn parse(&self, body: &str) -> String { body.to_string() }}impl RequestValidator { fn validate(&self, _request: &str) {}}impl UserRepository { fn save(&self, _request: &str) {}}impl ResponseFormatter { fn ok(&self) -> String { "200 OK".to_string() }}Example 3: Report Generation System
Section titled “Example 3: Report Generation System”A report system that processes data, generates reports, handles file operations, and sends email notifications 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); }}import * as fs from "fs";
interface DataItem { id?: number; active?: boolean; [key: string]: unknown;}
class Report { constructor(private data: DataItem[]) {}
// Responsibility 1: Data processing processData(): DataItem[] { return this.data.filter((item) => item.active === true); }
// Responsibility 2: Report generation generateReport(): string { const processed = this.processData(); return `Report: ${processed.length} items`; }
// Responsibility 3: File operations saveToFile(filename: string): void { const report = this.generateReport(); fs.writeFileSync(filename, report); }
// Responsibility 4: Email notification sendEmail(recipient: string): void { const report = this.generateReport(); console.log(`Sending report to ${recipient}: ${report}`); }}#include <string>#include <vector>#include <fstream>#include <iostream>
struct DataItem { int id; bool active;};
class Report {private: std::vector<DataItem> data;
public: Report(const std::vector<DataItem>& data) : data(data) {}
// Responsibility 1: Data processing std::vector<DataItem> processData() { std::vector<DataItem> processed; for (const auto& item : data) { if (item.active) { processed.push_back(item); } } return processed; }
// Responsibility 2: Report generation std::string generateReport() { auto processed = processData(); return "Report: " + std::to_string(processed.size()) + " items"; }
// Responsibility 3: File operations void saveToFile(const std::string& filename) { std::string report = generateReport(); std::ofstream file(filename); file << report; }
// Responsibility 4: Email notification void sendEmail(const std::string& recipient) { std::string report = generateReport(); std::cout << "Sending report to " << recipient << ": " << report << std::endl; }};using System;using System.Collections.Generic;using System.IO;using System.Linq;
public class Report{ private readonly List<Dictionary<string, object>> data;
public Report(List<Dictionary<string, object>> data) { this.data = data; }
// Responsibility 1: Data processing public List<Dictionary<string, object>> ProcessData() { return data.Where(item => item.ContainsKey("active") && (bool)item["active"]).ToList(); }
// Responsibility 2: Report generation public string GenerateReport() { var processed = ProcessData(); return $"Report: {processed.Count} items"; }
// Responsibility 3: File operations public void SaveToFile(string filename) { string report = GenerateReport(); File.WriteAllText(filename, report); }
// Responsibility 4: Email notification public void SendEmail(string recipient) { string report = GenerateReport(); Console.WriteLine($"Sending report to {recipient}: {report}"); }}package main
import ( "fmt" "os")
// ❌ Report has too many responsibilitiestype Report struct { data []map[string]any}
func NewReport(data []map[string]any) *Report { return &Report{data: data} }
// Responsibility 1: Data processingfunc (r *Report) ProcessData() []map[string]any { var result []map[string]any for _, item := range r.data { if active, ok := item["active"].(bool); ok && active { result = append(result, item) } } return result}
// Responsibility 2: Report generationfunc (r *Report) GenerateReport() string { return fmt.Sprintf("Report: %d items", len(r.ProcessData()))}
// Responsibility 3: File operationsfunc (r *Report) SaveToFile(filename string) error { return os.WriteFile(filename, []byte(r.GenerateReport()), 0644)}
// Responsibility 4: Email notificationfunc (r *Report) SendEmail(recipient string) { fmt.Printf("Sending report to %s: %s\n", recipient, r.GenerateReport())}struct Report;impl Report { fn fetch_data(&self) -> Vec<String> { vec!["row".to_string()] } fn calculate_totals(&self, rows: &[String]) -> usize { rows.len() } fn render_pdf(&self, total: usize) -> String { format!("PDF with {total} rows") } fn email_report(&self, pdf: &str) { println!("Emailing {pdf}"); }}// Data access, calculation, rendering, and delivery are coupled.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()import java.util.List;import java.util.ArrayList;import java.util.Map;import java.util.HashMap;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(); }}import * as fs from "fs";
interface DataItem { id?: number; active?: boolean; [key: string]: unknown;}
// Single responsibility: Process and clean dataclass DataProcessor { constructor(private data: DataItem[]) {}
process(): DataItem[] { return this.data.filter((item) => item.active === true); }}
// Single responsibility: Generate report contentclass ReportGenerator { constructor(private processedData: DataItem[]) {}
generate(): string { return `Report: ${this.processedData.length} items`; }}
// Single responsibility: Handle file operationsclass FileManager { save(content: string, filename: string): void { fs.writeFileSync(filename, content); }}
// Single responsibility: Handle email notificationsclass EmailNotifier { send(content: string, recipient: string): void { console.log(`Sending report to ${recipient}: ${content}`); }}
// Usageconst data: DataItem[] = [ { id: 1, active: true }, { id: 2, active: false },];const processor = new DataProcessor(data);const processed = processor.process();
const generator = new ReportGenerator(processed);const reportContent = generator.generate();
const fileManager = new FileManager();fileManager.save(reportContent, "report.txt");
const emailNotifier = new EmailNotifier();#include <string>#include <vector>#include <fstream>#include <iostream>
struct DataItem { int id; bool active;};
// Single responsibility: Process and clean dataclass DataProcessor { std::vector<DataItem> data;
public: DataProcessor(std::vector<DataItem> data) : data(std::move(data)) {}
std::vector<DataItem> process() { std::vector<DataItem> processed; for (const auto& item : data) { if (item.active) { processed.push_back(item); } } return processed; }};
// Single responsibility: Generate report contentclass ReportGenerator { std::vector<DataItem> processedData;
public: ReportGenerator(std::vector<DataItem> processedData) : processedData(std::move(processedData)) {}
std::string generate() { return "Report: " + std::to_string(processedData.size()) + " items"; }};
// Single responsibility: Handle file operationsclass FileManager {public: void save(const std::string& content, const std::string& filename) { std::ofstream file(filename); file << content; }};
// Single responsibility: Handle email notificationsclass EmailNotifier {public: void send(const std::string& content, const std::string& recipient) { std::cout << "Sending report to " << recipient << ": " << content << std::endl; }};
// Usageint main() { std::vector<DataItem> data = {{1, true}, {2, false}}; DataProcessor processor(data); auto processed = processor.process();
ReportGenerator generator(processed); std::string reportContent = generator.generate();
FileManager fileManager; fileManager.save(reportContent, "report.txt");
EmailNotifier emailNotifier;
return 0;}using System;using System.Collections.Generic;using System.IO;using System.Linq;
// Single responsibility: Process and clean datapublic class DataProcessor{ private readonly List<Dictionary<string, object>> data;
public DataProcessor(List<Dictionary<string, object>> data) { this.data = data; }
public List<Dictionary<string, object>> Process() { return data.Where(item => item.ContainsKey("active") && (bool)item["active"]).ToList(); }}
// Single responsibility: Generate report contentpublic class ReportGenerator{ private readonly List<Dictionary<string, object>> processedData;
public ReportGenerator(List<Dictionary<string, object>> processedData) { this.processedData = processedData; }
public string Generate() { return $"Report: {processedData.Count} items"; }}
// Single responsibility: Handle file operationspublic class FileManager{ public void Save(string content, string filename) { File.WriteAllText(filename, content); }}
// Single responsibility: Handle email notificationspublic class EmailNotifier{ public void Send(string content, string recipient) { Console.WriteLine($"Sending report to {recipient}: {content}"); }}
// Usageclass Program{ static void Main() { var data = new List<Dictionary<string, object>> { new Dictionary<string, object> { { "id", 1 }, { "active", true } }, new Dictionary<string, object> { { "id", 2 }, { "active", false } } };
var processor = new DataProcessor(data); var processed = processor.Process();
var generator = new ReportGenerator(processed); var reportContent = generator.Generate();
var fileManager = new FileManager(); fileManager.Save(reportContent, "report.txt");
var emailNotifier = new EmailNotifier(); }}package main
import ( "fmt" "os")
// Single responsibility: process and clean datatype DataProcessor struct{ data []map[string]any }
func NewDataProcessor(data []map[string]any) *DataProcessor { return &DataProcessor{data: data} }
func (p *DataProcessor) Process() []map[string]any { var result []map[string]any for _, item := range p.data { if active, ok := item["active"].(bool); ok && active { result = append(result, item) } } return result}
// Single responsibility: generate report contenttype ReportGenerator struct{ processedData []map[string]any }
func NewReportGenerator(data []map[string]any) *ReportGenerator { return &ReportGenerator{processedData: data}}
func (g *ReportGenerator) Generate() string { return fmt.Sprintf("Report: %d items", len(g.processedData))}
// Single responsibility: handle file operationstype FileManager struct{}
func (f *FileManager) Save(content, filename string) error { return os.WriteFile(filename, []byte(content), 0644)}
// Single responsibility: handle email notificationstype EmailNotifier struct{}
func (e *EmailNotifier) Send(content, recipient string) { fmt.Printf("Sending report to %s: %s\n", recipient, content)}
func main() { data := []map[string]any{ {"id": 1, "active": true}, {"id": 2, "active": false}, }
processed := NewDataProcessor(data).Process() reportContent := NewReportGenerator(processed).Generate()
(&FileManager{}).Save(reportContent, "report.txt")}struct ReportRepository;struct ReportCalculator;struct PdfRenderer;struct EmailSender;impl ReportRepository { fn fetch_data(&self) -> Vec<String> { vec!["row".to_string()] }}impl ReportCalculator { fn total(&self, rows: &[String]) -> usize { rows.len() }}impl PdfRenderer { fn render(&self, total: usize) -> String { format!("PDF with {total} rows") }}impl EmailSender { fn send(&self, pdf: &str) { println!("Emailing {pdf}"); }}fn generate_report() { let rows = ReportRepository.fetch_data(); let total = ReportCalculator.total(&rows); let pdf = PdfRenderer.render(total); EmailSender.send(&pdf);}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.
Make this lesson stick
Answer from memory, then check yourself. No typing or sign-in needed.
What is the Single Responsibility Principle (SRP)?