SOLID: Exploring the Single-Responsibility Principle
The Single-Responsibility Principle (SRP) is the first of the five SOLID principles of object-oriented design intended to make object-oriented designs more understandable, flexible, and maintainable. Originally described by Robert “Uncle Bob” Cecil Martin in March 1995 on the comp.object newsgroup as a part of “The Ten Commandments of OO Programming”, the five principles are S - Single-responsibility; O - Open-closed; L - Liskov Substitution; I - Interface Segregation; and D - Dependency Inversion.
In this blog, I will focus on the “S” – the Single-Responsibility Principle.
What is the Single-Responsibility Principle?
In short, the principle says that a class should have only one reason to change. So why is it called a “single responsibility principle” and not, say “single reason to change principle”? Martin in his book Agile Software Development refers to Tom DeMarco and Meilir Page-Jones, who researched the principle before and called it cohesion. Their definition focused on functional relationships between elements of a class or a module, hence “responsibility.”
Martin refines the principle formulation to make it more objective and applicable, but the meaning is still the same – if a class has more than one reason to change, it presumably means that it has more than one responsibility and vice versa. The single responsibility principle is also present in the Unix philosophy, where it is stated that programs shall do one thing and do it well.
Here’s why SRP matters: When each class is dedicated to a distinct task, developers can easily reuse particular components across various sections of the application or even in other projects. This practice enhances the efficiency of the codebase and streamlines the development process.
Take a Closer Look
Let’s look at an example from Martin’s book as a way to explore the SRP.
We have a class called Rectangle, which provides two public methods: draw() and area(). There are two applications, which use Rectangle for their own purposes. The Graphical Application makes use of the draw() function to render the rectangle. The Computational Geometry Application uses the area() function to perform some calculations. Because Rectangle deals with rendering it has a dependency on a GUI component, which is provided by some GUI library.
One consequence of this design is that the Computational Geometry Application depends on the GUI, and the library providing it would need to be linked into the Computational Geometry Application – even though the Computational Geometry Application does not need the GUI by itself. Additionally, any change to Rectangle will require recompilation of both the Computational Geometry Application and the Graphical Application.
Martin proposes to break Rectangle into two separate classes as shown below.
Indeed, we got rid of any unwanted GUI dependency of the Computational Geometry Application, and modification of the draw() function can’t affect GeometricRectangle. (However, changes to GeometricRectangle will affect Rectangle and Graphical Application.)
In the above example there were two possible reasons to change the Rectangle class: we might need to change the way it renders itself, or we might want to change the way it calculates the area. More importantly, the change could be caused by a change of requirements of GUI classes.
While the book describes this as Graphical Application, I believe this is a proofreading error since Graphical Application depends on Rectangle and not the other way around. As a remedy we turned Rectangle into two classes that both have a single reason to change, hence both classes have a single responsibility – meaning we conform to SRP.
Not So Simple
Of course, in real life things aren’t usually so simple. The problem with the example is that in comparison to real-life scenarios, they can be over-idealized. Let’s say we would need to make certain operations on a triangulated model of the rectangle. Our GeometricRectangle API could be extended like this:
Let’s assume that we have added an additional Triangulated Geometry Application, which would make use of the triangulated() method. To perform the triangulation we might want to use a popular CGAL library.
History repeats: we have two methods, dependency on CGAL library which needs to be linked into two different applications and two reasons to change GeometricRectangle class.
Even though dependencies are the same as in the initial example things are much more blurry. Shall we break the GeometricRectangle class again? This would most likely require a refactoring of the existing code. But what if the Computational Geometry Application would want to use the triangulated() method at some point?
Moreover, the GeometricRectangle now depends on an external CGAL library. That means GUI classes would depend on it as well, and we can’t get rid of one or the other by simply extracting the relevant portion of the API into a separate class. To add even more pepper, consider that in real-life frameworks you will see graphical aspects (e.g. rendering and appearance) mixed altogether with computational geometry aspects (e.g. transformations). You have to look no further than Qt Quick’s Rectangle component or Flutter’s Container class.
So does this mean that the designers of state-of-the-art frameworks have never heard about SRP? Or perhaps, that SRP is wrong? No and no. The problem lies in the scale and context.
Martin in his example uses two extremely specialized applications – one for computing the area and the second one solely for drawing. This illustrates his point in an academic sense, but such finely granular architecture is never found in real life. Neither Rectangle nor GeometricRectangle live without context. It is the surrounding that defines their “reason to change.”
In real life you can’t expect that your class will be ever used only by two other components. In fact, good designers should assume their class will be used by an infinite number of users. In this case, the “reason to change” becomes a statistic.
Qt Quick and Flutter provide a set of methods useful primarily for all sorts of GUI-related operations for GUI applications. Even though some of the methods could be used by computational geometry applications this can’t affect the design.
As a software designer you can put a fence – indicate that this class is not intended to be used by computational geometry applications. You can’t really predict what someone would want to do with your class, but you can tell them how you intended the class to be used. This way, you serve the majority of users. (One more point: you should see “reason to change” in terms of use cases rather than individual associations.)
To sum it up, SRP is a vital concept that you should lean on to guide your designs and architecture.