Single Responsibility Principle

Single Responsibility Principle

An Ingredient of SOLID Design Principles

·

5 min read

The principle states that a class should have only one reason to change which means every class should have a single purpose. This principle applies to the software that is developed on different levels: methods, classes, modules, and services which can be summed up into components.

Example

Suppose we are building a simple e-commerce system, and we have a Product class that handles both product information and inventory management. This violates the SRP since it has two distinct responsibilities.

class Product {
    private String name;
    private double price;
    private int quantityInStock;

    public Product(String name, double price, int quantityInStock) {
        this.name = name;
        this.price = price;
        this.quantityInStock = quantityInStock;
    }

    // Methods related to product information
    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    // Methods related to inventory management
    public int getQuantityInStock() {
        return quantityInStock;
    }

    public void decreaseStock(int quantity) {
        if (quantity > 0 && quantity <= quantityInStock) {
            quantityInStock -= quantity;
        } else {
            throw new IllegalArgumentException("Invalid quantity");
        }
    }
}

In this example, the Product class is responsible for both product information (name, price) and inventory management (quantityInStock). To adhere to the SRP, we should separate these responsibilities into distinct classes.

Refactored Example:

class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }
}

class InventoryManager {
    private Product product;
    private int quantityInStock;

    public InventoryManager(Product product, int initialStock) {
        this.product = product;
        this.quantityInStock = initialStock;
    }

    public int getQuantityInStock() {
        return quantityInStock;
    }

    public void decreaseStock(int quantity) {
        if (quantity > 0 && quantity <= quantityInStock) {
            quantityInStock -= quantity;
        } else {
            throw new IllegalArgumentException("Invalid quantity");
        }
    }
}In the refactored example, the Product class handles only product information, and the InventoryManager class is responsible for inventory management. This adheres to the Single Responsibility Principle, making the code more maintainable and easier to extend.

Types of responsibilities

  • Business Logic

For instance, extracting phone numbers from text, and converting an XML document into JSON. Business logic responsibility is knowing how to do (or encapsulate) a business function. E.g. a class knowing how to convert XML documents into JSON.

  • External Integration

In low-level programming, this involves integration between modules within the application, such as putting a message into a queue which is processed by another subsystem. Integration with external systems may include database transactions, reading from writing to a distributed message queue such as Kafka, or RPC calls to other services.

  • Data

Profile data of a person, a JSON document, is a responsibility of class (object), but not of a method, module or service. A specific kind of data is configuration; a collection of parameters for some other method class or system.

  • Control Flow

This is a part of the application's control flow, execution, or data flow. An example of this responsibility is a method that orchestrates calls to components that have other responsibilities.

What are the factors for the consideration of the size of the Responsibility

Components should be broken down until each one has only one reason to change, this is according to Bob Martin (the first proponent of the Single Responsibility Principle ). However, when we consider the purpose of using the Single Responsibility Principle, it’s to improve the overall codebase and of its production behaviour.

Therefore the best determinant of our use of the SRP is its effects on the quality of the code and the running application. The optimal scope of the responsibility for a component highly depends on the context.

  • The responsibility of the component.

  • The non-functional requirements of the application or the component we’re developing.

  • Lifetime support of the code in the future.

  • Number of the people who will work with this code.

The Impact of the Single Responsibility Principle On Different Software Qualities

  • Understandability and Learning Curve

When we split responsibilities between smaller methods and classes, the system becomes easier to learn overall. We can learn smaller components iteratively, one at a time. When we transition into a new codebase, we can learn the components on a need-to-know basis ignoring unnecessary internal components which are not relevant for us.

  • Flexibility

We can combine independent components through separate control flow components in different ways for different purposes or depending on configuration.

  • Reusability

It’s much easier to reuse components when they have a single narrow responsibility.

Most methods with a narrow responsibility shouldn’t have side effects and shouldn’t depend on the state of the class, which enables sharing and calling them from any place. Thus Single Responsibility Principle nudges us towards a functional programming style.

  • Testability

It becomes much easier for us to write and maintain tests for methods and classes with focused, independent concerns.

  • Debuggability

When classes and methods are focused on a single concern, it is easier to locate and fix bugs. Debugging becomes simpler because you can focus on a specific aspect of the code without getting tangled in unrelated functionality.

  • Reliability

Having methods that have a single responsibility, changes to one aspect of the system are less likely to affect other parts. This reduces the risk of introducing unintended side effects and improves the overall reliability of the software.

  • Performance

The Single Responsibility Principle can lead to more efficient and optimized code. Classes with clear responsibilities are often more focused and easier to optimize for specific use cases.

  • Observability and Operability

With well-defined responsibilities, it's easier to monitor and observe the behaviour of individual components. For instance, if the product-related logic needs adjustment, you can focus on the Product class without affecting other parts of the system.