Home » SOLID Principles » SOLID: Single Responsibility Principle

SOLID: Single Responsibility Principle

Last Updated on December 30, 2024 by KnownSense

The single responsibility principle has a pretty straightforward definition. Every function, class, or module should have one and only one reason to change.

This definition seems pretty simple. However, it might be a little bit abstract. For example, what is a reason to change? Well, in this context, a reason to change is a synonym for responsibility. We set up a class which has a single responsibility, also has a single reason to change. So we can use these terms interchangeably. Take a look at some responsibilities that you’ve probably seen in your own applications.

SOLID: Single Responsibility Principle

One important habit that you need to develop as a software developer is the ability to identify the reasons to change that your components have and then to be able to reduce them to a single one.

Benefits of SRP

  • It makes code easier to understand, fix, and maintain.
  • Developers spend almost 90% of their time reading code, so understanding and reasoning about it quickly is crucial.
  • SRP has a significant impact on code readability.
  • Code adhering to SRP creates classes that are less coupled and more resilient to change.
  • Using SRP minimizes fragility and rigidity in the code.
  • It leads to a more testable design, which improves software quality.

Identifying Multiple Reasons to Change

If statements are a clear sign that the method has multiple reasons to change. We have one reason to change on the if branch and a different reason to change on the else branch. Clearly, Such piece of code violates SRP. What we could do here based on the complexity of the logic is that we could extract the logic for the if branch to a separate method or class and the logic for the else branch to a different method or class. The same goes for switch statements. Each case represents one responsibility. Again, we can extract them to different methods or classes and make code more easy to read, maintain, and understand.

We should also pay attention to monster methods. We can identify them quickly because they have a large number of lines of code, and also they kind of mix the levels of abstraction within the implementation.
For example, below method is called getIncome.

Income getIncome (Employee e) {
    Income income = employeeRepository.getIncome(e.id);
    StateAuthorityApi.send(income, e.fullName);
    Payslip payslip = PayslipGenerator.get(income);
    JsonObject payslipJson = convertToJson(payslip);
    EmailService.send(e.email, payslipJson);
    ........
    return income;
}

By reading the method name, we would expect this method to return an employee income. But when we look at this method, we see that it does a whole lot of things. Indeed, it gets the income from the employeeRepository. But then it sends that income to the StateAuthority via an API. It generates a pay slip. It converts it to JSON. And then he uses that JSON and sends it via email. Obviously, this method does not do one thing. It has many responsibilities, many reasons to change, and it’s very, very fragile. Also notice the many dependencies that above method has. In order to do its job, it needs to know about the StateAuthorityApi, the Payslip, about JSON format, about the EmailService. This large number of dependencies is also a clear indicator that a method or a component does more than one thing. So, each time we see a monster method, we need to identify the responsibilities and then split it into multiple methods or even classes that are more manageable and that do just one thing.

Let’s take a look at God classes. Do you have projects where you keep helper methods, classes called Utils, helpers, shared, or something like this? Well, those are classes that clearly do more than they should. A typical Utils class will look like this. We have methods to save objects to a database. We have methods for serialization. We have logging in here. We have some friendly date helpers. And each time we write a piece of code that needs a place to live but it’s not important enough to have its own class, we would probably put it in one of these classes. Please don’t fall into this trap. Instead, prefer having specialized classes that handle pretty clear use cases. For example, you can have a utility class that just handles dates. You can give it a very meaningful name. Then you can have classes that deal only with serialization, classes that deal only with logging, classes that deal only with persistence. Don’t put everything in a single class just because it’s easier or because others have done it.

People are also actors of change in software applications. For example, below method generates a report that is used by both HR and management. The report is pretty similar to both parties. But at some point in time, HR will want a specific set of features and management will want a different set of features. Because of those needs, it’s far better to separate the report and make a specific report for HR and a different one for management. This way both reports will have a single reason for change. This is a more subtle case of violating the single responsibility principle. Knowing when people are responsible for a piece of code depends entirely up to you and your expertise of the business domain. But it’s worth knowing that the single responsibility principle is not all about code. It’s also all about the actors who use your application.

