#python #solid

Single Responsibility Principle


Before we dive into the Single Responsibility Principle (SRP), let's say a few words about SOLID.
SOLID is an acronym for five popular software engineering design principles. These principles address software engineering design choices in general and are not tailored for a specific programming language, though having some basic OOP capabilities is a requirement I'd say.

SRP is one of the principles (the S in SOLID). While all the principles are important and should be followed, this article will focus just on SRP, since it's easy to follow and I think it provides the biggest return of investment out of the five.

SRP states that a function or class should implement a single responsibility, and should therefore have a single reason to change. But what does that mean exactly ? In simpler terms, an entity (function or class) should have a limited core scope and not try to cram extra functionality into it. Think micro-services, but instead of decomposing the monolith app, you'd be splitting up classes and functions. SRP isn't a recent trend though; Robert Martin (aka Uncle Bob) coined the term in the early 2000s, but the underlying concept goes further back.

 

An example of SRP

Let's take an example with a function, because it's easier to get started with compared to a class. We have the following function:


This is a function in Python which takes as argument an iterable (a sequence of values), and returns a boolean depending on whether the passed iterable has duplicate values or not. It achieves that by comparing the length of the original iterable, with the length of the set version of that iterable. In case it's not clear, casting to set removes duplicates, so the lengths would differ if there are duplicates.

It's a simple, clean-looking, one-liner function that does its job well. You could argue that if we span the body a line or two more, we could further improve the readability, but that's beside the point. So far we haven't clocked anything remarkable about it. But it's that simplicity what makes this function abide to SRP.

The has_duplicates() function does one thing and one thing only, and it's exactly what the name suggests: checking for duplicates. That's what the single responsibility principle preaches: sticking to the proclaimed responsibility and not delving into other areas of functionality. This function does not remove the duplicates. It does not write to the database. It does not acquire a lock or modify a global. It does not explicitly raise an exception. It simply checks for duplicates.

It must be said that practicing SRP goes hand in hand with naming the entities properly. That is how we deduce the responsibility of the entity in the first place; by reading its name!


Earlier we mentioned that SRP also implies that the entity must have a single reason to change. What would be a reason for has_duplicates() to change ? Only if the current implementation for finding duplicates does not handle some future case, or we come up with a better, perhaps more efficient way of doing so. The only reason that this function might have to change is because we want to find duplicates differently. In other words, we want to perform its (single) responsibility differently. In this case, it's highly unlikely that the function will ever need a change, and that's a great thing.

 

Benefits of SRP

When code changes often, it breeds bugs, especially if there is no test coverage. But when following SRP, entities have separate responsibilities, hence they only change when the specific action they must perform needs to change. And even when they do change, that change doesn't affect other entities! SRP forces the developer to separate concerns.
Frequent changes don't just introduce bugs, but they're also bad news for backwards compatibility, among other things. This is definitely something to keep in mind if your product is a library on which other software builds upon, or if you provide an API to clients. See the Open-Closed principle for more on that.
But other than avoiding frequent changes, what other benefits does following SRP yield ? Perhaps an obvious one is code readability. When your entities are small and modular, and are named properly, the code itself expresses intent. It's not difficult for another developer to quickly grasp what the code at hand is supposed to do, or even how it accomplishes that.
Another important benefit is that SRP code is much easier to test. I'm referring to unit testing. If the code itself is written in small functional units, writing the corresponding unit tests will feel more intuitive and less daunting.
SRP code is easier to debug too. Have you ever been debugging a large function line by line and at some point you lost the context ? Modular code on the other hand allows for black-box debugging. That's because SRP code has no side affects; if the function name says has_duplicates(), you can fairly assume that's all it does! You don't have to step into it; you simply verify the input and output, hence treat it as a black box.

 

How to write SRP code

What are some markers of SRP code ? Body size is a really good metric. If a function is over 100 lines of code long, it's probably doing more than one thing. But perhaps an even better indicator is the number of parameters. As you might've picked up by now, the fewer parameters, the better. A function should, if possible, not have any parameters. I say if possible because we shouldn't try to abide to SRP by violating other good practices. So yeah, making everything a global is still a bad idea.
One parameter isn't a concern, two is fine but not desirable, and three only for special cases. Anything over that is an  immediate red flag that this function does more than one thing. This is especially true if any of the parameters has a boolean nature (even when it's not a boolean value per se but it's used in a boolean check). A boolean check means your function does two things: one if the check is true, and another if it's false. I don't count safeguarding as a violation of SRP, though some might disagree with that. I'm fine with checking whether the length of an iterable is bigger than zero, but not with doing one thing if that length is between 100 and 1000, and another if it's over 1000, all of that within the same function. 


But what is "one thing" ? How do you define it? That's probably the hardest part about practicing SRP. What is one thing to you, can be two things to another developer. It's not something we can have an objective consensus about. Let's say you have to write a piece of code for building a report on sales statistics for a commerce site. Do you simply write a function called build_statistics_report() and put everything in it? 
To me it seems like this operation can be broken down into smaller functional chunks, such as gathering the raw data, performing some calculations for the statistics, formatting the result, saving it in the file system or printing it, etc. Each of these can be a standalone function. In fact, the function for gathering the data can probably be split into "sub functions" of its own.
So...how deep do you go ? I tend to lean towards the thinner side. Make the entities as thin as possible without them being just syntactic sugar over the language or framework's base syntax. That's where we can reasonably draw the line. Build entities that add at least some new behavior or group together existing one that is already provided by the environment you're writing code in. And, as with most principles in programming, apply it alongside some common sense. It's possible to overdo SRP!


What is the nemesis of SRP ? If SRP is the pattern, what is the anti-pattern ? We've all seen it in the wild. We've all suffered due to it.

It's the infamous mega-function that in fact is not a function but a whole feature! You know what I'm talking about. It's that function with hundreds of lines of code, with over half a dozen parameters, with multiple nested loops and conditions, with many side effects and leaks, in which you get lost trying to understand it. It's the class with hundreds of methods, that may as well be a standalone library!

With the anti-pattern in mind, it's not hard to imagine how practicing SRP can improve the quality of code we write.

 

Further readings: Agile Software Development: Principles, Patterns, and Practices (Robert Martin), Clean Code (Robert Martin), Refactoring (Kent Beck & Marting Fowler).