SOLID: Exploring the Single Responsibility Principle
Single Responsibility Principle (SRP) is the first of the five SOLID principles, which were researched by Robert Cecil Martin. What became knowns as SOLID was firstly described by “Uncle Bob” in March of 1995 [1] on comp.object newsgroup as a part of “The Ten Commandments of OO Programming” proposal [3].
What is 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” [2] 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 [4].
More detailed look
Let’s take a look at Single Responsibility Principle by an example. I want to open some discussion later, so I will use the same example as in the Martin’s book [2]. The example is shown on the diagram below.
classDiagram Rectangle <-- `Graphical Application` GUI <-- `Graphical Application` Rectangle --> GUI `Computational Geometry Application` --> Rectangle class Rectangle { +draw() +area() double } class `Graphical Application`:::app { } class `Computational Geometry Application`:::app { } class `GUI`:::app { }
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 - Graphical Application
, which makes use of draw()
function to
render the rectangle and Computational Geometry Application
, which uses
area()
function to perform some calculations. Because Rectangle
deals with
rendering it has a dependency on GUI
component, which is provided by some GUI
library.
One consequence of this design is that Computational Geometry Application
depends on GUI
and the library providing it would need to be linked
in into Computational Geometry Application
, even though
Computational Geometry Application
does not need GUI by itself. Secondly, any
change to Rectangle
will require recompilation of both:
Computational Geometry Application
and Graphical Application
.
Robert Martin proposes to break Rectangle
into two separate classes as shown
on the diagram below.
classDiagram Rectangle <-- `Graphical Application` GUI <-- `Graphical Application` Rectangle --> GUI Rectangle --> GeometricRectangle `Computational Geometry Application` --> GeometricRectangle class Rectangle { +draw() } class GeometricRectangle { +area() double } class `Graphical Application`:::app { } class `Computational Geometry Application`:::app { } class `GUI`:::app { }
Indeed, we got rid of unwanted GUI
dependency of
Computational Geometry Application
and modification of draw()
function can not affect GeometricRectangle
(changes to GeometricRectangle
will affect Rectangle
and Graphical Application
however).
In the above example we initially had two 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 change of requirements of GUI
classes (Graphical Application
in
the book, which I believe is a clerical error, since it is
Graphical Application
, which depends on Rectangle
not the other way
around). As a remedy we turned Rectangle
into two classes that both have
single reason to change, hence both classes have single responsibility and we
conform to SRP. Well… In real life things aren’t usually so simple.
Discussion
The problem with the examples is that in comparison to real life scenarios, they
can be over-idealized. Let’s say we would need to make certain operations on
triangulated model of the rectangle. Our GeometricRectangle
API could
be extended like this:
classDiagram class GeometricRectangle { +area() double +triangulated() Triangulation }
Let’s assume that we have added additional Triangulated Geometry Application
,
which would make use of triangulated()
method. To perform the triangulation we
might want to use a popular CGAL library.
classDiagram GeometricRectangle <-- `Computational Geometry Application` Triangulation <-- `Triangulated Geometry Application` GeometricRectangle --> Triangulation `Triangulated Geometry Application` --> GeometricRectangle class GeometricRectangle { +area() double +triangulated() Triangulation } namespace CGAL { class Triangulation } class `Triangulated Geometry Application`:::app { } class `Computational Geometry Application`:::app { }
The 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 just like in initial
example things are much more blurry. Shall we break GeometricRectangle
class
again? This would most likely require a refactoring of the existing code. But
what if Computational Geometry Application
would want to use triangulated()
method at some point? Moreover, the GeometricRectangle
now depends itself on
external CGAL library, so GUI classes would depend on it as well and we can’t
get rid of one or another by simply extracting relevant portion of 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). Just take a look at Qt Quick’s Rectangle component or Flutter’s Container class.
Does it mean that designers of state-of-the-art frameworks never heard about SRP? Or perhaps, 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 shows the point, but such finely granular architecture is never found in
real life. Neither Rectangle
nor GeometricRectangle
live without a context.
It is the surrounding which defines their “reason to change”. In real life you
can’t expect that your class will be ever used by only two other components. In
fact, a good designer should assume that his class will be used by indefinite
amount of users. The “reason to change” becomes a statistics. 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 - 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 and serve the majority of users. And you should see “reason to change”
in terms of use cases, rather than individual associations.
To sum it up, Single Responsibility Principle is an important concept, which should guide your designs and architecture. You need to look at it from a proper perspective. Other software principles, good practices and your own experience should help you find this perspective.
References
- R. C. Martin, The Principles of OOD, www.butunclebob.com. http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod (accessed: Mar. 31, 2024).
- R. C. Martin, Agile software development, principles, patterns, and practices: Pearson new international edition. London, England: Pearson Education, 2013.
- R. C. Martin, The Ten Commandments of OO Programming. object.comp newsgroup, Mar. 1995.
- E. S. Raymond, “Chapter 1. Philosophy”, in The Art of Unix Programming. Eric S. Raymond, 2003. Accessed: Mar. 31, 2024.