The Danger of Multiple Responsibilities

  • Code with multiple responsibilities is more difficult to read and reason about. Developers spend 90% of their time reading and understanding code, so simplifying this process is essential.
  • Code with many responsibilities has poor quality because it is harder to test.
  • Side effects are a symptom of not following the Single Responsibility Principle (SRP). Side effects occur when a function declares one purpose but performs additional hidden actions internally. They are dangerous because they are often hidden and require digging into internal implementations to identify.
  • High coupling is the most dangerous symptom of not following SRP.Coupling refers to the level of interdependency between software components. Components with multiple reasons to change are typically tightly coupled. Coupling with concrete components is particularly risky as it exposes dependencies to internal implementations.

The above getIncome method was depended on the concrete RepositoryImpl class. When the RepositoryImpl constructor changes, the getIncome method may break, introducing fragility and rigidity. A better approach is to create an abstraction for RepositoryImpl and pass it as a parameter. This ensures getIncome is not aware of RepositoryImpl internals and remains focused on its task.

Key takeaway: If Module A knows too much about Module B, changes in Module B can break Module A. Always pay attention to component dependencies and extract or abstract them as much as possible.

Demo: Applying the Single Responsibility Principle

EmployeeService Without SRP

In this approach, the EmployeeService class is responsible for multiple tasks: serialization, writing to a file, and logging to the console. This violates the Single Responsibility Principle.

import java.io.FileWriter;
import java.io.IOException;
import com.google.gson.Gson;

class Employee {
    private String id;
    private String name;

    // Constructor, Getters, and Setters
    public Employee(String id, String name) {
        this.id = id;
        this.name = name;
    }
    public String getId() {
        return id;
    }
    public String getName() {
        return name;
    }
}

class EmployeeService {
    public void processEmployee(Employee employee) {
        Gson gson = new Gson();

        // Serialize the Employee object
        String serializedEmployee = gson.toJson(employee);

        // Write serialized data to a file
        try (FileWriter fileWriter = new FileWriter("employee.json")) {
            fileWriter.write(serializedEmployee);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Log serialized data to the console
        System.out.println("Serialized Employee: " + serializedEmployee);
    }
}

public class WithoutSRPExample {
    public static void main(String[] args) {
        Employee employee = new Employee("1", "John Doe");
        EmployeeService employeeService = new EmployeeService();
        employeeService.processEmployee(employee);
    }
}

Refactored EmployeeService With SRP

import java.io.FileWriter;
import java.io.IOException;
import com.google.gson.Gson;

// Employee Class
class Employee {
    private String id;
    private String name;

    // Constructor, Getters, and Setters
    public Employee(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

// Serialization Service
class EmployeeSerializer {
    private Gson gson = new Gson();

    public String serialize(Employee employee) {
        return gson.toJson(employee);
    }
}

// File Writing Service
class EmployeeFileWriter {
    public void writeToFile(String serializedData, String fileName) {
        try (FileWriter fileWriter = new FileWriter(fileName)) {
            fileWriter.write(serializedData);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// Logging Service
class EmployeeLogger {
    public void logToConsole(String message) {
        System.out.println("Log: " + message);
    }
}

// EmployeeService Class
class EmployeeService {
    private EmployeeSerializer serializer = new EmployeeSerializer();
    private EmployeeFileWriter fileWriter = new EmployeeFileWriter();
    private EmployeeLogger logger = new EmployeeLogger();

    public void processEmployee(Employee employee) {
        // Serialize
        String serializedEmployee = serializer.serialize(employee);

        // Write to File
        fileWriter.writeToFile(serializedEmployee, "employee.json");

        // Log to Console
        logger.logToConsole("Serialized Employee: " + serializedEmployee);
    }
}

// Main Class
public class WithSRPExample {
    public static void main(String[] args) {
        Employee employee = new Employee("1", "John Doe");
        EmployeeService employeeService = new EmployeeService();
        employeeService.processEmployee(employee);
    }
}

Conclusion

This article explored the single responsibility principle, focusing on identifying reasons for change in classes and methods. We discussed how indicators like if statements, switch statements, God classes, and monster methods signal the need for refactoring. High coupling and code fragility were also covered, as code with many dependencies tends to be brittle. The article demonstrated how to refactor responsibilities into specialized components, using a three-step approach: identifying reasons for change, extracting them into specialized components, and refactoring the code. As The Pragmatic Programmer says, “We always want to design components that are self-contained, independent, and with a single and well-defined purpose.”

Scroll to Top