Skip to content

🧑‍💻 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:

cpp
#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

  1. Multiple Reasons to Change:

    • Tax calculation rules change
    • Database schema changes
    • Email system changes
    • Report format changes
  2. High Coupling: Changes in one area affect others

  3. Difficult Testing: Can't test parts in isolation

  4. Code Duplication: Similar logic scattered across methods

  5. 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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

cpp
#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

  1. Can I describe the class responsibility in a single sentence without using "and" or "or"?
  2. How many reasons can I think of for changing this class?
  3. Does this class have methods that operate on different sets of data?
  4. 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

java
// 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

java
@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

java
// 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.

java
// ✅ 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.

java
// ✅ 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

  1. Identify Responsibilities: List all things the class does
  2. Group Related Methods: Find methods that work together
  3. Extract Classes: Create new classes for each responsibility
  4. Define Interfaces: Create contracts between classes
  5. Update Dependencies: Inject dependencies where needed
  6. Test: Ensure functionality is preserved

Example Refactoring Process

cpp
#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?

cpp
#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

  1. Create StudentRepository for data management
  2. Create GradeCalculator for grade-related calculations
  3. Create FileManager for file operations
  4. Create EmailService for notifications
  5. Create ReportGenerator for reports
  6. 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!