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.