SOLID Principle Part 1: Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) is one of the SOLID principles in software engineering that states that every module or class should have only one responsibility and that responsibility should be entirely encapsulated within the class.

In other words, a class should have only one reason to change and it should be focused on doing one thing well. This principle helps us to maintain, test and scale the codebase easily because changes in requirements do not affect multiple areas of the codebase.

Let's Start with a Story!

Mohan was sitting in his room, waiting for his next task to be assigned after promotion. Soon Rahul came to his office. Rahul: "Congratulation on your promotion Mohan!" Mohan: "Thanks, Rahul."

Rahul: "As you already know, the US team is moving on to another project. So now it’s our responsibility to sunset their current project The Dosa Maker and come up with an innovative design for the brand new The Dosa Maker V2.0."

Mohan: "In that case, why do we need a V2? Can’t we use the same code here?"

Rahul: "Stop talking nonsense! If we don’t duplicate, then who will pay for your salary? :)"

Mohan: "Got it, I will do my best :)"


This is what the Dosa Maker Modules look like.

class DosaMaker {
    makeDosa();
    makeBatter();
    cook();
    serve();
    takeOrder();
    takePayment();
} 

The Wise Developer: "Do you see any problem with this?"

Mohan: "It’s a mess! Why a single class is handling so many responsibilities?"

The Wise Developer: "Good question Mohan, seems like that promotion wasn’t a fluke!"

A more well-known term for such type of class is God Class. As the class seems to be doing everything instead of delegating those responsibilities to smaller units. A good guiding principle for finding such a class is to try writing down a brief description of the class without using conjunctions like if, and, also, etc. If you can’t, then probably the class is handling multiple responsibilities. Let’s apply the principle here! The Dosa Maker class takes orders and payment, prepares the dosa, and serves it to the customer.

The Wise Developer: "Do you know why classes with overloaded responsibilities are a bad idea?"

Mohan: "Hmm, I can think of a few reasons. Testing such classes is hard because: God classes usually have complex state variables and too many permutations of states that need to be tested. For each test, we need to write significant logic just for state initialization and management. On other side, A class with too many reasons to change has too many reasons to break as well. And it’s hard to figure out what went wrong."

The wise developer: "Correct! But there are a couple of concepts that you need to be aware of to completely understand why overloading is a bad idea, and those are: Overloading an entity with multiple responsibilities Increases Coupling and Decreases Cohesion."

Mohan: "What does that mean?"

A good system should be ‘high’ on cohesion and ‘low’ on coupling.

Example of coupling and cohesion in oops

Cohesion in OOPS

Cohesion refers to the degree to which the elements inside a component belong together. In other words, Cohesion measures the relevance between the logical sub-units within a component. For example, Can you comment on the cohesiveness of this class? Hint: Try finding the logical sub-units first.

class BreakfastMaker {
    ....
    Dosa dosa;
    makeDosa() {
        // recipe for making a delicious dosa
    }
    
    serveDosa() {
        // logic for serving dosa
    }
    
    ....
    Idli idli;
    makeIdli() {
        // recipe for making idli
    }
        
    serveIdli() {
        // logic for serving idly
    }
    
    ....
    Coffee coffee;
    makeCoffee() {
        // recipe for making coffee
    }
    
    serveCoffee() {
        // logic for serving coffee
    }
}  

The Wise Developer: "What Do you think? Is this class highly cohesive or slightly cohesive?"

Mohan: "If we look at the dependency graph of the above class, we can clearly see 3 disconnected entities. As there is no dependency between these sub-units, I think this class can be categorized as slightly cohesive".

Dependency graph of breakfast maker class

Dependency graph of breakfast maker class

The Wise Developer: "Yes, that’s a correct observation."

If your class or system is slightly cohesive, it means:

  • The system can be divided into smaller and more cohesive units.
  • The logical flow of the system crosses its boundaries, which suggests that redefining these boundaries could improve cohesion.

On the other hand, a "highly cohesive" system implies:

  • The system is an atomic unit that cannot be easily divided into smaller, more cohesive units.
  • All the logical dependencies needed for its function are contained within the system, not outside it.

For example, a mobile phone is a highly cohesive system. It has multiple logical sub-units, such as a battery, screen, circuit board, camera, etc., and they are all interdependent. However, since all these dependencies are within the system boundary, the phone functions well as a single, unified unit.

Coupling in OOPS

Coupling is the degree of interdependence between software modules. In other words, Coupling measures the relevance between multiple modules, i.e, how closely connected two modules are and what is the strength of the relationships between modules.

Low coupling means that there are fewer dependencies on other modules, thereby making systems easier to change, test, and reuse in the future. Conversely, High coupling means that your module is strongly dependent on other modules for its function, thereby making the system:

  • Brittle, as changes in one sub-system can potentially cause failure in another sub-system.
  • Harder to test, as you will need to mock all those dependencies before writing any tests.
  • Harder to reuse, as the system is not isolated. 

Food for thought: Reusability vs Coupling

