🧑💻 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:
#include <iostream>
#include <string>
using namespace std;
// ❌ VIOLATION: This class has multiple responsibilities
class Employee {
private:
string name;
string employeeId;
double salary;
string department;
public:
// Constructor
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
void setName(string name) { this->name = name; }
string getName() const { return name; }
void setSalary(double salary) { this->salary = salary; }
double getSalary() const { return salary; }
// Responsibility 2: Salary calculations
double calculateTax() const {
return salary * 0.25;
}
double calculateBonus() const {
return salary * 0.1;
}
// Responsibility 3: Database operations
void saveToDatabase() const {
cout << "Saving employee to database..." << endl;
}
void deleteFromDatabase() const {
cout << "Deleting employee from database..." << endl;
}
// Responsibility 4: Report generation
void generatePayslip() const {
cout << "=== PAYSLIP ===" << endl;
cout << "Employee: " << name << endl;
cout << "Salary: $" << salary << endl;
cout << "Tax: $" << calculateTax() << endl;
cout << "Bonus: $" << calculateBonus() << endl;
}
// Responsibility 5: Email notifications
void sendEmail(const string& message) const {
cout << "Sending email to " << name << ": " << message << endl;
}
};
// Example usage
int main() {
Employee emp("John Doe", "E123", 50000, "Engineering");
emp.generatePayslip();
emp.saveToDatabase();
emp.sendEmail("Your payslip has been generated.");
return 0;
}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
#include <string>
using namespace std;
// ✅ GOOD: Only handles employee data
class Employee {
private:
string name;
string employeeId;
double salary;
string department;
public:
// Constructor
Employee(const string& name, const string& employeeId, double salary, const string& department)
: name(name), employeeId(employeeId), salary(salary), department(department) {}
// Getters and Setters — data management only
string getName() const { return name; }
void setName(const string& name) { this->name = name; }
string getEmployeeId() const { return employeeId; }
void setEmployeeId(const string& employeeId) { this->employeeId = employeeId; }
double getSalary() const { return salary; }
void setSalary(double salary) { this->salary = salary; }
string getDepartment() const { return department; }
void setDepartment(const string& department) { this->department = department; }
};2. Salary Calculator Class
#include <iostream>
using namespace std;
class Employee; // Forward declaration (since Employee will be defined elsewhere)
class SalaryCalculator {
private:
static constexpr double TAX_RATE = 0.25;
static constexpr double BONUS_RATE = 0.1;
public:
double calculateTax(const Employee& employee) const;
double calculateBonus(const Employee& employee) const;
double calculateNetSalary(const Employee& employee) const;
};3. Employee Repository Class
#include <iostream>
#include <string>
using namespace std;
// Mock DatabaseConnection class (for illustration)
class DatabaseConnection {
public:
void connect() const {
cout << "Connecting to database..." << endl;
}
void disconnect() const {
cout << "Disconnecting from database..." << endl;
}
};
// Forward declaration of Employee class
class Employee {
private:
string name;
string employeeId;
double salary;
string department;
public:
Employee(const string& name, const string& employeeId, double salary, const string& department)
: name(name), employeeId(employeeId), salary(salary), department(department) {}
string getName() const { return name; }
string getEmployeeId() const { return employeeId; }
};
// ✅ GOOD: Only handles database operations
class EmployeeRepository {
private:
DatabaseConnection connection;
public:
EmployeeRepository(const DatabaseConnection& conn) : connection(conn) {}
void save(const Employee& employee) const {
// Database save logic
cout << "Saving employee " << employee.getName() << " to database..." << endl;
}
void deleteById(const string& employeeId) const {
// Database delete logic
cout << "Deleting employee " << employeeId << " from database..." << endl;
}
Employee* findById(const string& employeeId) const {
// Database query logic
cout << "Finding employee " << employeeId << " in database..." << endl;
return nullptr; // Simplified for example
}
};
// Example usage
int main() {
DatabaseConnection db;
EmployeeRepository repo(db);
Employee emp("Alice", "E102", 60000, "Finance");
repo.save(emp);
repo.findById("E102");
repo.deleteById("E102");
return 0;
}4. Payslip Generator Class
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
// Forward declarations
class Employee;
class SalaryCalculator;
class PayslipGenerator {
private:
const SalaryCalculator& calculator;
public:
PayslipGenerator(const SalaryCalculator& calc) : calculator(calc) {}
void generatePayslip(const Employee& employee) const {
cout << "=== PAYSLIP ===" << endl;
cout << "Employee: " << employee.getName() << endl;
cout << "ID: " << employee.getEmployeeId() << endl;
cout << "Department: " << employee.getDepartment() << endl;
cout << "Gross Salary: $" << employee.getSalary() << endl;
cout << "Tax: $" << calculator.calculateTax(employee) << endl;
cout << "Bonus: $" << calculator.calculateBonus(employee) << endl;
cout << "Net Salary: $" << calculator.calculateNetSalary(employee) << endl;
cout << "================" << endl;
}
string generatePayslipAsString(const Employee& employee) const {
stringstream ss;
ss << "Payslip for: " << employee.getName() << "\n";
ss << "Net Salary: $" << calculator.calculateNetSalary(employee);
return ss.str();
}
};5. Email Service Class
#include <iostream>
#include <string>
using namespace std;
// Forward declaration
class Employee;
class EmailService {
private:
string smtpServer;
int port;
public:
EmailService(const string& smtpServer, int port)
: smtpServer(smtpServer), port(port) {}
void sendEmail(const string& recipientEmail, const string& subject, const string& message) const {
// Email sending logic
cout << "Sending email to: " << recipientEmail << endl;
cout << "Subject: " << subject << endl;
cout << "Message: " << message << endl;
}
void sendPayslipEmail(const Employee& employee, const string& payslipContent) const {
string subject = "Payslip for " + employee.getName();
sendEmail(employee.getName() + "@company.com", subject, payslipContent);
}
};🏗️ Class Diagram After SRP
🎯 Using the Refactored Classes
#include <iostream>
#include <string>
using namespace std;
// Forward declarations of all used classes
class DatabaseConnection;
class Employee;
class SalaryCalculator;
class EmployeeRepository;
class PayslipGenerator;
class EmailService;
class EmployeeManagementSystem {
public:
static void main() {
// Create dependencies
SalaryCalculator calculator;
EmployeeRepository repository(DatabaseConnection());
PayslipGenerator payslipGenerator(calculator);
EmailService emailService("smtp.company.com", 587);
// Create employee
Employee 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);
}
};
// Entry point
int main() {
EmployeeManagementSystem::main();
return 0;
}🎨 More SRP Examples
Example 1: User Authentication
❌ Violation
#include <iostream>
#include <string>
using namespace std;
class User {};
class UserManager {
public:
bool validateUser(const string& username, const string& password) { /* ... */ return true; }
void sendPasswordResetEmail(const string& email) { /* ... */ }
void logUserActivity(const string& username, const string& activity) { /* ... */ }
void saveUserToDatabase(const User& user) { /* ... */ }
User getUserFromDatabase(const string& username) { /* ... */ return User(); }
};✅ Solution
#include <iostream>
#include <string>
using namespace std;
class User {};
class AuthenticationService {
public:
bool validateUser(const string& username, const string& password) {
cout << "Validating user: " << username << endl;
return true;
}
};
class EmailService {
public:
void sendPasswordResetEmail(const string& email) {
cout << "Sending password reset email to: " << email << endl;
}
};
class ActivityLogger {
public:
void logUserActivity(const string& username, const string& activity) {
cout << "User " << username << " performed: " << activity << endl;
}
};
class UserRepository {
public:
void saveUser(const User& user) { cout << "User saved to DB\n"; }
User getUser(const string& username) { cout << "Getting user from DB\n"; return User(); }
};Example 2: Order Processing
❌ Violation
#include <iostream>
#include <vector>
using namespace std;
class OrderItem {};
class Order {
vector<OrderItem> items;
public:
void addItem(const OrderItem& item) { /* ... */ }
double calculateTotal() { /* ... */ return 0.0; }
double calculateTax() { /* ... */ return 0.0; }
void saveToDatabase() { /* ... */ }
void sendConfirmationEmail() { /* ... */ }
void updateInventory() { /* ... */ }
};✅ Solution
#include <iostream>
#include <vector>
using namespace std;
class OrderItem {};
class Order {
vector<OrderItem> items;
public:
void addItem(const OrderItem& item) { cout << "Item added\n"; }
};
class OrderCalculator {
public:
double calculateTotal(const Order& order) { cout << "Calculating total\n"; return 100.0; }
double calculateTax(const Order& order) { cout << "Calculating tax\n"; return 18.0; }
};
class OrderRepository {
public:
void save(const Order& order) { cout << "Order saved to database\n"; }
};
class OrderNotificationService {
public:
void sendConfirmationEmail(const Order& order) { cout << "Email sent to customer\n"; }
};
class InventoryService {
public:
void updateInventory(const Order& order) { cout << "Inventory updated\n"; }
};🔍 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
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Book {
string title;
public:
Book(string t) : title(t) {}
string getTitle() const { return title; }
};
class BookRepository {
public:
void save(const Book& book) { cout << "Book saved: " << book.getTitle() << endl; }
vector<Book> findByQuery(const string& query) {
cout << "Searching for books: " << query << endl;
return {};
}
};
class NotificationService {
public:
void sendNotification(const string& message) {
cout << "Notification: " << message << endl;
}
};
class ReportGenerator {
public:
void generateBookReport() { cout << "Generating book report\n"; }
};
class BookService {
BookRepository repository;
NotificationService notificationService;
public:
void addBook(const 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?
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Student {};
class StudentManager {
vector<Student> students;
public:
void addStudent(const Student& student) { /* ... */ }
void removeStudent(const string& id) { /* ... */ }
Student findStudent(const string& id) { /* ... */ return Student(); }
double calculateGPA(const string& id) { /* ... */ return 0.0; }
string getGradeLevel(double gpa) { /* ... */ return "A"; }
void saveToFile(const string& filename) { /* ... */ }
void loadFromFile(const string& filename) { /* ... */ }
void sendGradeReport(const string& id) { /* ... */ }
void sendWelcomeEmail(const Student& student) { /* ... */ }
void generateClassReport() { /* ... */ }
void generateStudentTranscript(const string& id) { /* ... */ }
};Solution Approach
- Create
StudentRepositoryfor data management - Create
GradeCalculatorfor grade-related calculations - Create
FileManagerfor file operations - Create
EmailServicefor notifications - Create
ReportGeneratorfor reports - Create
StudentServiceto 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!
