SOLID violations in the wild: The Single Responsibility Principle
This blog is part of a series explaining the SOLID design principles by looking at code from real projects or frameworks. Last time, we looked at the Liskov Substitution Principle. This time, we’ll discuss the Single Responsibility Principle.
SOLID is an acronym for the following design principles:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
The SOLID principles are often explained by using simple examples, but that sometimes makes it hard to spot them in your own code for a real project. That’s why I searched for some examples of these issues in Open Source projects and use them in this series.
The Single Responsibility Principle
The Single Responsibility Principle, introduced by Robert C. Martin, is a derivative of the work of Tom DeMarco and Meilir Page-Jones and states:
A class should have only one reason to change.
A common misconception about this principle is that people think it means that a class should do only one thing. This is not the case: a class can do multiple things. If not, you’d end up with classes that only have a single public method. Instead, this principle is all about cohesion. Cohesion is making sure that things that are related are grouped together, and things that aren’t related are not. The Single Responsibility Principle states that we want to group only those things that satisfy a single responsibility.
Why do we want this? Because we want to be able to safely change behavior related to a responsibility without unintentionally change behavior that is related to another responsibility. In other words: we want to have one axis of change on which changes can happen, instead of multiple axes that can cross each other. If a class has multiple responsibilities those responsibilities become coupled, leading to fragile designs. These designs can break and lead to regression (unwanted change in functionality) when changing some code to satisfy new requirements for one of the responsibilities.
It’s good to realize the connection between SRP and Don’t Repeat Yourself (DRY) when DRY is not applied properly. Don’t Repeat Yourself is a principle that says that you should not duplicate business logic in your application. But this is sometimes misinterpreted as avoiding duplicate blocks of code. One way to avoid duplicate code blocks is to put everything in a single class, but this could lead to a class that has multiple responsibilities that are coupled through the shared code block. So DRY and SRP only conflict with each other when not applied properly.
An example violation in the wild
Again, let’s look at some code from the NLog framework, a .NET framework that deals with logging and is similar to frameworks like Log4j in Java. I selected this project not because it has low quality code but because it is a medium sized project and has multiple contributors. Chances are that you’ll find potential issues like this in any reasonably sized project, either introduced consciously or unconsciously. That is one of the things that make Open Source great: we can be exposed to others’ work and learn from it.
In this project, there’s a class LogFactory that is responsible for creating and managing Logger objects. It does multiple things (showing that a class doesn’t just have to do one thing), but they are all related to this one responsibility. There is however one method feels out of place, and that’s the LoadConfiguration method:
This LoadConfiguration method is responsible for reading an XML file containing the configuration for the LoggerFactory to use. All of the sudden, we have another responsibility besides creating and managing Logger objects: reading configuration from a file system. These responsibilities are coupled, and both have their own reasons to change.
For example, if I want to add functionality to not only support XML config files but also JSON config files, I need to change this class, which potentially leads to regression in the “create and manage Logger objects” functionality. In this case, I would refactor this code and move this functionality to its own class.
How to spot these kinds of violations
There are a couple of ways to spot these kinds of violations. If you don’t have immediate problems with your code (e.g. regression) you can start with checking if you have big classes with lots of methods. Carefully inspect these methods of those classes and determine if they all really satisfy the same responsibility.
When you have regression in your application, check if the class or module that is the source of the regression problem has multiple responsibilities. If the problem is not caused by the violation of this principle, it could still be caused by other design problems for example due to a hard coupling with other classes.
Another sign of this violation is when you are working with multiple developers or even development teams on the same code base but on different functionality and you constantly interfere with each other. This interference pops up as merge conflicts or regression in files that both parties changed. If you use Git as VCS, you can get a list with the top 10 files that changed most often, thus are a potential problem area:
How to solve these violations
Most of the time, solving this problem is easy. If you have a class that violates this principle, extract the methods belonging to one of the responsibilities and create a separate class for them. Continue doing this until you have only one responsibility per class.
If you still want to have one entry point to call instead of having to work with multiple classes from your caller, you could use the Façade Pattern to achieve this. The Façade class is then responsible for instantiating and delegating to the appropriate classes. Robert C. Martin describes this solution in his book Clean Architecture.
The Single Responsibility Principle is a powerful yet often misunderstood design principle. Adhering to it makes changing functionality safer and easier by reducing the blast radius and preventing unwanted side effects of the change. Luckily, there are simple ways to find and solve violations in your code, making sure your code is in a good shape for change!
This blog was written by Harm Pauw.