Welcome to the five-part series of blog posts about SOLID Principles. In each part I will describe and analyze one of these principles. In the last part, expect summary of the entire series containing a few tips and thoughts.
Let’s start. What are SOLID Principles? There are five general design principles of object-oriented programming intended to make software more understandable, extendable, maintainable and testable.
Today, more about first principle - Single Responsibility Principle.
Single Responsibility Principle
“Class (object) should have one and only one responsibility (and this responsibility should be fully encapsulated)”.
“A class should have only one reason to change”.
To simplify it even more, we can say that a class should do only one thing.
But how to define responsibility of a class? How to define this “reason to change”? Answer to this question is crucial and not so easy.
Consider the following sentence: “There should be no more than one reason to modify (rewrite) a class” (modifying class code in a file, not object memory state). We will try to determine this reason in this article.
Example: Suppose that we want to create an user. The first thing we need to do is to validate data. Let’s make it in service object. Such a class can see similar to this:
What does this class do? It validates user input and then processes it (probably saving user in the database). Validating and processing? It’s sound like two separate actions. Like two different responsibilities. Before we proceed to fixing (or improving) this solution, we must clarify why such solution is not good.
First of all, these two processes are highly coupled. There is no easy way to reuse one of them separately. Maintaining such class will be harder and leave more room for potential mistakes. Testing will be also harder (more cases to cover).
So maybe it’s good idea to move the validation-related code to a separate class? Let’s do this!
Now, we have got two classes, each with a single responsibility. The first class processes the data, the second one validates it. Classes are no longer highly coupled. If we have to change the validation rules, we will be modifying only UserValidation class. Similarly if we have to change actions done after validation, we won’t have to touch UserCreateService class.
We have also gained reusability each of these classes. We can use UserValidation in other context and use UserCreateService with another validation class (we used Depedency Injection right here).
Are there any other advantages? Sure! We always write the tests for our code, don’t we? We will write tests for validating and processing services separately (in isolation). Then we will mock UserValidator response in the tests for UserCreateService.
You probably heard about Domain Driven Development (DDD). This concept is related to other terms such as information expert or rich domain model (as opposite for anemic domain models)
Let’s stop at rich domain models. This concept assumes that we keep all behaviour strictly connected to particular model in that model. So we keep data & logic that operates on it in the same place. As we are talking about OOP it seems to be obvious.
Please look on the following example.
Ok, we have validations, logic for notifying user, logic for soft deleting right there. A lot of responsibility at once? So the question is: SRP is violated or not?
My answer is: No, it isn’t.
Why? Domain model is still responsible only for its own data & integrity.
For me, it’s all about context.
You will probably say: “Hello! You just said that two actions in single class is really bad”. Still, it’s all about context.
Purpose of service object is to perform single (atomic) operation. Nothing more.
As you have probably noticed, understanding this principle is not so simple. Mainly because defining “reason for change” can cause difficulties and there is also room for own opinions and interpretations. Implementation itself is not so difficult.
So my advice is as follows (and this applies to all rules): do not try to fulfil X rule. Strive to obtain benefits that should result from its use. That’s why we use it. Am I right?
Finally, applying this principle correctly should lead to keep our code clean, easier to maintain and thus easier to understand and analyse. Writing tests for such code is also easier. Such tests are cleaner and more maintainable too.