The Single Responsibility Principle

Sharing is caring

What is the Single Responsibility Principle?

The SRP is often misinterpreted to stipulate that a module should only do one thing. While designing functions only to have one purpose is good software engineering practice, it is not what the single responsibility principle states.

In a nutshell, the single responsibility principle is one of the SOLID principles for developing clean and modular software architectures. The single responsibility principle states that a module or class should only be responsible to one actor.

Don’t try to turn a module into a swiss army knife. Source

Why Should You Apply the Single Responsibility Principle?

The SOLID principles exist to help engineers and architects compose software systems out of cleanly separated components. The main goal is to avoid the dreaded spaghetti monolith in which everything depends on everything else, and changes have ripple effects across the entire system.

Imagine a module A is responsible to more than one other module or external stakeholder, meaning that more than one actor directly relies on its functionality. If the requirements of one of these actors change, module A has to change in response. But the other actors will still require the functionality of the original module A. So you pile on new functionality and perhaps expand the interface of module A in response to the new requirements while still maintaining the old functionality and interface for the other actors.
Now imagine another one of the dependent actors has a change in requirements. It’s not hard to see that module A will soon grow into a huge code monster in order to satisfy the changing requirements of many actors. This process of continuously expanding module A may work for a while, but it increasingly becomes a nightmare to maintain and expand.

The single responsibility principle helps you avoid such a scenario. It is also often described as stating that “a module should only have one reason to change.” But this leaves room for an interpretation that may be too restrictive. Since a module changes in response to the requirements of other actors, it is sufficient to respond to one or more changing requirements, provided all changes come from one and only one actor.

Single Responsibility Principle Example

Suppose we are tasked with building an application to manage a book store. In the book store application, we have a BookQuery class that has the following methods:

getBookPricebyTitle(title)

The clerk at the bookstore uses the first method to retrieve the prices of books at the customers’ requests. Now an inexperienced developer who has never heard of the single responsibility principle is tasked with adding functionality to the software that will allow the accounting team to retrieve the books sold in a specific timeframe and the money made through those sales.

Since the BookQuery class is already connected to the database, the developer thinks: “That’s easy. I just need to add two more methods to the class.”

getListOfBooksSold(date)

getTotalPriceofBookList(booklist)

The first function simply takes a date and then queries the company database for all books sold after that date, returning a list of titles. The second function accepts that list as an argument. It then iterates through the list using a simple for loop, calling the getBookPricebyTitle(title) method for every title in the list and adding up the total price.
The developer reports that he has solved the problem. The class and its functions are being tested on the current database of books and deployed into production.
The accounting department uses the function to calculate the revenue of the book store. At the end of the year, the accounting department realizes that they have consistently overstated their profits and paid much more in taxes than they would have had to. The owners are extremely angry. What happened?
Due to inflation, the prices of books have increased continuously over the year. Price changes are immediately entered into the database so that the clerks report the up-to-date price to the customers.
The accounting department has relied on the same method as the clerks for retrieving the most recent prices. But the book may have been sold several weeks ago when it still had the old, cheaper price.

This issue is a direct consequence of ignoring the single responsibility principle. By adding the two methods, the class has become responsible to the clerk as well as the accounting department. Once the requirements between the two actors diverged, a problem ensued.

Single Responsibility Principle vs. Separation of Concerns

Software architects often emphasize the importance of separating concerns. As the name implies, you want to ensure that the responsibilities of different modules are cleanly separated. In the example of the bookstore above, finding books in the library and accounting for the books sold are two different concerns. Accordingly, they should be handled by different classes and potentially by different modules. For example, you could create an accounting module and a book search module. The accounting module queries the database separately from the book search module and retrieves the historical data it needs to calculate the total value of books sold.
The developer above clearly violated the separation of concerns in his initial implementation. But by applying the single responsibility principle, he was forced to think about how to separate the code responsible to the two actors that were using it. By ultimately putting the two functions used by the accounting department into a new class, he automatically moved towards a better separation of concerns.

In other words, the single responsibility principle provides guidance during the development process that enables you to achieve a separation of concerns at a system level.

Single Responsibility Principle in Java

To see how the single responsibility principle works in practice, we use our previous example of the book store and write an implementation in Java.

First, we need to declare the BookQuery class that our developer is going to extend in violation of the SRP.

public class BookQuery {

    public String getBookPriceByTitle(String title){
        return "price";
    };
    //private methods for database access..
}

The Wrong Approach

Now our developer gets to work adding the functionality required by the accounting department. The accountants want to be able to retrieve a list of all the books sold, along with the total price of all the books. The developer adds two corresponding functions to the BookQuery class. The getListOfBooksSoldBetween function takes two date strings as an input, goes to the database, and retrieves all books that were sold between the two specified dates.

Then the list of books gets passed to the getTotalPriceofBookList function, which determines the current price of every book and adds those prices all up.

public class BookQuery {

    public String getBookPriceByTitle(String title) {
        //database access magic happening here
        return "price";
    };

    public String getListOfBooksSoldBetween(String beginningDate, String endDate) {
        //database access magic happening here
        return "date";
    };

    public String getTotalPriceofBookList(ArrayList<String> booklist) {
        //database access and price calculation magic happening here
        return "totalprice";
    };

}

As we’ve seen above, this structure would lead to potentially unforeseen consequences and is a violation of the single responsibility principle.

The Better Approach

We realized in the section above that the prices that the clerk retrieves may be different from those that the accounting department needs. The database should have a table with the books available in the store reflecting current prices and a different table of books sold that reflects the historical prices.

In our code, we should use different classes to access the different tables. First of all, we remove the offending methods from the BookQuery class. I’ve also moved the BookQuery class into a new Java package, “BookSearch” to indicate that it is in a different module.

package BookSearch;

import java.util.ArrayList;

public class BookQuery {

    public String getBookPriceByTitle(String title) {
        //database access magic happening here
        return "price";
    };

    //private methods for database access..


}

Then we create a new class which I’ve called “RevenueCalculator.” This class will host the methods removed from the previous class. It’s in a module called “Accounting.”

package Accounting;

import java.util.ArrayList;

public class RevenueCalculator {

    public ArrayList<String> getListOfBooksSoldBetween(String beginningDate, String endDate) {
        //database access magic happening here
        ArrayList<String> books = new ArrayList<String>();
        return books;
    };

    public String calculateTotalBookRevenue(ArrayList<String> booklist) {
        //database access and price calculation magic happening here
        return "totalprice";
    };


    //private methods for accessing database. These methods should access
    // a different database table compared to the Book Query table.

}

You may have realized that I’ve renamed the second method to something I find more appropriate. After all, the accounting department is not interested in the price of the books but revenue the store made from selling books in the past. This class will also host several private methods for accessing the database. But these will access a different table or set of database tables compared to the first.

Summary

By separating the code so that every module is responsible to only one actor, we achieve a cleaner separation of responsibilities. This separation helps us prevent unforeseen interdependencies that can lead to errors down the line once a system is already running in production.


Sharing is caring