Tuesday, November 19, 2024
Google search engine
HomeLanguagesJavaSingle Responsibility Principle in Java with Examples

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…

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle
  3. Liskov’s Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. 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

Examples

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.

Java




import java.io.*;
import java.util.*;
 
class GFG {
    public static void main(String[] args)
    {
        Customer customer1 = new Customer();
        customer1.setName("John");
        customer1.setAddress("Pune");
        Order order1 = new Order();
        order1.setItemName("Pizza");
        order1.setQuantity(2);
        order1.setCustomer(customer1);
 
        order1.prepareOrder();
 
        BillCalculation billCalculation
            = new BillCalculation(order1);
        billCalculation.calculateBill();
 
        DeliveryApp deliveryApp = new DeliveryApp(order1);
        deliveryApp.delivery();
    }
}
 
class Customer {
    private String name;
    private String address;
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getAddress() { return address; }
    public void setAddress(String address)
    {
        this.address = address;
    }
}
 
class Order {
 
    private Customer customer;
    private String orderId;
    private String itemName;
    private int quantity;
    private int totalBillAmt;
 
    public Customer getCustomer() { return customer; }
    public void setCustomer(Customer customer)
    {
        this.customer = customer;
    }
    public String getOrderId() { return orderId; }
    public void setOrderId(String orderId)
    {
        Random random = new Random();
 
        this.orderId = orderId + "-" + random.nextInt(500);
    }
    public String getItemName() { return itemName; }
    public void setItemName(String itemName)
    {
        this.itemName = itemName;
        setOrderId(itemName);
    }
    public int getQuantity() { return quantity; }
    public void setQuantity(int quantity)
    {
        this.quantity = quantity;
    }
    public int getTotalBillAmt() { return totalBillAmt; }
    public void setTotalBillAmt(int totalBillAmt)
    {
        this.totalBillAmt = totalBillAmt;
    }
 
    public void prepareOrder()
    {
        System.out.println("Preparing order for customer -"
                           + this.getCustomer().getName()
                           + " who has ordered "
                           + this.getItemName());
    }
}
 
class BillCalculation {
 
    private Order order;
    public BillCalculation(Order order)
    {
        this.order = order;
    }
 
    public void calculateBill()
    {
        /* In the real world, we would want a kind of lookup
          functionality implemented here where we look for
          the price of each item included in the order, add
          them up and add taxes, delivery charges, etc on
          top to reach the total price. We will simulate
          this behaviour here, by generating a random number
          for total price.
        */
        Random rand = new Random();
        int totalAmt
            = rand.nextInt(200) * this.order.getQuantity();
 
        this.order.setTotalBillAmt(totalAmt);
        System.out.println("Order with order id  "
                           + this.order.getOrderId()
                           + " has a total bill amount of "
                           + this.order.getTotalBillAmt());
    }
}
 
class DeliveryApp {
 
    private Order order;
    public DeliveryApp(Order order) { this.order = order; }
 
    public void delivery()
    {
        // Here, we would want to interface with another
        // system which actually assigns the task of
        // delivery to different persons
        // based on location, etc.
        System.out.println("Delivering the order");
        System.out.println(
            "Order with order id as "
            + this.order.getOrderId()
            + " being delivered to "
            + this.order.getCustomer().getName());
        System.out.println(
            "Order is to be delivered to: "
            + this.order.getCustomer().getAddress());
    }
}


Output

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.

RELATED ARTICLES

Most Popular

Recent Comments