The Art of Single Responsibility
The single responsibility principle(SRP) is probably the easiest of the SOLID principles to understand but the hardest to implement.
The idea that your classes, modules and methods should do only one thing is well and good. But what is one thing?
Here is a typical conversation about SRP.
Senior Dev: Definition of SRP
Dev: Makes sense, but what counts as one thing?
Senior Dev: Hard to understand example 1, 2 and 3
And yet if you look at the Senior Dev’s code, it is clean and simple, they are following SRP.
In many ways SRP is more of an art than a science. But even art has some rules.
At its heart SRP is all about abstraction.
Abstraction
Abstraction is taking multiple concept and grouping them together into a new concept.
It is like taking some Legos and building an animal. By putting basic blocks together in a certain configuration you now have a giraffe!
Now you could build a few more animals and put them in a zoo. You wouldn’t be putting Lego blocks in your zoo you would be putting Lego animals in it. This is an important distinction, once you create an abstraction it frees you from needed to focus on lower level concepts.
Unless it is a petting zoo you don’t want your animals just wondering about. Your animals should be in an enclosure. This also allows us to create another abstraction layer: animal enclosure.
Some abstractions are closer to the bottom of the abstraction tree than others, this is referred to as abstraction level or depth.
Zoo > Animal Enclosures > Animals > Lego bricks
Lego bricks is a low level concept whereas Lego Zoo is a high level concept.
In building applications or services the application or service is a high level concept and connecting to a socket for instance is a low level concept.
So what does abstraction have to do with SRP. This circles back to the question: what does it mean to do one thing?
One thing?
Isn’t the entry point of an Application doing all the things? Well yes and no.
Yes in that it is executing all of your code but no in that you shouldn’t put all your code directly in the entry method.
This is where abstraction comes in. Each method or class should only span a single abstraction layer.
If we take the zoo example: The buildZoo method shouldn’t do anything with Lego bricks or animals it should deal with animal enclosures only.
SRP dictates that each method should only handle a single abstraction concept. It is important that not only should methods only span one abstraction layer they should only handle the responsibility of their top level abstraction, in other words a method shouldn’t interact with its siblings or cousins. It should just interact with its parent and children.
Let’s look at an example. Zoo’s almost always have restaurants in addition to animal enclosures.
Zoo >
Animal Enclosures > Animals > Lego bricks
Restaurants >
Kitchen > Lego bricks
Dinning area > Tables > Lego bricks
Ordering area > … > Lego bricks`
So what I mean by methods shouldn’t interact with cousins and siblings is that you wouldn’t want a method buildAnimalsAndTables. By dealing with two unrelated (but equal abstraction level) concepts this method is doing two things and violating SRP.
Method names provide a lot of information about what a method is doing. If a method has ‘and’ in the name often (but not always) this is an indication of a violation of SRP. If you are struggling to name a method it is also a sign that the method may be dealing with too many different concepts.
Creating more abstraction
You get to decide what layers of abstractions you create. This is of course easier said than done. Often when writing code or when requirements change you may find a single concept is too big. This is when you want to create a new layer of abstraction.
buildEnclosures() {
buildGiraffeEnclosure()
buildLionEnclosure()
buildElephantEnclosure()
buildChimpanzeeEnclosure()
...
}
At first building every enclosure inside buildEnclosures works well but as you add more animals the buildEnclosures method starts to get too large. To fix this we will add another layer of abstraction. Maybe one that lets us group enclosures together by region. e.g. Africa Savana, Tropical Rain Forrest, Temperate Forrest… This lets us simplify buildEnclosures.
buildEnclosures() {
buildAfricaSavanaEnclosures()
buildTropicalRainForrestEnclosures()
buildTemperateForrestEnclosures()
...
}
Getting the abstraction layers in your application right isn’t easy. It is a balance between the depth of your application and the size of your methods. Generally you want your methods to be small but you also don’t want your application to have too many layers. Part of balancing this is thinking carefully about the abstraction layers we introduce.
If our zoo is 90% mammals then creating enclosures by animal class isn’t a good abstraction. It would cause our application to be unbalanced. Although it is fine to have different depths of abstraction in different parts of your code (some concepts are just more complicated than others) you don’t want the minimum and maximum abstraction depths to be wildly off from each other.
The best way to achieve this sort of (width) balance is to try to evenly group your concepts when creating additional layers of abstraction.
Dealing with crosscutting concerns
In examples like building a Lego zoo, thinking about the different abstraction levels is relatively easy (which is what makes it a good example) but in the real world it is a bit tricker. There are cross cutting concerns.
What level of abstraction does logging live at? It is a bit of a trick question. For starters logging is really two concepts not one. The act of writing log messages to a cloud service, file or db is a lower level concept. But tracking the state of your application by logging what it is doing is an application level concept.
Generally when talking about logging we are talking about the second of these concepts. And it is problematic. Because each abstraction layer needs to interact with this higher level concept.
I don’t think there is a magic solution to this, applications are going to have some cross cutting concerns. Luckily these concepts tend to be very few relative to all the concepts in an application. Try to minimize the number of cross cutting concerns and use patterns like singleton and dependency injection to avoid having to pass logger into every method.
Conclusion
Getting the single responsibility principal right isn’t easy. Thinking about the abstractions that exist in your application is a great place to start. How many concepts does a method deal with? Does it deal with both high and low level concepts? If so it might be time to do some refactoring.
Note: It is important to note that you can become great at writing clean code and following SRP without ever thinking about abstraction. Just like you can become great a shooting baskets just by practicing. You don’t need to know anything about body position or spin. But sometimes it’s nice to have a shooting coach :). By trying to undersand the inner workings of SRP you can accelerate your learning.
If you found this helpful let me know in the comments and don’t forget to share it with your friends and colleagues.