DEV Community

Cover image for Its not a Swiss Army Knife - Single Responsibility Principle
Abhinav Pandey
Abhinav Pandey

Posted on • Edited on

Its not a Swiss Army Knife - Single Responsibility Principle

The Single responsibility principle (SRP) can be explained in two parts:

  1. A class should have only one reason to change. - It performs one task well and it only changes when it needs to perform that same task differently.

  2. Only one actor should cause the change. - An actor is best explained as a person or a role who brings in the requirements which could potentially require a change in the class.

Why can't my class have multiple responsibilities ?

Requirements change over time. When a class changes, it could in turn mean that classes that depend on it will be impacted too. You will need to recompile these classes, re-test every functional component involved and tackle any side-effects in the process.

If your class is performing multiple responsibilities, it will have different reasons to change and will be updated frequently.

Over the time, the code for the class will get complicated and accommodating new changes will take more time than it should have. This also means that if there is a bug, you will have a hard time debugging it. Maybe you wouldn't even know where it is written and will have to get there the hard way.

Why there shouldn't be multiple actors demanding the change?

Your class is used to implement a full-fledged functionality (hopefully not many functionalities). In any average end-to-end functionality, requirements come from multiple sources - For e.g., business analysts, web designers, database administrators, third-party integrators, etc. If a class performs a composite task which can be impacted by several actors, it will be changed more frequently. We will see this in the code example below.

It makes your code less robust if there are more than one scenarios in which it can be changed. Developers who changed it to fulfil one requirement may not be aware of the impact it can create on the other requirement which they did not touch. This is in fact the cause of majority of code merge conflicts. Two developers were trying to solve two different problems by changing the same code.

Let's see some code

Consider the below class which is concerned with creating a new subscription when a user submits a subscription form:

public class SubscriptionCreator {
    private User user;

    public SubscriptionCreator(User user) {
        this.user = user;
    }
    public void createSubcription() {
        if(isUserValid()) {
            updateSubscribers();
            sendEmail();   
        }
    }

    private boolean isUserValid() {
        //validates the details entered by a user
    }

    private void sendEmail() {
        //sends an email to the user that subscription is sucessful
    }

    private void updateSubscribers() {
        // updates subscriber list in a db
    }
}
Enter fullscreen mode Exit fullscreen mode

The class looks fairly simple. However, we can see that it fails to follow SRP:

Multiple reasons to change - It performs two tasks - validates the details entered by the user. It will be susceptible to change for below reasons now -

  1. If there is a change in user detail validations.
  2. If there is a change in the email structure.
  3. If there is a change in user attributes being stored in database.

Multiple actors causing the change - Lets consider the scenarios below:

  1. The product owners want you to add a new validation i.e., a change in business logic. They want to create subscriptions only if the user is more than 18 years in age.
  2. The content designers want you to display a different message in the email if the customer is located in European region i.e., a change in presentation logic.
  3. The database administrator instructs that user profiles should have a full name attribute i.e., a change in persistence logic.

How can we improve it?

Let's separate the reasons to change (or tasks being performed) and replace them with specialized classes (or interfaces) which encapsulate it better.

public class SubscriptionCreator {
    private User user;
    private ValidationService validationService;
    private EmailService emailService;
    private DatabaseService databaseService;
    public SubscriptionCreator(User user, ValidationService validationService, EmailService emailService) {
        this.user = user;
        this.validationService = validationService; 
        this.emailService = emailService; 
    }

    public void createSubcription() {
        if(validationService.isUserValid(user)) { 
            databaseService.updateSubscribers(user);            
            emailService.sendEmail(user);   
        }
    }
}

public interface EmailService {
    //only concerned with sending an email
    void sendEmail(User user);
    ...
}

public interface ValidationService {
    //only concerned with validating the user details
    void isUserValid(User user);
    ...
}

public interface DatabaseService {
    //only concerned with database interactions
    void updateSubscribers (User user);
    ...
}
Enter fullscreen mode Exit fullscreen mode

Now the implementation of EmailService will only change if there is a change concerning email content. ValidationService will only change if you need to change the validation logic. DatabaseService only changes if there is a change from our Database administrator. Our original class SubscriptionCreator will change only if the sequence of operations being performed in the subscription process changes.

Benefits

  1. More understandable - The classes have high cohesion and their methods are fairly small in number and self-explanatory.
  2. Reduced code conflicts - When the changes come from one actor, it is unlikely that two developers will change it simultaneously.
  3. Prevents collateral damage - Introducing new changes becomes easier as their impact area decreases.

Thought Experiment

  1. Is SRP only concerned with classes? - SRP forms the basis of a lot of good stuff. It was initially proposed at the class level but the principle can be applied at any level - methods, components, modules - and does not limit itself to object oriented programming. It can be applied very well in functional programming as well considering functions as the fundamental unit. Moreover, it is the basis of domain driven design. Microservices architecture is in principle an implementation of SRP at service level.
  2. Should you always follow it? - In most cases it will be useful to keep SRP in mind while writing your code. However, it is not a rule of thumb. You have to use your discretion while designing your solution and you may not always have benefits of putting in extra efforts to comply with the principle.

Top comments (0)