Single Responsibility Principle in Java with Examples
SOLID is an acronym used to refer to a group of five important principles followed in software development. This principle is an acronym of the five principles which are given below…
- Single Responsibility Principle (SRP)
- Open/Closed Principle
- Liskov’s Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
In this post, we will learn more about the Single Responsibility Principle. As the name indicates, it states that all classes and modules should have only 1 well-defined responsibility. As per Robert C Martin,
A class should have one, and only one reason to change.
This means when we design our classes, we need to ensure that our class is responsible only for 1 task or functionality and when there is a change in that task/functionality, only then, that class should change.
In the world of software, change is the only constant factor. When requirements change and when our classes do not adhere to this principle, we would be making too many changes to our classes to make our classes adaptable to the new business requirements. This could involve lots of side effects, retesting, and introducing new bugs. Also, our dependent classes need to change, thereby recompiling the classes and changing test cases. Thus, the whole application will need to be retested to ensure that new functionality did not break the existing working code.
Generally in long-running software applications, as and when new requirements come up, developers are tempted to add new methods and functionality to the existing code which makes the classes bloated and hard to test and understand. It is always a good practice to look into the existing classes and see if the new requirements fit into the existing class or should there be a new class designed for the same.
Benefits of Single Responsibility Principle
- When an application has multiple classes, each of them following this principle, then the applicable becomes more maintainable, easier to understand.
- The code quality of the application is better, thereby having fewer defects.
- Onboarding new members are easy, and they can start contributing much faster.
- Testing and writing test cases is much simpler
In the java world, we have a lot of frameworks that follow this principle. JSR 380 validation API is a good example that follows this principle. It has annotations like @NotNull, @Max, @MIn, @Size which are applied to the bean properties to ensure that the bean attributes meet the specific criteria. Thus, the validation API has just 1 responsibility of applying validation rules on bean properties and notifying with error messages when the bean properties do not match the specific criteria
Another example is Spring Data JPA which takes care of all the CRUD operations. It has one responsibility of defining a standardized way to store, retrieve entity data from persistent storage. It eases development effort by removing the tedious task of writing boilerplate JDBC code to store entities in a database.
Spring Framework in general, is also a great example of Single Responsibility in practice. Spring framework is quite vast, with many modules – each module catering to one specific responsibility/functionality. We only add relevant modules in our dependency pom based on our needs.
Let’s look at one more example to understand this concept better. Consider a food delivery application that takes food orders, calculates the bill, and delivers it to customers. We can have 1 separate class for each of the tasks to be performed, and then the main class can just invoke those classes to get these actions done one after the other.
Preparing order for customer -John who has ordered Pizza Order with order id Pizza-57 has a total bill amount of 46 Delivering the order Order with order id as Pizza-57 being delivered to John Order is to be delivered to: Pune
We have a Customer class that has customer attributes like name, address. Order class has all order information like item name, quantity.
The BillCalculation class calculates the total bill sets the bill amount in the order object. The DeliveryApp has 1 task of delivering the order to the customer. In the real world, these classes would be more complex and might require their functionality to be further broken down into multiple classes.
For example, the bill calculation logic might require some kind of lookup functionality to be implemented where we look for the price of each item included in the order against some kind of database, add them up, add taxes, delivery charges, etc and finally reach the total price. Depending on how complex the code starts to become, we might want to move the taxes, database queries etc, to other separate classes. Similarly, the delivery class might want to interface with another task management system that actually assigns the task of delivery to different delivery agents based on location, shift timings, whether that delivery person has actually shown up to work, etc. These individual steps could move to separate classes when they need specialized handling.
If the functionality of bill calculation, as well as order delivery, was added in the same class, then that class gets modified whenever the bill calculation logic or the delivery agent logic needs to change; which goes against the Single Responsibility Principle. As per the example, we have a separate class for handling each of these functions. Any single business requirement change should ideally have an impact on only one class, thus catering to the Single Responsibility Principle.