DEV Community

Cover image for Single-Responsibility Principle done right
Riccardo Cardin
Riccardo Cardin

Posted on

Single-Responsibility Principle done right

Originally posted on: Big ball of mud

I am a big fan of SOLID programming principles by Robert C. Martin. In my opinion, Uncle Bob did a great work when it first defined them in its books. In particular, I thought that the Single-Responsibility Principle was one of the most powerful among these principles, yet one of the most misleading. Its definition does not give any rigorous detail on how to apply it. Every developer has left to his own experiences and knowledge to define what a responsibility is. Well, maybe I found a way to standardize the application of this principle during the development process. Let me explain how.

The Single-Responsibility Principle

As it is used to do for all the big stories, I think it is better to start from the beginning. In 2006, Robert C. Marting, a.k.a. Uncle Bob, collected inside the book Agile Principles, Patterns, And Practices in C# a series of articles that represent the basis of clean programming, through the principles also known as SOLID. Each letter of the word SOLID refers to a programming principle:

  • S stands for Single-Responsibility Principle
  • O stands for Open-Closed Principle
  • L stands for Liskov Substitution Principle
  • I stands for Interface Segregation Principle
  • D stands for Dependency Inversion Principle

Despite the resonant names and the clearly marketing intent behind them, in the above principles are described some interesting best practices of object-oriented programming.

The Single-Responsibility principle is one of the most famous of the five. Robert uses a very attractive sentence to define it:

A class should have only one reason to change.

Boom. Concise, attractive, but so ambiguous. To explain the principle, the author uses an example that is summarized in the following class diagram.

Violation of SRP

In the above example, the class Rectangle is said to have at least two responsibilies: drawing a rectangle on a GUI and calculate the area of such rectangle. Is it really bad? Well, yes. For example, this design forces the ComputationalGeometryApp class to have a dependency on the class GUI.

Moreover, having more than one responsibility means that, every time a change to a requirement linked to the user interface comes, there is a non zero probability that the class ComputationalGeometryApp could be changed too. This is also the link between responsibilities and reasons to change.

The design that completely adheres to the Single-Responsibility Principle is the following.

Design SRP-proof

Arranging the dependencies among classes as depicted in the above class diagram, the geometrical application does not depend on user interface stuff anymore.

The dark side of the Single-Responsibility Principle

Well, probably it is one of my problems, but I ever thought that a principle should be defined in a way that two different people understand it in the same way. There should be no space left for interpretation. A principle should be defined using a quantitave approach, rather than a qualitative approach. Probably, my fault comes from my mathematical extraction.

Given the above definition of the Single-Responsibility Principle, it is clear that there is no mathematical rigor to it.

Every developer, using its own experience can give a different meaning to the word responsibility. The most common misunderstanding regarding responsibilities is which is the right grain to achieve.

Recently, a "famous" blogger in the field of programming, called Yegor Bugayenko, published a post on his blog in which he discusses how the Single-Responsibility Principle is a hoax: SRP is a Hoax. In the post, he gave a wrong interpretation of the conception of responsibility, in my opinion.

He started from a simple type, which aim is to manage objects stored in AWS S3.

class AwsOcket {
    boolean exists() { /* ... */ }
    void read(final OutputStream output) { /* ... */ }
    void write(final InputStream input) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

In his opinion, the above class has more than one responsibility:

