I believe that every developer should be familiar with the SOLID principles. The SOLID principles are comprised of five principles that all aim for the same purpose: writing understandable, readable, maintainable, and testable code, especially in the OO style that many developers can collaboratively work on.
SOLID is a helpful acronym you can use to remember five essential principles:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Before diving into the first principle, let’s know what is PDD.
PDD is Pain-Driven Development. It means you should write your code as simple as possible to solve a specific problem, not worrying about SOLID. Because applying the SOLID upfront is premature optimization.
Instead, look for places in your code where the app is painful to work with as your app grows. This pain may be due to coupling, many duplications, or difficulty in testing.
When you get there, see if any SOLID principles you can apply to improve your design and mitigate this pain.
In this article, let’s begin with the SOLID first principle, the Single Responsibility Principle (SRP).
Robert C. Martin (Uncle Bob) defines the Single Responsibility Principle as:
“Each software module should have one and only one reason to change.”
Uncle Bob
You can think this definition is so easy to implement. Or in contrast, you can think of some questions like what is the module exactly? What is the reason to change? And what does it relate to the responsibilities?
To be honest, this definition was very tricky for me. I was always thinking of these questions. So I am writing this article to share my approach to following this principle.
OK, first of all, you can think of a module as a function, class, component, or even microservice.
To better understand this principle, we must keep another two principles in mind, encapsulation, and delegation. But why?
I think we agree that the better we’re able to separate the what and the how a class does, the better our software design is. This separation could be done using encapsulation and delegation.
A class should encapsulate doing a specific task in a specific way. And when this class is a single-purpose, it does this purpose perfectly, and we can use it easily.
Or a class can delegate a specific task to another instance of a class that encapsulates doing this task in an abstracted way.
On the other hand, when a class is multi-purpose, it often ends up coupling things that shouldn’t be related together and making it harder to use.
As you can see, the Auth
class has a method login
that is responsible for its business logic and sending an SMS to the user after logging in. The Auth
class delegates sending the SMS task to another class SmsProvider
which encapsulates its implementation in a way that the Auth
class doesn’t know how the SmsProvider
does its implementation.
Responsibility is the answer to the question of how something is done.
Responsibility at the code level might be data persistence, logging, validation, third-party integration, or the business logic itself. The distributed system level might be caching, message queuing, proxy, or load balancer.
SRP suggests that every module has one reason to change and has one responsibility. Each of these responsibilities may be changed in the future. For example, data persistence may be changed from files to databases or from one database to another. Validation criteria may be changed. The logging tool can be changed. Business logic is usually changed. The caching type may be changed. Or the message queuing tool may be changed.
Yes, defining the responsibility and reason to change is tricky. As a result, you have to be aware of any potential request for change that might violate the SRP in your code. These requests are the source of the reason to change. They might be requests from your managers or even improvements from your perspective.
The more you know about these requests, the more you can separate your decisions and apply the SRP perfectly.
Unfortunately, following the SRP sounds easier than it is.
Some developers take the SRP to the extreme by creating a class with just one method. And when they write actual code, they have to inject many classes, making the code more complex and unreadable.
You shouldn’t oversimplify your code. You have to use the SRP in common sense. There is no benefit if you create every class with only one method. In that case, why do classes exist, and why not go back to procedural programming?
You must define the balance point between your code’s over-simplicity and over-complexity. That point can be defined by:
- Ask yourself, “What is the responsibility of this module?”. If your answer has the word “and,” you likely violate the SRP.
- Ask yourself, “What are the potential reasons to change this module?”. If you have many reasons that don’t relate to each other, you likely violate the SRP, and your module is low cohesive and tightly coupled.
The SRP is closely related to the concept of coupling. When a class performs many details that aren’t related, these details are tightly coupled. And the more details a class has the more reasons for a change.
A loosely coupled class is responsible for some higher-level concerns and delegates to other classes responsible for the details of how to perform the lower-level operations for these concerns. This introduces us to another principle, Separation of Concerns.
Separation of Concerns suggests that a program should be separated into sections. Every section has to deal with one concern and should not know how another section does a specific task.
A key benefit of following the Separation of Concerns is that the higher-level code doesn’t have to deal with lower-level code and doesn’t know how the lower-level details are implemented.
Another concept that is closely related to the SRP is Cohesion. Cohesion refers to how strong the relationship between module elements is. The more a module has responsibilities, the lower cohesion between its elements.
Take a look at this example to understand these principles better, Class
has three fields and two methods. method1
uses only field2
and doesn’t use the other fields. method2
uses field1
and field3
doesn’t use field2
.
This diagram depicts that Class
might be tightly coupled, low cohesive, and doesn’t separate its concerns. So let’s try to refactor it with these principles in our mind:
Now, we can say that Class1
and Class2
are highly cohesive, loosely coupled, and concerns are separated perfectly.
Now, we know where and when we should use the SRP, but why should we bother ourselves by applying it?
Many benefits come from applying the SRP:
- We know that requirements change over time. Each change affects the responsibility of at least one class. The more responsibilities your class has the more reasons for changes you have.
- The single-purpose module is much easier to read, explain, and understand. You may remember how frustrated you feel when you have to refactor a big class.
- For sure, separated modules are more flexible and configurable than many responsible modules. If you have a big class and want to add a new feature or make a feature configurable, yes, you can do that in the class itself but only by increasing the class size and complexity.
- Single responsible modules are likely more reusable than many responsible modules. You may note that the methods with only one purpose most likely have no side effects and don’t depend on the class state. Yes, you are right as you thought. It is functional programming. The SRP nudges us toward the functional programming style.
- It’s away easier to test and maintain single-purpose modules.
- Having classes and methods with only one purpose helps to investigate performance issues easily. And more remarkable at the level of the distributed systems, services that only have one purpose are easier to monitor the load and resources bottlenecks and, as a result, scale up or down independently.
- If your class depends on one class or more, any change in this class would affect its dependencies. You might need to update these dependencies or recompile them even though they are not directly affected by your change.
Having said that, you might think: I’m convinced, I will use the SRP every time, and everywhere. Before going that far, keep in mind these points:
- At the level of distributed systems, the more services you build, the low reliability you get. Yes, there are many ways to overcome this point, but you must remember that everything has its cost of time, effort, and money.
- As we know, separating concerns increases the code size and the effort and time you need to write this code. However, in the long term, it decreases the effort and time.
- Indeed, separating concerns in our code hits the overall performance. However, this point could be neglected at the level of code, but at the level of distributed systems, it hits a lot. Many services directly affect the system performance through higher latency and networking issues.
Let’s introduce a simple example that introduces a class with many reasons to change and then try applying the SRP to it.
First of all, let’s make our investigation and ask ourselves:
- What is the responsibility of this class? You might say it only registers a user. Another one might go deeper and say it is responsible for logging, validation, and persistence. OK, if you’re confused about identifying, jump into the next test.
- What are the potential reasons to change this class? I think we agree that we may change the logging mechanism, the validation criteria, or the persistence approach, right? So we have many reasons to change this class. So it might be an alert to refactor your class and apply the SRP.
As a result. Let’s refactor this example with SRP in mind.
As you can see, there is more code in this version than in the previous version. That’s right, but the refactored version is easier to test, maintain, update, and readable. Now, the Auth
class, which is the higher-level code, doesn’t know how the lower-level code is implemented. It only delegates other classes to do specific tasks encapsulating their implementations.
At the end of the day, the Single Responsibility Principle is essential. But be careful when using it. Use it only to eliminate pain by improving your design after writing some working code. Don’t oversimplify your code.
SRP helps you to achieve high cohesion, loose coupling, and separation of concerns.
Finally, keep your modules as small and simple as possible. Give them one responsibility and one reason to change. This eases your testability, maintainability, and readability.
- SOLID Principles for C# Developers
- SOLID Design Principles Explained: The Single Responsibility Principle
- Single Responsibility Principle Unpacked
If you liked this article please rate and share it to spread the word, really, that encourages me a lot to create more content like this.
If you found this article useful, check out these articles as well:
- Open-Closed Principle: The Hard Parts
- Liskov Substitution Principle Isn’t Complex. Just Give It A Try
- 4 Ways to Handle Async Operations in Javascript
- Strategy vs State vs Template Design Patterns
Thanks a lot for staying with me up till this point. I hope you enjoy reading this article.