Expanding upon the Single Responsibility Principle (Part 2)
The Single Responsibility Principle, sometimes referred to as SRP, is the first of the five SOLID principles. In our last post we…
The Single Responsibility Principle, sometimes referred to as SRP, is the first of the five SOLID principles. In our last post we introduced the idea of a module, and how having only a single responsibility for each module makes the modules, and thereby our system, more robust. We touched upon the idea that single responsibility transcends modular levels. We will be going over this concept in detail, and then round up our understanding of SRP in this post.
Just to summarize what we covered in the previous post, modules are units of code that are reusable, compose-able, and obey a contract. In C# this can be related to classes or functions. And, modules with single responsibilities are sometimes made up of other modules. In that case, we would want those sub-modules to also have a single responsibility.
This means that single responsibility extends across levels. In the same way that a sub-module should also have a single responsibility, a super-module should also have a single responsibility. Extending this further and further makes the entire system more robust — the entire software system has only a single responsibility, as also the smallest module in the system.
Let’s delve into these ideas in detail.
Refactoring an entire system to follow Single Responsibility Principle
Starting from any single module, we can refactor our entire system to follow SRP.
After a bit of refactoring to follow SRP, we had a few functions we considered as modules, each with their own single responsibility. Let’s take a look at that code again.
Now that we’ve done a reasonable job of refactoring the methods, let’s go over the current state so that we understand what we mean when we say sub-modules and super-modules.
Taking the ValidateAndCreate method, it is a module with a single responsibility. It has two sub-modules. A sub-module is a module that works as a part of another module. It is easy to extend the idea to a super-module. A super-module is actually the inverse of a sub-module. It is a module which encompasses another module. Taking the example here, StudentRepository is the super-module of the ValidateAndCreate module.
The easiest way to refactor to follow the Single Responsibility Principle is from bottom-to-top, targeting the smallest modules first, like we attempted here.
Here’s a sample algorithm that works with anything we might consider to be a low-level module:
- First, we take a look at the module candidate, and verify that it is a module. If it is not, we move on to look at other module candidates.
- We examine each of its sub-modules recursively. If there are no further sub-modules, we catalog its responsibilities. If the number of responsibilities is 1, we leave it as it is — it already follows the Single Responsibility Principle. We examine its super-module.
- We split the responsibilities and refactor the module into separate sub-modules. We examine each of the sub-modules recursively, with these same 3 steps.
Taking the ValidateAndCreate method as the module, let’s see what we’ve done so far:
- We verified it was a module (initially it was named Create).
- We examined and found that it had no sub-modules.
- We then cataloged its responsibilities, and found it had 2 — one for validation and one for creation.
- We refactored it into 2 separate sub-modules Validate and Create.
- We then verified that each of them were modules, cataloged their responsibilities, and found that they each had a single responsibility.
- Then we re-examined the ValidateAndCreate module, to see it had a single responsibility, and its sub-modules also followed SRP.
So now, we’re in step 2 of the algorithm above. And it’s time to examine its super-module, the StudentRepository class.
Refactoring super-modules to follow Single Responsibility Principle
Revisiting our repository class for a better understanding.
Now that we have to look at the StudentRepository class, let’s do that exactly:
A repository in computing is usually a central location in which data is stored and managed. So by extension, our StudentRepository in this example, must be a place in which data relating to students is stored and managed. We usually say a repository abstracts away the storage mechanism by exposing CRUD (Create, Read, Update, and Delete) operations. Can we perhaps consider data storage and/or management as the single responsibility of such a module? Absolutely.
But we find that it has an additional responsibility of validating a student entity via the ValidateAndCreate method (sub-module) inside it. Uh-oh, time to refactor for single responsibility. But how do we go about it? Here’s a simple rule:
If a sub-module is unnecessarily overloading the responsibilities of a module, a super- module can act as an orchestrator to bind those responsibilities together.
Does that help? If not, let’s go over that again.
In our case, the ValidateAndCreate method (and by extension, the Validate method as well) is a sub-module that is overloading the responsibility of the StudentRepository module. How do we fix this? By moving the responsibility of the ValidateAndCreate module to an outside, independent module. In our case, the Validate module is a method, and cannot be moved outside as such. So what do we do? We wrap this business logic (of having to validate a student before creation) in another class meant to handle business logic. For the sake of convenience, let’s call this class a StudentsService.
Refactoring with what we have so far, we get:
Going over the changes,
- We have marked the StudentRepository class internal (so that it is not exposed to the outside world), to illustrate it cannot be consumed as a separate module by a consumer other than via a (super-)module. But this is a minor detail.
- We have made the Create method method public in the repository (it used to be private). This is to start exposing the functionality of this sub-module via the StudentRepository module.
- We have moved the Validate method (sub-module) into another (super-)module called StudentsService.
- We have moved the ValidateAndCreate method (sub-module) into another (super-)module called StudentsService, and renamed it to Create. This method (module) makes use of a sub-module (of a sub-module) in order to complete its task.
Now, the question we need to ask is, is the refactoring complete? The only question which can answer that is actually another question — do all modules in the system now follow SRP? We just made sure that StudentRepository follows SRP after this bit of refactoring. But are all modules following SRP?
Let’s take a look at the new module, the StudentsService — its single responsibility was supposed to be to handle business logic. While the Create method manifests this, the Validate method (sub-module) doesn’t. So the StudentsService actually has 2 responsibilities — to validate a student entity, and, to handle business logic. Ooh, this is tricky, and I like it already.
Are these really 2 responsibilities? Validation as part of Create is a business logic, yes. But is the validation the responsibility of the StudentsService? I would argue, absolutely not.
While the StudentsService can be responsible for validation to occur before a create operation — or an edit, for that matter — it’s not responsible for doing the validation itself. Having the validation performed before the create operation — or the edit, again — is the business logic, and this responsibility is distinct from having to possess the validation logic and to actually do it.
I understand that’s a lot to take in, but think about it — I’ll give you some time to process that. Take your time, this piece of text is going nowhere. Re-read that last paragraph if needed.
If you’re clear, let’s move on, and you can skip this paragraph. If not, bear with me, and I will try to explain with a simpler example. Let’s say I want you to spell-check a document, proof-read it, and then email it to me. I’m basically giving you 4 tasks — (1) spell-check the document, (2) proof-read the document, (3) email the document, (4) make sure the 3 tasks are done in that order. In essence, your only responsibility (if you had to pick only one, and still complete what I wanted from you) is 4: making sure the 3 tasks are done in that order. Delegating the 3 tasks to the best people in those areas is your best bet to get that one task done optimally. Right?
So, let’s refactor yet again, this time the validation part:
Now, we have all modules (in consideration) following single responsibility principle, and each module can evolve independently in order to become better at its own responsibility, increasing the robustness of the system as a whole.
Well, that’s about as much I could cram into a 2-part post expanding upon the Single Responsibility Principle in a short time while trying to make things as clear as possible. In the next post in the Do you write SOLID C# Code? series, we will be expanding upon the Open/Closed Principle, the O in SOLID.
Please leave your suggestions, comments, and any feedback on the comments section of the page. Also, if you liked the article, please do click the little heart icon to recommend the article on medium, and also share it in your circles or via social media, by whatever way or form you wish — you never know who it might help.
Cheers!