  1. Checking the existence of an object in AWS S3
  2. Reading its content
  3. Modifying its content

Uhm. So, he proposes to split the class into three different new types, ExistenceChecker, ContentReader, and ContentWriter. With this new type, in order to read the content and print it to the console, the following code is needed.

if (new ExistenceChecker(ocket.aws()).exists()) {
  new ContentReader(ocket.aws()).read(System.out);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, Yegor experience drives him to defined too fine-grained responsibilities, leading to three types that clearly are not properly cohesive.

Where is the problem with Yegor interpretation? Which is the keystone to the comprehension of the Single-Responsibility Principle? Cohesion.

It's all about Cohesion

Telling the truth, Uncle Bob opens the chapter dedicated to the Single-Responsibility Principle with the following two sentences.

This principle was described in the work of Tom DeMarco and Meilir Page-Jones. They called it cohesion. They defined cohesion as the functional relatedness of the elements of a module.

Wikipedia defines cohesion as

the degree to which the elements inside a module belong together. In one sense, it is a measure of the strength of the relationship between the methods and data of a class and some unifying purpose or concept served by that class.

So, which is the relationship between the Single-Responsibility Principle and cohesion? Cohesion gives us a formal rule to apply when we are in doubt if a type owns more than one responsibility. If a client of a type tends to use always all the functions of that type, then the type is probably highly cohesive. This means that it owns only one responsibility, and hence only one reason for changing.

It turns out that, like the Open-Closed Principle, you cannot say if a class fulfills the Single-Responsibility Principle in isolation. You need to look at its incoming dependencies. In other words, the clients of a class define if it fulfills or not the principle.

Shocking.

Looking back at Yegor example, it is clear that the three classes he created, thinking of adhering to the Single-Responsibility Principle in this way, are loosely cohesive and hence tightly coupled. The classes ExistenceChecker, ContentReader, and ContentWriter will probably always be used together.

Pushing to the limit: Effects on the degree of dependency

In the post Dependency, I defined a mathematical framework to derive a degree of dependency between types. The natural question that arises is: applying the above reasoning, does the degree of dependency of the overall architecture decrease or increase?

Well, first of all, let's recall how we can obtain the total degree of dependency of a type A.

$$
\delta_{tot}^{A} = \frac{1}{n} \displaystyle\sum_{C_j \in {C_1, \dots, C_n}} \delta_{A \to C_j }
$$

In our case, type A is the client of the class AwsOcket. Recalling that the value of \(\delta_{A \to C_j }\) ranges between 0 and 1, dividing without any motivation the class AwsOcket into three different types will not increase the overall degree of dependency of client A. In fact, the normalizing factor \(\frac{1}{n}\) assure us that refactoring processes will not increase the local degree of dependency.

The overall degree of the entire architecture will instead increase since we have three new types that still depend on AwsOcket.

Does this mean that the view of the Single-Responsibility Principle I gave during the post is wrong? No, it does not. However, it shows us that the mathematical framework is incomplete. Probably, the formula for the degree of dependency should be recursive, in order to take into consideration the addition of new tightly coupled types.

Conclusions

Starting from the definition given by Robert C. Martin of the Single-Responsibility Principle, we showed how simple is to misunderstand it. In order to give some more formal definition, we showed how the principle can be viewed in terms of the concept of cohesion. Finally, we try to give a mathematical proof of what we have done, but we went to the conclusion that the framework that we were using is incomplete.

This post concludes the year 2017. I want to thank all the people that took some of their time to read my post during this year. I will certainly return in 2018. Stay tuned.

Happy new year.

References

Top comments (10)

Collapse
 
courier10pt profile image
Bob van Hoove

I've always felt that SRP is the most confusing of SOLID principles and perhaps the hardest to get right.

If you (mis)take 'Single Responsibility' to the extreme in a language like Java or C# you end up with many classes exposing a single function. This should make you wonder, am I using the right language? All these functions in mini objects could be partially applied functions in another language.

In my interpretation I decided to focus on the 1 reason to change aspect, the single aspect never really made sense to me. As you point out, the unit of responsibility should center around cohesion. And now it does make sense.

Thanks for writing.

Collapse
 
riccardo_cardin profile image
Riccardo Cardin

Thanks to you to have summarized the entire post so well.

Collapse
 
orkon profile image
Alex Rudenko

I think SRP might be more easily interpreted when applied to functions, not classes. With functions, it's easier to say what it is responsible for because a function is not a collection of attributes and methods like a class. Also, a class is expected to correspond to some domain concepts which may not follow SRP themselves. I think Yegor's example does not need classes at all: one can have just three functions which all follow SRP: exists, read and write (pretty much like corresponding POSIX system calls). Also, the Yegor's example is quite similar to Martin's example, and I am not convinced that the following conclusion is true:

The classes ExistenceChecker, ContentReader, and ContentWriter will probably always be used together.

Some clients might only want to read the data, others just write. The existence check might not be used at all. I notice the same patterns used in Java. For example, docs.oracle.com/javase/7/docs/api/... & docs.oracle.com/javase/7/docs/api/... and tons of other classes are split into readers and writers.

P.S. is it a good thing when a Rectangle draws itself using a draw method? Should not some other thing draw a rectangle? I noticed that this kind of patterns is often confusing to people new to OOP.

Collapse
 
alainvanhout profile image
Alain Van Hout • Edited

I've always found it counterintuitive that a square should be able to draw itself. What if you don't use a method of drawing that was known when the Rectangle class was created? (assuming perfect foresight is a big OOP issue -- and decorators and visitors aren't a panacea)

Collapse
 
riccardo_cardin profile image
Riccardo Cardin

Well, talking about functional programming, your reasoning is correct. But, here we are talking about object-oriented programming ;)

Anyway, if clients of reading and write operations are different, then the design should be reviewed. In this fact, I agree with you.

Collapse
 
courier10pt profile image
Bob van Hoove • Edited

Some clients might only want to read the data, others just write.

OP:

the clients of a class define if it fulfills or not the principle.

Seems like you're on the same page.

Collapse
 
ozzyaaron profile image
Aaron Todd

Thanks! This is a great article. As primarily a Ruby developer I find Interface Segregation to be a difficult one to convey myself :) It is possible but you need some convoluted examples. I really liked that you came down to cohesion being the responsibility.

I feel like with most of these principled they are great in theory and great to keep in mind whilst working BUT where and when to apply them requires consideration and it is not always best to apply them especially at the outset of solving an issue.

I personally find SRP to be more about dependencies and what would need to change if the assumptions we built this code under change.

I recently attended a course by Sandi Metz where she quite honestly made the point (that I will paraphrase) that OOP is about tradeoffs and that OOP is quite often about making the entire problem harder to think about with the hopes that we'll be able to create lots of little things to think about. By focussing on SOLID we aim to make those pieces so easy to think about, work on and maintain that the overall additional burden of using OOP is moot.

With that in mind I would say that you have to ask, is splitting something like data access into 3 classes beneficial to everybody that would touch this code? Not to mention how often are we in this code fixing bugs and adding features?

In the end I think we should consider ourselves in the application space to be like engineers where our output has to be justified by the resources used to achieve the goal. If we split a simple data access class into 3 classes that we never touch again OR that we need to load every time we think about data access then we have made the wrong choice.

Thanks again!

Collapse
 
riccardo_cardin profile image
Riccardo Cardin

Many thanks to you for the long comment!

Collapse
 
hamsterasesino profile image
Gabriel

In this case I agree with Yegor's approach. Most of the times when dealing with I/O you have a Reader class and a Writer class (like Java does: FileReader,
FileWriter). I think it is the best approach in terms of separation of concerns.

What I would expect from the class AwsOcket is to be some kind of simplified interface that actually dellegates the implementation of the read() and write() methods in proper classes.

Good post! Thank you

Collapse
 
riccardo_cardin profile image
Riccardo Cardin

Maybe you're right. However, if the reading and write operations are separated into two classes, the overall dependency degree of a client will be increased without any reason.

This fact makes me prefer the solution that uses a single type that exposes both the reading and write operations.