Expanding upon the Single Responsibility Principle (Part 1)
Almost every techie I come across who has worked with writing object oriented code, seems to be proud that they write SOLID code. Not…
Almost every techie I come across who has worked with writing object oriented code, seems to be proud that they write SOLID code. Not solid, in the sense that the code is stable, but rather SOLID, as in applying the five basic principles. The other day I asked a person how a piece of code followed the Liskov Substitution Principle (the L in SOLID), and they couldn’t really reason it out. And they had just boasted about having written SOLID code all long. And I just thought, maybe, just maybe, I could do something to clear some of this stuff up. So, let’s do it!
SOLID seems to be a good thing to write about. Several articles on SOLID can be found, and Wikipedia itself is a good source to learn more about SOLID. However, I thought I should take a shot at it. And since I’ve been writing in C# for a large part of my professional career, going through it in C# should be fun.
What is SOLID?
A set of five basic principles in object oriented programming and design
SOLID is a mnemonic acronym standing for the following five basic principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
These principles, when applied together, are supposed to help us create a system that is easy to maintain and extend over time. It is to be noted that these are mere guidelines that can be followed irrespective of the language or framework of our choice.
We will be going over the five principles, but for today, let’s look at the first, called the Single Responsibility Principle.
What is the Single Responsibility Principle?
A module should have only one reason to change.
Single Responsibility Principle (SRP), states that a module should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by it. All its services should be narrowly aligned with that responsibility.
Put simply, we can say, that a module should have a very narrow piece of responsibility in the entire system. Or, as some people say, a module should have no more than one reason to change. Hmm? Narrow responsibility, reason to change. All of these are pretty fuzzy. How do we know if something is reason enough to change? Or if a responsibility is narrow enough? And what the hell is a module? Also, the bigger questions. Why? How does it help me, really?
Let’s tackle those things.
Why single responsibility?
It is a mere consequence of understanding how to build maintainable software.
If a module has only a single responsibility, it has the opportunity to be very robust. It is easier to verify if it is working as it should, and easier to reason about the changes in the module, if it has a single responsibility.
I’d like to provide the analogy of the assembly line here. According to Wikipedia, an assembly line is a manufacturing process in which parts are added as the semi-finished assembly moves from workstation to workstation where the parts are added in sequence until the final assembly is produced.
Think about it: A big process, broken down into small steps. A small step happens, then the result is moved to another small step. The small step happens, then the result is moved to yet another small step. This is the assembly line.
And this resulted in huge cost cutting and higher specialization. Instead of a worker having to know all the steps, they could do one step, and only step only. It was easier for them to obtain mastery over the single small step, and they could do it progressively better.
Of course, it had its drawbacks. Doing the same thing over and over again introduced stress on the same joints. And the workers never felt inspired, their work was mundane, repetitive, and uninspiring.
Let’s apply this to our software system. Let’s make each worker of the assembly line a module, and each assembly line a software system. The software system is a big entity, which can be broken into a lot of small steps, or, responsibilities. When the responsibilities are small, the module can be made progressively better at it, that is, the module can be made more robust. It is easier to test, validate, improve and optimize. That’s why.
But the disadvantages? Sure, they come too. The module will never be inspired, and the same joints will feel the stress. But the module isn’t a living person to feel the stress, or the lack of inspiration! We want our module to do its job, not become Leonardo da Vinci! (unless, of course, its job is to become da Vinci)
So, that’s why the single responsibility principle (SRP).
What is a module in SRP?
First we must understand what SRP talks about, only then can we apply it.
Right, right. So now we know why we must use SRP, and also how it helps us. But it talks about modules, and what are they? The answer is simple. Let’s take help from a dictionary:
module (n.) — each of a set of standardized parts or independent units that can be used to construct a more complex structure, such as an item of furniture or a building.
Put simply, a module is a standardized, independent unit that can be used to construct anything complex. Any unit of code that is reusable, compose-able, and obeys a contract in software can be treated as a module. Since SOLID is usually applied to object-oriented programming, and we’re primarily interested in C#, that unit of code would most likely be a class. In functional programming, I’m pretty sure that a module would be a function. But, we can also make a module in C# correspond to a function — after all, all we need is reusable, compose-able unit of code that obeys a contract, and a function is reusable, compose-able, and its signature is indeed a contract.
What is reason to change?
Or, what is narrow responsibility?
Now that we have the definition of a module cleared out, let’s see what a reason to change might refer to. Or, how narrow a responsibility is considered a narrow responsibility. The interpretations are pretty subjective, and like most things in software, the answer is usually, it depends.
But it’s easier to think of it in this way. If describing what a module does can be broken down, it should be. That’s pretty abstract, so let me try better. Well, it’s actually getting difficult to explain without code, so let’s dive in to some code.
How to refactor to obey SRP?
Let’s go over some code, because words are no longer enough.
Let me take some fairly common code:
This is some code in some student repository. We don’t know the rest of the code. Considering the Create method as a module, let’s try to describe in words what the Create method is doing:
int StudentRepository.Create(Student student) checks to see if the student’s name is blank, and throws an error if so. If not, it saves the student entity to the database and returns the saved entity’s primary key.
Notice how the description can be broken down into parts? Let’s try to point out the parts:
- checks to see if the student’s name is blank, and throws an error if so
- saves the student entity to the database and returns the saved entity’s primary key
Let’s try to refactor it. We obviously need 2 methods, so let’s work them out. Naming them should be fairly simple:
Now we have two methods, each with their own responsibility. The ThrowIfStudentNameIsBlank method does exactly that, and the Create method does exactly that.
If you’re wondering that the Create method adds, then saves, then returns the id, that’s not multiple responsibilities, a function is allowed to perform an action and return a value. In effect, the Create method here is saving to the db and returning the primary key. That’s a perfectly valid single responsibility. There is no way to reduce its responsibility further, because we simply cannot decompose it.
For fun, let’s try doing that exactly, to understand why and when it shouldn’t be done, at the very least. So, what does the Create method do now?
int StudentRepository.Create(Student student) adds a student entity to a table, saves the changes to the database and returns the saved entity’s primary key.
Notice how we’ve expanded what the create method does? So that we can quickly split it into its parts:
Now that we have separate methods with single responsibilities, what does Create do?
int StudentRepository.Create(Student student) runs a series of methods that in turn adds a student entity to a table, saves the changes to the database and returns the saved entity’s primary key.
Now the only step in the Create method is that it runs a series of methods in order to do its job — which is saving a student entity to a database table. Let’s go over the methods, to see how much sense they make:
The first method, AddStudent(Student student)
adds a student to the database table, but does not save the changes. Do we have a use case for calling this method in a stand alone fashion? Do we have any business logic that makes this reusable and independent? If so, it is a good piece of refactoring. But, it doesn’t seem so here. It is not reusable. That makes it a poor candidate for a module, only then can we apply SRP to it.
The same goes for the second method SaveChangesToDb()
. Sure, the save changes method can be reused, but is it independent? No, it’s heavily dependent on the actions that precede it. That makes it a poor candidate for a module, only then can we apply SRP to it.
The last method returns a student’s primary key when a student is provided. But student.Id
is a property, which means it itself is composed of a getter and setter (methods). So the GetStudentId(Student student)
is both resusable and independent, but it is also redundant — there is already a method that does the same thing. So, it makes no sense to have this method at all. Let’s remove the unnecessary addition.
Now that we have established that the three methods are not modules (in our particular use case), and that we cannot apply SRP to it, let’s look at it from another angle. The refactored Create method which called these 3 methods, basically delegated the actions to other methods. If we take another look at the code, didn’t our previous code already do that? Good, you’re noticing.
So the takeaway is, before applying SRP, we need to make sure we’re dealing with a module. And the only way to make sure if we’re dealing with a module, is to understand what constitutes a module, with respect to our requirements. Here the requirement is to persist student information, and hence the Create method is a module. It has the sole responsibility of persisting the new student information into a database.
Let’s revisit our latest valid code:
ThrowIfStudentNameIsBlank was a rather long name, and we just reduced its length meaningfully just for the purposes of brevity in this post. In all honesty, I prefer the more descriptive name, I don’t have to navigate to the definition in order to know what it does.
But now, we’ve lost a rather important piece of functionality. Validating before creating a student entry in the database. We now have 2 functions with single responsibilities, no doubt — one validates a student, and another saves the student to the database — but how do we get these to work together? Well, let’s create a screw, an orchestrator, or whatever you want to call it.
We’ve introduced a new method, called ValidateAndCreate which does exactly that, to a student. It merely delegates its responsibilities to lesser modules each of which have single responsibilities. In a sense, its only responsibility is to run those two methods in succession and return the result. It follows SRP.
We have also made the Validate method static, because it doesn’t depend on any private variables. It was also made private, because, for the requirement, it need not be exposed separately. Keep in mind that it is still a module, though private. Validate itself follows SRP as well.
We have made the Create method private, and it is still a module, although it isn’t exposed (because our use case demands that validation and creation be done in series, and that is the only way to create — we cannot expose Create as a stand-alone piece of functionality without the Validate). The Create method is still a module that follows SRP.
Well, that’s some pretty good refactoring. But, as we’ve seen, SRP doesn’t stop at one level, ValidateAndCreate is a module that itself has sub-modules which follow SRP. It only makes sense that its super-modules also follow SRP. We will be diving into that area in the Part 2 of Expanding upon the Single Responsibility Principle in the Do you write SOLID C# code? series.
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 whatever way or form you wish.
Cheers!
PS: This article is a day late, but I haven’t broken my New Year Resolution ;)