DEV Community

Theofanis Despoudis
Theofanis Despoudis

Posted on

Understanding SOLID Principles: Single Responsibility

Single Responsibility

This is the 2nd part of the series of understanding SOLID Principles where we explore what is Single Responsibility and why it helps with readability, lose coupling and cohesion of your code.

As a small reminder, in SOLID there are five basic principles which help to create good (or solid) software architecture. SOLID is an acronym where:-

S stands for SRP (Single responsibility principle)
O stands for OCP (Open closed principle)
L stands for LSP (Liskov substitution principle)
I stand for ISP ( Interface segregation principle)
D stands for DIP ( Dependency inversion principle)

We’ve discussed Dependency Injection before and why it helps deliver software that is loosely coupled and testable.

Now we are going to discuss Single Responsibility.

Single Responsibility

Every module or class should have responsibility for a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

Or in simple terms:

A class or module should have one, and only one, reason to be changed.

Ok just by reading it, it makes a little bit of effort to understand why a class or a module is easier to change if it does one thing only. In order to understand the whole principle lets focus on each part and analyze it, starting with what is Single, then what is Responsibility and then what is Change.

What is Single

Only one; not one of several.
Synonyms: one, one only, sole, lone, solitary, isolated, by itself;

Single denotes some work in isolation. If your method, class component does one thing it does not do two things. Let's see an example:

class UserComponent { 
  getUserInfo(id) {
    this.api.getUserInfo(id).then(saveToState) // save user info to state
  }
  render() {
    const { userInfo } = this.state;
    return <div>
      <ul>
        <li>Name: { userInfo.name }</li>
        <li>Surname: { userInfo.surname }</li>
        <li>Email: { userInfo.email }</li>
      </ul>
    </div>
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, you might be thinking, hey I do that all the time in my React components!

Well, to tell you the truth it might work for one very small project but it won’t work for a bigger one as your component is doing more than it already does. If you happen to change the API service to perform additional logic you would have to modify 2 places or more now. One in your API service and one in your component. That could easily escalate to other places.

A better way is to just simply provide the user info data for the component using props and let the component do 1 thing only just render the data.

There are various ways you do to identify what is single and what is not. In general, you need to recognize when your code tends to know a little bit more than already does.

A God Object aka an Object that knows everything and does everything.

Let's see what is Responsibility next.

What is Responsibility

Responsibility is the work or action that each part of your system, the methods, the classes, the packages, the modules are assigned to do.

Too much responsibility leads to coupling.

One thing to understand about coupling is the level of awareness or details a part of the system knows about another part of the system.

If client code needs to know class B in order to use class A, then A and B are said to be coupled.

This is bad as this complicates change and makes things worse to alter in the long run.

Aim for the right amount of coupling that maintains a good level of Cohesion or the measurement of the each component intended tasks or how focused are the components to the task.

Components with low cohesion are doing tasks that are not related to their responsibilities. For example, let's say we have a User Class that we keep some info in there.

class User {
  public age;  
  public name;
  public slug;
  public email;
}
Enter fullscreen mode Exit fullscreen mode

It does make sense to keep only methods that set or get the role, name, age properties.

However, if we decided to add some other methods:

class User {
  public age;  
  public name;
  public slug;
  public email;
  // Xmm why do we have them here?
  checkAge();
  validateEmail();
  slugifyName();
}
Enter fullscreen mode Exit fullscreen mode

Those checkAge, validateEmail, slugifyName methods look strange for sure.

That would actually make the class less cohesive as it would assign those methods that make no sense to have in a User class. It could be extracted in a different class for example UserFieldValidation.

Ok now let's see what exactly is change.

What is Change

Change

A change is an alteration or a modification of the existing code.

Who or what are the sources of change?

Studies of historical data from legacy software systems have identified three specific causes of this change: adding new features; correcting faults; and restructuring code to accommodate future changes.

I know what you are thinking. We all been there, didn’t we? Let's say you have finished a component you are very proud of, as at the moment it is super fast and super readable. Probably the best piece of software in your current career. Let's name it SuperDuper component.

class SuperDuper {
  makeThingsFastAndEasy() {
    // Super readable and efficient code
  }
}
Enter fullscreen mode Exit fullscreen mode

Then at some point, your manager asks you to add a new feature to call a function from another class so that it will do more things. You decide to pass this in the constructor and call it in your method.

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code
    this.notDuper.invokeSomeMethod()
  }
}
Enter fullscreen mode Exit fullscreen mode

This is an example of adding a new feature. You provide a new feature by adding some extra lines of code.

Ok, you made that change and you run the test suit. Suddenly you find out that you broke 100 test cases. It is because you need to add an extra check before you call this notDuper method.

class SuperDuper {
  constructor(notDuper: NotSoDuper) {
    this.notDuper = notDuper
  }
  makeThingsFastAndEasy() {
     // Super readable and efficient code

    if (someCondition) {
      this.notDuper.invokeSomeMethod()
    } else {
      this.callInternalMethod()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is an example of correcting faults. You provide a fix for this change you made by fixing regression tests or handling edge cases.

Ok at some point you say that you don’t like this approach and you want to refactor the whole thing. You implement a different way of calling the notDuper method without the ifs using a signal dispatcher.

class SuperDuper {

  makeThingsFastAndEasy() {
     // Super readable and efficient code
     ...
     dispatcher.send(actionForTheNotDuper(payload)) // Send a signal
  }
}
Enter fullscreen mode Exit fullscreen mode

This is an example of restructuring code to accommodate future changes. You restructure your code in order to be more readable while preserving the existing functionality.

As you can see from the example the original method is not the same anymore as it has changed in order to accommodate new features or bugs. It's up to how you structure your code to make it easy to change that helps.

Ok, How can I make my code adhere to this rule?

KISS

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Isolate change by looking closely at the things that make the whole and separate them logically. You need to be aware of your code and why does is written that way. Always look for a too big method or function.

Big is bad, small is good…

Keep track of the dependencies by checking for example if a constructor has too many input parameters thus many dependencies. If it's possible always inject them via DI.

Use Dependency Injection

Keep track of method parameters as it denotes that the method may be required on many things in order to function.

Use simple naming as it will help you refactor the code for single responsibility. Long function names imply that there is something fishy there.

Name things descriptively

Refactor early and as often as you see that something can be simplified. This will help you with tidying up you code on the go.

Refactor to Design Patterns

Finally, introduce change where it matters, and not where it will make things more coupled and less cohesive so that your code always stays on top of those principles.

Introduce change where it matters. Keep things simple but not simpler.

Recap

I hope I’ve helped you understand what Single Responsibility stands for and how you can make your code even more SOLID. Stay put for the next article.

References

Coming up next is Understanding SOLID Principles: Open Closed Principle

If this post was helpful please share it and stay tuned on my other articles. You can follow me on GitHub and LinkedIn. If you have any ideas and improvements feel free to share them with me.

Happy coding.

If you would like to schedule a mentoring session visit my Codementor Profile.

Read the original Article here

Top comments (0)