SOLID Principles Series: Single Responsibility Principle (SRP)

Every software component should have one and only one responsibility.

There are two concepts that can help us understand more the meaning of the Single Responsibility Principle:

I. Cohesion

Cohesion is the degree to which the various parts of a software component are related.

Take a look at the following Square class:

public class Square {
    int side = 5;

    public int calculateArea() {
        return side * side;
    }

    public int calculatePerimeter() {
        return side * 4
    }

    public void draw() {
        if (highResolutionMonitor) {
          // Render a high resolution image of a square
        } else {
          // Render a normal image of a square
    }

    public void rotate(int degree) {
        // Rotate the image of the square clockwise to
        // the required degree and re-render
    }
}

The methods calculateArea() and calculatePerimeter() are closely related as they both deal with the measurements of a square. This means there is a high level of cohesion between these two methods.

In addition, the draw() and the rotate() methods deal with the image rendering of the square, and they also have high cohesion when grouped together.

However, if you combine all methods as a whole, the level of cohesion is low. For instance, the calculatePerimeter() method has an entirely different responsibility than the draw() method.

To increase the level of cohesion, we can split them into two classes:

 public class Square {
    int side = 5;

    public int calculateArea() {
        return side * side;
    }

    public int calculatePerimeter() {
        return side * 4
    }
}
 public class SquareUI {
    public void draw() {
        if (highResolutionMonitor) {
          // Render a high resolution image of a square
        } else {
          // Render a normal image of a square
    }

    public void rotate(int degree) {
        // Rotate the image of the square clockwise to
        // the required degree and re-render
    }
}

As a result, we increased the level of cohesion in each of the classes. In the Single Responsibility Principle, we should always aim for high cohesion within a component so that we can assign a single responsibility to all of its methods as a whole. As for our example, we can say that the responsibility of the Square class is to deal with the measurements related to a square. Similarly, the responsibility of the SquareUI class is to deal with rendering the image of the square.

II. Coupling

Coupling is defined as the level of interdependency between various software components.

Take a look at the example Student class below which has a save method that will convert the Student class into a serialized form and persist it in a database.

public class Student {
    private String studentID;
    private Date studentDOB;
    private String address;

    public void save() {
        // Serialize object into a sting representation
        String objectStr = MyUtils.serializeIntoAsString(this);
        Connection connection = null;
        Statemente stmt = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyDB", "root", "password");
            stmt = connection.createStatement();
            stmt.execute("INSERT INTO STUDENT VALUES (" + objectSTR + ")");
        } catch (Exception e) {
            e.printStackTrace();
        } 
     }

    public String getStudentId() {
            return studentId;
    }

    public void setStudentId(String studentId) {
          this.studentId = studentId;
     }
}

The save method deals with a lot of low-level details related to handling record insertion into a database. Assuming that now we are using MySQL as the database. If in the future there is a need to change the database into MongoDB, we can expect that most of the above code inside the method will change. This means that the Student class is tightly coupled with the database layer that we use at the backend.

Ideally, the Student class should only deal with basic student-related functionalities like getting the student id, date of birth, address, or any related information. It should not include low-level details like the backend database.

To fix this, we need to refactor our code a little bit.

public class Student {
    private String studentID;
    private Date studentDOB;
    private String address;

    public void save() {
        new StudentRepository().save(this);
     }

    public String getStudentId() {
            return studentId;
    }

    public void setStudentId(String studentId) {
          this.studentId = studentId;
     }
}
public class StudentRepository {

    public void save(Student student) {
        // Serialize object into a sting representation
        String objectStr = MyUtils.serializeIntoAsString(this);
        Connection connection = null;
        Statemente stmt = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyDB", "root", "password");
            stmt = connection.createStatement();
            stmt.execute("INSERT INTO STUDENT VALUES (" + objectSTR + ")");
        } catch (Exception e) {
            e.printStackTrace();
        } 
     }
}

As noticed, we moved the database-related code to a new StudentRepository class and called the save method inside that class from the Student class save method. If there is a need to change the underlying database in the future, there is no need for the Student class to be changed and recompiled. This made our code loosely coupled. This also goes with the rule that Every software component should have only one reason to change.

In terms of responsibilities, the Student class has the responsibility of dealing with core student-related data, whereas the StudentRepository class deals with the database operations.

In summary, the Single Responsibility Principle always advocates higher cohesion and always recommends loose coupling.