See Part 1 of this series on how the SOLID principles can help us follow emergent design without the Big Up Front Design and without the code rot.
Single Responsibility Principle
"A class should have one, and only one, reason to change"
In software, we tend to enjoy grouping things together, but really what we should be doing is splitting them apart. This really gets back to the notion of cohesion.
Every time a class has multiple responsibilities, you’re creating unecessary dependencies that can lead to code rot. You know the problem, you change one part of your code, only to find it has completely unintended effects on a completely different part of your application. What we want instead is to make sure code never gets touched, heck never even has to go through a recompile, unless we really do intend to change it’s behavior.
How can we fix this? By limiting each class to a Single Responsibility. Every time you place more than one responsibility into a single class, those two responsibilities become coupled together and every time you change one responsibility, you risk impairing the class’s ability to meet its other responsibilities. Again, you risk changing one area of the code only to inadvertently break another area.
Okay, but what is a responsibility?
A responsibility, in this context, is a reason to change. Sounds simple enough, but we’re so accustomed to grouping things by type or data that we often miss it. For example, consider the Modem class below that Bob Martin uses as an example:
That’s just how we were taught to design good objects, right? Take a thing from the real world and group it with the actions it can perform. Here, we’ve got a modem (thing) that can dial, hang up, send, and receive data (actions). So, what’s the problem?
The problem is that we actually have 2 different responsibilities here. The first is connection management, the second is data communication. You can probably easily envision the method of data communication changing – perhaps the data transfer protocol changes — while the modem continues to dial and hang up the same way it always did. As such, the recommendation is to separate these responsibilities into two separate interfaces, as such:
This way, if our scenario of the data transfer protocol changing occurs then it will impact the Data Channel (if the protocol changes then it will in all likelihood will impact both send() and recv()), but not Connection. Similarly, if your physical hardware changes – that would impact Connection (how you dial and hang up on that hardware) but not necessarily how data is sent and received.
Simple? Well, according to Bob Martin,
"The SRP is one of the simplest of the principles, and one of the hardest to get right. Conjoining responsibilities is something that we do naturally. Finding and separating those responsibilities from one another is much of what software design is really about."