Introduction
Most experienced software engineers have likely, at some point, read Clean Code by Robert C. Martin.
In this book, Martin highlights five principles of object-oriented programming that, if followed correctly, promises to result in a "clean" codebase. These five principles make up an acronym known as the "SOLID" principles:
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
If you're unfamiliar with SOLID, feel free to either check out the Clean Code book, or the Wikipedia page.
A Brief Tale
It was very early in my career when I first studied these principles. Probably too early. As I was lacking in experience, I wasn't fully clear on what problems the principles were aiming to solve. Therefore, when I tried to apply them to my work, my efforts were somewhat aimless.
As is common with design patterns among some junior engineers, I was applying principles and patterns for the sake of applying them, without necessarily trying to solve a specific problem.
It didn't help that I didn't really understand the SOLID principles. Moreover, I wasn't even yet fully versed in object-oriented programming, so how could I possibly understand the importance of decoupling via abstractions and interfaces?
The inevitable result was that I misunderstood SOLID. Specifically, the Single Responsibility Principle (SRP). Furthermore, after being exposed to a variety engineering teams for over a decade, I've observed that it's not just I who misunderstood SRP—and that this misunderstanding is not limited to just junior engineers!
The Single Responsibility Principle
For the duration of this article, we'll be focusing on the Single Responsibility Principle, which I'll henceforth refer to simply as SRP.
If you're not familiar, the original definition of SRP was:
A class should have one, and only one, reason to change.
— Robert C. Martin, Clean Code
The problem with programming principles is that trying to fit their meaning into a single, catchy sentence leaves so much room for interpretation! What constitutes a 'reason'? This, coupled with the naming of the principle (Single Responsibility), results in engineers commonly misquoting the principle as:
A class should have one, and only one, responsibility.
— Engineers everywhere, including me—once!
A dangerous statement indeed. What do we mean by "responsibility"? What is the boundary of a given responsibility? Again, it's completely open to interpretation.
Consider the following primitive code sample:
class Person {
public int ageInYears;
public boolean canApplyForDriversLicence() {
return ageInYears >= 17;
}
public boolean canDrinkAlcohol() {
return ageInYears >= 18;
}
}
Playing devil's advocate, I can immediately identify two reasons for this class to change:
- We may wish to change the age at which a Person becomes eligible to apply for a driver's licence; or
- we may wish to change the age at which a Person is allowed to drink alcohol.
Does this mean that we should extract these checks out into separate classes? Not necessarily! Person
is a domain entity, and is an ideal place for our business logic to live. In past projects, however, I've frequently witnessed such pointless extraction under the pretext of SRP.
Therein lies my point: blindly following SRP can lead to code fragmentation, in turn leading to increased complexity, and decreased maintainability.
SRP clarification
Fortunately, Robert C. Martin was aware of this confusion and, in 2014, published an article in an attempt to clarify his thinking behind the principle.
The key takeaway from his article is the following:
This principle is about people.
— Robert C. Martin
That narrows the boundaries somewhat in the statement "a class should have only one reason to change". The analogy Martin uses in his article is the division of labour between executives at a fictional company:
- A CTO is responsible for all things technology;
- a COO is responsible for all things operations; and
- a CFO is responsible for all things finance.
According to Martin, it is therefore safe to assume that the change boundaries lie with each role:
- Any code in our software relating to technology (i.e. saving database records) should be grouped together;
- any code in our software relating to operations (i.e. timesheet generation for employees) should be grouped together; and
- any code in our software relating to finances (i.e. algorithms for calculating the pay of employees) should be grouped together.
This grouping ensures that changes to one part of the system don't affect other parts of the system. If we were to update an employee's pay, we wouldn't want to cause a regression in the timesheet generation.
It's a well-written article, and the above example makes sense, but, in my opinion, it doesn't clear up the ambiguity around SRP.
Cohesion & coupling
Most engineers will have heard the term cohesion being tossed about. In essence, cohesion is a metric which refers to how closely the functionality of a module is related. Low cohesion means that the code within a module is loosely related and makes code harder to reason about. Conversely, high cohesion means that the code within a module is closely related.
Another commonly used term is coupling, which is a metric used to measure the interdependence between different modules of the codebase. If you have high (AKA tight) coupling, you have lots of classes that are dependent on each other. This is bad because a change to a given class can easily break the functionality of any other class which depends on it.
Mandatory disclaimer: coupling isn't inherently evil and has plenty of valid use cases in OOP. A high level of coupling, however, is generally considered a code smell and can lead to unmaintainability.
It is generally advised within OOP to strive for high cohesion and low coupling. While cohesion is related to the idea of classes and modules focusing on a single responsibility, it does not necessarily mean that a class should have only one responsibility as implied by SRP.
Cohesion vs. SRP
To prove my closing remark of the last paragraph, consider the following oversimplified code sample:
public class Book {
private String title;
private int totalPageCount;
private int currentPage = 0;
private Integer bookmarkedPage = null;
public Book(String title, int totalPageCount) {
this.title = title;
this.totalPageCount = totalPageCount;
}
public void turnPage() {
if ((currentPage + 1) < totalPageCount) {
currentPage++;
}
}
public void turnBackPage() {
if (currentPage != 0) {
currentPage--;
}
}
public void placeBookmark() {
bookmarkedPage = currentPage;
}
}
Is this code cohesive? I'd say so, with a high degree of confidence. It clearly groups together related attributes and methods, focusing on book-related functionality. We're also keeping our domain logic centralised which is a central theme of Domain Driven Design (DDD).
Does the code adhere to SRP? Well, you could say it has two responsibilities: turning a page, and placing a bookmark. Therefore the answer to this question would be a resounding no. Unless "managing a book" can be considered an overarching responsibility? In which case... the answer is yes... probably?
Going from Martin's latest definition of SRP being about people, you could say that the person in question would be the reader, in which case the class would be compliant with SRP.
However, if an engineer came along and deemed this class to be in violation of SRP by following its misunderstood definition, he may see fit to extract the class into two (or more!), leading to increased code fragmentation and possibly even tight coupling.
While there is a level of subjectivity to measuring cohesion, it carries nowhere near the level of ambiguity that comes with the Single Responsibility Principle.
Conclusion
In my opinion, the "resposibility" part of SRP is far too vague to be able to offer any kind of meaningful guidance.
While SRP shares commonalities with the concept of cohesion, and Martin's 2014 article does well to clarify the principle, it still adds an unnecessary layer of ambiguity in my view.
Striving for SRP alone can often do more harm than good. If you want my advice: instead, focus on attaining high cohesion and low coupling.