How to use Single Responsibility Principle the Easy Way

In this article, let's understand the context of Single Responsibility and everything in detail with examples and use cases

Flexibility, Maintainability and Extensibility are some of the buzzwords we often hear when dealing with developing application software.

While these are the desired characteristics of an ideal application component developed, we seldom forget to keep them in mind while designing our strategies owing to tighter deadlines and various other factors which often go hand in hand with development.

There are simple principles which help us design and develop some really good components which also comply with the desired characteristics of an ideal software, and we call them the SOLID principles.

SOLID is an acronym coined for five guiding principles which describe the things we need to keep in mind while designing a component or developing a system. In this article we shall discuss the first of the five principles – the Single Responsibility Principle (S).

What is Single Responsibility Principle?

Single Responsibility Principle or shortly called SRP is the first letter of SOLID (S) which talks about a component’s flexibility and maintainability. The rule states that:

A component (a class, method or a function) must contain only one function to perform. It must have only one reason to change.

We generally tend to stuff up a single component (say a class or a method) with all the logic we need it to implement without keeping in mind of its future. What would happen if some of the dependencies of it need to change? Or how would that component embrace any extension for a part of its functionality. We call such components as God Objects.

What is a God Object?

A God object can be defined as an object which knows more than what it needs to know, or does more than what it is expected to do. All the components we develop can become god objects if we don’t have a vision on what we expect that particular component to perform or what that component needs to actually contain. For example let’s assume we have an API for Readers and Users which caters a variety of operations on the entities.

Let’s say we have a class LogicController which is the only single endpoint for all our operations on Users and Readers. Now if we take all the CRUD operations (Create, Retrieve, Update and Delete) on both Reader and User entities into account, then we have a fat controller class with eight endpoints.

This makes the controller not only hard to understand for a newbie but also difficult to maintain, talking about extensibility is a waste already.

Such a class is called as a God object and Single Responsibility principle talks about avoiding such heavy lifting God objects by assigning a single responsibility for a single class to stick to: the LogicController must hold only either User actions or Reader actions but not both.

This helps in the overall maintainability of the classes at lower level and enhance readability at the higher level.


    public class LogicController : ControllerBase
    {
        public LogicController()
        {
        }

        [HttpGet]
        [Route("allreaders")]
        public List<Reader> GetReaders()
        {
	    var store = new ReaderStore();
            var readers = store.Readers.ToList();
            return readers;
        }

        [HttpGet]
        [Route("allusers")]
        public List<User> GetUsers()
        {
	    var store = new ReaderStore();
            var readers = store.Users.ToList();
            return readers;
        }
    }

Cohesion vs Coupling

To understand the second statement of the principle, “only one reason to change”, we have two indicators which can explain how good is the component to be flexible for change – Cohesion and Coupling.

In any given component, the Cohesion describes how closely the class elements are related to each other. The Coupling on the other hand explains closely two classes or components are related to each other. Let’s take the example of the LogicController class we discussed previously.

We have all the functionalities and respective dependencies related to Readers and Users together in a single encapsulation, and we can understand clearly that all the things related to Users entity are unnecessary for Readers entity and vice versa. Hence in this case there is a low cohesion among the members of the component LogicController.

On the other hand we have an instance of ReaderStore class being created inside each method of LogicController which creates a sort of link between the components LogicController and ReaderStore, making things tougher to maintain and to extend. Such instantiations and dependencies indicate a higher degree of Coupling across the components.

An Ideal component should have a higher degree of Cohesion and a lower degree of Coupling

Poor responsibility means difficult to test

Another factor that is impacted by a poor sense of Responsibility is the testability of the component. A component which handles multiple responsibilities and has high degree degree of coupling is less easily testable, since we would end up writing more logic for test cases and their execution.

Such a component which is hard to test is prone to more and more issues when moved to real-world request loads.

And hence a component when assigned to only a single responsibility for itself can have all the dependencies and functionality related to only that responsibility and this results in a higher degree of cohesion. And since we utilize only those dependencies which the component requires, we end up in components which are lighter in weight and are of lower degree of coupling.

This also results in effective test implementations which can result in fewer issues. As a whole, having a Single Responsibility for components can impact a lot on the quality of the components developed.


    [Route("api/[controller]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        ReaderStore store;

        public UserController()
        {
	    this.store = new ReaderStore();
        }

        [HttpGet]
        [Route("allreaders")]
        public List<User> GetReaders()
        {
            return store.GetUsers();
        }
    }

By applying the SRP on the LogicController, we can break the class into two different classes each for Reader and User; say ReaderController and UserController. And we design the dependencies such that the components are loosely coupled with each other and the result can be as above.

Same can be applied to ReaderController which holds a single reason for existence: the Reader entity. In this way, we can follow the above steps and have our components comply to SRP.

Recommended Reading

Open/Closed Principle


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *