🧑💻 Author: RK ROY
Single Responsibility Principle (SRP)
🎯 Definition
"A class should have only one reason to change."
- Robert C. Martin
The Single Responsibility Principle states that every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.
🤔 What Does "Single Responsibility" Mean?
A class has a single responsibility if it has only one reason to change. If you can think of more than one motive for changing a class, then that class has more than one responsibility.
Key Concepts
❌ SRP Violation: The "God Class"
Let's look at a classic example that violates SRP:
// ❌ VIOLATION: This class has multiple responsibilities
public class Employee {
private String name;
private String employeeId;
private double salary;
private String department;
// Constructor
public Employee(String name, String employeeId, double salary, String department) {
this.name = name;
this.employeeId = employeeId;
this.salary = salary;
this.department = department;
}
// Responsibility 1: Data management
public void setName(String name) { this.name = name; }
public String getName() { return name; }
public void setSalary(double salary) { this.salary = salary; }
public double getSalary() { return salary; }
// Responsibility 2: Salary calculations
public double calculateTax() {
return salary * 0.25;
}
public double calculateBonus() {
return salary * 0.1;
}
// Responsibility 3: Database operations
public void saveToDatabase() {
// Database connection and save logic
System.out.println("Saving employee to database...");
}
public void deleteFromDatabase() {
// Database connection and delete logic
System.out.println("Deleting employee from database...");
}
// Responsibility 4: Report generation
public void generatePayslip() {
System.out.println("=== PAYSLIP ===");
System.out.println("Employee: " + name);
System.out.println("Salary: $" + salary);
System.out.println("Tax: $" + calculateTax());
System.out.println("Bonus: $" + calculateBonus());
}
// Responsibility 5: Email notifications
public void sendEmail(String message) {
System.out.println("Sending email to " + name + ": " + message);
}
}
Problems with This Design
Multiple Reasons to Change:
- Tax calculation rules change
- Database schema changes
- Email system changes
- Report format changes
High Coupling: Changes in one area affect others
Difficult Testing: Can't test parts in isolation
Code Duplication: Similar logic scattered across methods
Violation of Single Responsibility: Class does too many things
✅ SRP Solution: Separated Responsibilities
Let's refactor this into multiple classes, each with a single responsibility:
1. Employee Data Class
// ✅ GOOD: Only handles employee data
public class Employee {
private String name;
private String employeeId;
private double salary;
private String department;
public Employee(String name, String employeeId, double salary, String department) {
this.name = name;
this.employeeId = employeeId;
this.salary = salary;
this.department = department;
}
// Only getters and setters - data management only
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmployeeId() { return employeeId; }
public void setEmployeeId(String employeeId) { this.employeeId = employeeId; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
}
2. Salary Calculator Class
// ✅ GOOD: Only handles salary calculations
public class SalaryCalculator {
private static final double TAX_RATE = 0.25;
private static final double BONUS_RATE = 0.1;
public double calculateTax(Employee employee) {
return employee.getSalary() * TAX_RATE;
}
public double calculateBonus(Employee employee) {
return employee.getSalary() * BONUS_RATE;
}
public double calculateNetSalary(Employee employee) {
return employee.getSalary() - calculateTax(employee) + calculateBonus(employee);
}
}
3. Employee Repository Class
// ✅ GOOD: Only handles database operations
public class EmployeeRepository {
private DatabaseConnection connection;
public EmployeeRepository(DatabaseConnection connection) {
this.connection = connection;
}
public void save(Employee employee) {
// Database save logic
System.out.println("Saving employee " + employee.getName() + " to database...");
}
public void delete(String employeeId) {
// Database delete logic
System.out.println("Deleting employee " + employeeId + " from database...");
}
public Employee findById(String employeeId) {
// Database query logic
System.out.println("Finding employee " + employeeId + " in database...");
return null; // Simplified for example
}
}
4. Payslip Generator Class
// ✅ GOOD: Only handles report generation
public class PayslipGenerator {
private SalaryCalculator calculator;
public PayslipGenerator(SalaryCalculator calculator) {
this.calculator = calculator;
}
public void generatePayslip(Employee employee) {
System.out.println("=== PAYSLIP ===");
System.out.println("Employee: " + employee.getName());
System.out.println("ID: " + employee.getEmployeeId());
System.out.println("Department: " + employee.getDepartment());
System.out.println("Gross Salary: $" + employee.getSalary());
System.out.println("Tax: $" + calculator.calculateTax(employee));
System.out.println("Bonus: $" + calculator.calculateBonus(employee));
System.out.println("Net Salary: $" + calculator.calculateNetSalary(employee));
System.out.println("================");
}
public String generatePayslipAsString(Employee employee) {
StringBuilder sb = new StringBuilder();
sb.append("Payslip for: ").append(employee.getName()).append("\n");
sb.append("Net Salary: $").append(calculator.calculateNetSalary(employee));
return sb.toString();
}
}
5. Email Service Class
// ✅ GOOD: Only handles email operations
public class EmailService {
private String smtpServer;
private int port;
public EmailService(String smtpServer, int port) {
this.smtpServer = smtpServer;
this.port = port;
}
public void sendEmail(String recipientEmail, String subject, String message) {
// Email sending logic
System.out.println("Sending email to: " + recipientEmail);
System.out.println("Subject: " + subject);
System.out.println("Message: " + message);
}
public void sendPayslipEmail(Employee employee, String payslipContent) {
String subject = "Payslip for " + employee.getName();
sendEmail(employee.getName() + "@company.com", subject, payslipContent);
}
}
🏗️ Class Diagram After SRP
🎯 Using the Refactored Classes
public class EmployeeManagementSystem {
public static void main(String[] args) {
// Create dependencies
SalaryCalculator calculator = new SalaryCalculator();
EmployeeRepository repository = new EmployeeRepository(new DatabaseConnection());
PayslipGenerator payslipGenerator = new PayslipGenerator(calculator);
EmailService emailService = new EmailService("smtp.company.com", 587);
// Create employee
Employee employee = new Employee("John Doe", "EMP001", 50000, "Engineering");
// Save to database
repository.save(employee);
// Generate payslip
payslipGenerator.generatePayslip(employee);
String payslipContent = payslipGenerator.generatePayslipAsString(employee);
// Send payslip via email
emailService.sendPayslipEmail(employee, payslipContent);
}
}
🎨 More SRP Examples
Example 1: User Authentication
❌ Violation
public class UserManager {
public boolean validateUser(String username, String password) { ... }
public void sendPasswordResetEmail(String email) { ... }
public void logUserActivity(String username, String activity) { ... }
public void saveUserToDatabase(User user) { ... }
public User getUserFromDatabase(String username) { ... }
}
✅ Solution
public class AuthenticationService {
public boolean validateUser(String username, String password) { ... }
}
public class EmailService {
public void sendPasswordResetEmail(String email) { ... }
}
public class ActivityLogger {
public void logUserActivity(String username, String activity) { ... }
}
public class UserRepository {
public void saveUser(User user) { ... }
public User getUser(String username) { ... }
}
Example 2: Order Processing
❌ Violation
public class Order {
private List<OrderItem> items;
public void addItem(OrderItem item) { ... }
public double calculateTotal() { ... }
public double calculateTax() { ... }
public void saveToDatabase() { ... }
public void sendConfirmationEmail() { ... }
public void updateInventory() { ... }
}
✅ Solution
public class Order {
private List<OrderItem> items;
public void addItem(OrderItem item) { ... }
// Only order data management
}
public class OrderCalculator {
public double calculateTotal(Order order) { ... }
public double calculateTax(Order order) { ... }
}
public class OrderRepository {
public void save(Order order) { ... }
}
public class OrderNotificationService {
public void sendConfirmationEmail(Order order) { ... }
}
public class InventoryService {
public void updateInventory(Order order) { ... }
}
🔍 How to Identify SRP Violations
Questions to Ask
- Can I describe the class responsibility in a single sentence without using "and" or "or"?
- How many reasons can I think of for changing this class?
- Does this class have methods that operate on different sets of data?
- Would different stakeholders want to change different parts of this class?
Warning Signs
- ❌ Class names with "Manager", "Handler", "Controller", "Util"
- ❌ Methods that don't use instance variables
- ❌ Large classes (> 200-300 lines)
- ❌ Many import statements
- ❌ Methods that belong to different conceptual groups
🏆 Benefits of Following SRP
1. Easier Maintenance
// If tax rules change, only SalaryCalculator needs to change
public class SalaryCalculator {
public double calculateTax(Employee employee) {
// Easy to modify tax logic without affecting other classes
return employee.getSalary() * getCurrentTaxRate();
}
}
2. Better Testability
@Test
public void testTaxCalculation() {
SalaryCalculator calculator = new SalaryCalculator();
Employee employee = new Employee("John", "001", 50000, "IT");
double tax = calculator.calculateTax(employee);
assertEquals(12500.0, tax, 0.01);
}
3. Improved Reusability
// EmailService can be reused for different purposes
EmailService emailService = new EmailService("smtp.server.com", 587);
emailService.sendEmail("user@example.com", "Welcome", "Welcome message");
emailService.sendEmail("admin@example.com", "Alert", "System alert");
4. Reduced Coupling
- Changes in one class don't affect others
- Classes can be developed and tested independently
- Easier to understand and debug
🎯 Common Misconceptions
❌ "One Method Per Class"
SRP doesn't mean one method per class. A class can have multiple methods if they all serve the same responsibility.
// ✅ GOOD: Multiple methods, single responsibility (string manipulation)
public class StringFormatter {
public String toUpperCase(String input) { ... }
public String toLowerCase(String input) { ... }
public String capitalize(String input) { ... }
public String removeSpaces(String input) { ... }
}
❌ "Avoid All Dependencies"
SRP doesn't mean classes can't depend on each other. Dependencies are fine if they support the single responsibility.
// ✅ GOOD: OrderService depends on other services but has single responsibility
public class OrderService {
private PaymentProcessor paymentProcessor;
private InventoryService inventoryService;
public void processOrder(Order order) {
// Single responsibility: orchestrating order processing
}
}
🛠️ Refactoring to SRP
Step-by-Step Process
- Identify Responsibilities: List all things the class does
- Group Related Methods: Find methods that work together
- Extract Classes: Create new classes for each responsibility
- Define Interfaces: Create contracts between classes
- Update Dependencies: Inject dependencies where needed
- Test: Ensure functionality is preserved
Example Refactoring Process
// Before: Mixed responsibilities
public class BookService {
public void addBook(Book book) { ... } // Data management
public List<Book> searchBooks(String query) { ... } // Search logic
public void sendNotification(String message) { ... } // Communication
public void generateReport() { ... } // Reporting
}
// After: Separated responsibilities
public class BookRepository {
public void save(Book book) { ... }
public List<Book> findByQuery(String query) { ... }
}
public class NotificationService {
public void sendNotification(String message) { ... }
}
public class ReportGenerator {
public void generateBookReport() { ... }
}
public class BookService {
private BookRepository repository;
private NotificationService notificationService;
// Orchestrates operations - single responsibility
public void addBook(Book book) {
repository.save(book);
notificationService.sendNotification("Book added: " + book.getTitle());
}
}
🎓 Practice Exercise
Exercise: Refactor the Violation
Here's a class that violates SRP. Can you refactor it?
public class StudentManager {
private List<Student> students = new ArrayList<>();
// Student management
public void addStudent(Student student) { ... }
public void removeStudent(String studentId) { ... }
public Student findStudent(String studentId) { ... }
// Grade calculation
public double calculateGPA(String studentId) { ... }
public String getGradeLevel(double gpa) { ... }
// File operations
public void saveToFile(String filename) { ... }
public void loadFromFile(String filename) { ... }
// Email notifications
public void sendGradeReport(String studentId) { ... }
public void sendWelcomeEmail(Student student) { ... }
// Report generation
public void generateClassReport() { ... }
public void generateStudentTranscript(String studentId) { ... }
}
Solution Approach
- Create
StudentRepository
for data management - Create
GradeCalculator
for grade-related calculations - Create
FileManager
for file operations - Create
EmailService
for notifications - Create
ReportGenerator
for reports - Create
StudentService
to orchestrate operations
📚 Summary
The Single Responsibility Principle is about creating focused, cohesive classes that have one clear purpose. By following SRP:
- ✅ Easier to maintain: Changes are localized
- ✅ Better testability: Isolated testing of functionality
- ✅ Improved reusability: Focused classes can be reused
- ✅ Reduced coupling: Classes are less dependent on each other
- ✅ Clearer code: Each class has a clear, single purpose
Remember: A class should have only one reason to change!