In the book Design Patterns Explained, the author uses the example of a smartphone capturing an image of a UFO to illustrate the Single Responsibility Principle. The design of modern smartphones, which combine numerous functions—such as calling, photography, and music playback—violates this principle. As a result, smartphones perform poorly in specialized tasks: their camera quality falls short compared to dedicated cameras, and their audio output is inferior to that of CD or MP3 players.
After studying the book, here is my interpretation of the Single Responsibility Principle.
Concept
What exactly is the Single Responsibility Principle? The book defines it as: "A class should have only one reason to change." While this phrasing may sound abstract, its essence is straightforward: a class or module should be responsible for only one specific task or function. This promotes cleen, maintainable, and easily understandable code.
We’ve already applied this principle in previous discussions on the Simple Factory Pattern and Strategy Pattern. In the calculator example, we separated calculation logic (addition, subtraction, multiplication, division) from user interface logic by introducing a base Operation class and specific subclasses. Similarly, in the supermarket billing scenario, we decoupled pricing strategies from UI logic using a CashSuper base class and various strategy implementations. These are classic examples of applying the Single Responsibility Principle.
Let’s now explore another simple example to deepen our understanding.
Suppose we are building an employee management system that retrieves employee data based on name and age from a database. Initially, we might create a Java class called Employee:
public class Employee {
private String name;
private Integer age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return this.age;
}
public void setAge(Integer age) {
this.age = age;
}
public List<Employee> searchEmployee() {
// Logic to query employee records from the database
}
}
This design violates the Single Responsibility Principle because the Employee class has two distinct responsibilities:
- Managing employee attributes (name and age).
- Performing database searches for employee information.
As a result, changes to either attribute rules or search logic will require modifications to the same class. Although this may seem manageable in small-scale applications, imagine if this class contained hundreds of operations—each requiring updates would lead to a highly complex, unmaintainable class.
To adhere to the principle, we should separate these concerns. We can introduce a new class named DataRepository that handles all data retrieval tasks. Move the searchEmployee() method into this new class, and pass an Employee instance through the constructor:
public class DataRepository {
private Employee employee;
public DataRepository(Employee employee) {
this.employee = employee;
}
public List<Employee> searchEmployee() {
// Implementation for querying employee records
}
}
With this refactoring, we've split the original monolithic Employee class into two focused classes, each handling a single responsibility.
Benefits
Adhering to the Single Responsibility Principle brings several advantages:
-
Clarity and Readability: Each class focuses on one purpose, making the code easier to understand. Developers can quickly locate relevant functionality without sifting through unrelated logic.
-
Maintainability: Reduced coupling means changes to one feature do not affect others. When modifying a behavior, you only need to work within the relevant class, minimizing the risk of unintended side effects.
-
Reusability: Classes with a single responsibility are more likely to be reused across different parts of a project or even in other projects, since they don’t carry dependencies from unrelated tasks.
-
Testability: Unit testing becomes simpler. You can write independent tests for each responsibility, ensuring that every function behaves correctly in isolation.
-
Flexibility and Extensibility: Adding new features is easier. Instead of modifying existing code, you can create a new class to handle the new responsibility, preserving the integrity of the current implementation.
In summary, following the Single Responsibility Principle leads to higher-quality software systems—more maintainable, scalable, and resilient to change. It enhances development efficiency and reduces the likelihood of bugs.