Mohan: "Is it even possible to have a completely independent system? Don’t you think we will end up re-implementing the functionality of other modules if we focus solely on loose coupling?"

The Wise Developer: "Any system will have some degree of dependence on another system, hence it's not possible to create a completely independent system. And you are right, if we focus solely on loose coupling, we will end up violating the DRY principle, which states that we should strive for reducing duplication in our code."

As it happens in life as well, there is no correct answer here, what you need to remember is that a wise developer always strives for balance in conflict i.e. Whenever you have to decide between duplicating code and reducing coupling, do the cost-benefit analysis of both approaches, try thinking about what would be more beneficial in the longer term of the project life cycle, and make the decision accordingly.


Now we have a basic foundation for coupling and cohesion in OOPS.

Lets understand Single Responsibility Principle via an example

“A class should have only one reason to change”  — Robert C. Martin. This is popularly known as the Single Responsibility Principle (SRP), a term coined by Robert C. Martin, author of SOLID principles. Let's apply this principle by breaking down the DosaMaker module. Currently, the DosaMaker class manages three distinct responsibilities, which are:

  1. Order and payment management (Cashier)
  2. Dish preparation (Chef)
  3. Dish serving (Waiter)

Therefore, in the code, we can separate these three responsibilities of the DosaMaker class into three separate classes: Cashier, Chef, and Waiter. By doing this, each class will have a single, focused responsibility, making the code more organized, maintainable, and easy to understand.

class Cashier {
    public void takeOrder(String dish) {
        System.out.println("Order received: " + dish);
    }

    public void processPayment() {
        System.out.println("Payment processed");
    }
}

class Chef {
    public void prepareDish(String dish) {
        System.out.println("Preparing " + dish);
    }
}

class Waiter {
    public void serveDish(String dish) {
        System.out.println("Serving " + dish);
    }
}

public class DosaMaker {
    public static void main(String[] args) {
        Cashier cashier = new Cashier();
        Chef chef = new Chef();
        Waiter waiter = new Waiter();

        String dish = "Masala Dosa";

        cashier.takeOrder(dish);
        cashier.processPayment();
        chef.prepareDish(dish);
        waiter.serveDish(dish);
    }
}

The above code is already highly cohesive because each class has a clear and single responsibility. But we can still be improve in terms of coupling. To make the code less coupled, we can use the Dependency Injection principle i.e. instead of creating objects within the DosaMaker class, we will pass them as parameters to the DosaMaker class.

class Cashier {
    public void takeOrder(String dish) {
        System.out.println("Order received: " + dish);
    }

    public void processPayment() {
        System.out.println("Payment processed");
    }
}

class Chef {
    public void prepareDish(String dish) {
        System.out.println("Preparing " + dish);
    }
}

class Waiter {
    public void serveDish(String dish) {
        System.out.println("Serving " + dish);
    }
}

class DosaMaker {
    private Cashier cashier;
    private Chef chef;
    private Waiter waiter;

    public DosaMaker(Cashier cashier, Chef chef, Waiter waiter) {
        this.cashier = cashier;
        this.chef = chef;
        this.waiter = waiter;
    }

    public void makeDosa(String dish) {
        cashier.takeOrder(dish);
        cashier.processPayment();
        chef.prepareDish(dish);
        waiter.serveDish(dish);
    }
}    

public class Main {
    public static void main(String[] args) {
        Cashier cashier = new Cashier();
        Chef chef = new Chef();
        Waiter waiter = new Waiter();
        DosaMaker dosaMaker = new DosaMaker(cashier, chef, waiter);

        String dish = "Masala Dosa";
        dosaMaker.makeDosa(dish);
    }
}

In the above code, DosaMaker class is no longer dependent on the specific implementation of the Cashier, Chef, and Waiter classes. This will make code more flexible and easier to maintain.

Advantages of Single Responsibility Principle

  • Improved maintainability: We can easily understand, modify and test classes with a single responsibility, which reduces the chances of bugs or unintended side effects.
  • Better separation of concerns: By separating responsibilities into different classes, the code will be more modular and organized.
  • Enhanced reusability: We can easily reuse or modify classes designed with a single responsibility in different contexts.
  • Enhanced modularity: By breaking down code into smaller, single-responsibility classes, it becomes easier to add or remove functionality without affecting other parts of the code.

Real-life use cases of Single Responsibility Principle

We can apply Single Responsibility Principle in many real-life scenarios to improve software design and reduce complexity.

  • Web development: We can have separate classes (with their own single responsibility) for handling user authentication, database access, view rendering, etc.
  • Mobile app development: We can design separate classes with single responsibility for handling user input, displaying data, managing network requests, etc.
  • Game development: We can have separate classes for handling player movement, enemy movement, sound effects, etc.
  • Financial software: We can have separate classes for handling transactions, generating reports, calculating taxes, etc.
  • E-commerce websites: We can design separate classes for handling customer orders, product catalog management, payment processing, etc.

Enjoy learning, Enjoy oops!

More from EnjoyAlgorithms

Self-paced Courses and Blogs