DEV Community

Cover image for The Single Responsibility Principle Revisited
Matthieu Cneude
Matthieu Cneude

Posted on • Originally published at thevaluable.dev

The Single Responsibility Principle Revisited

coupling vs cohesion

Once upon a time, at the beginning of my journey as a professional developer, I quickly heard about the principle which will save us all, part of the Sacred SOLID principles. The senior developers, the chosen ones, were calling it the Single Responsibility Principle, or SRP.

The SRP looked like a magic spell I could cast for my code to be instantaneously better. But first I had to understand it. What's a responsibility? I was asking myself, my mouth full of muesli, a Sunday morning, while sloppily browsing The Internet.

What did I find? My classes should only have one reason to change. Like this class which represent an animal in this article, or this Circle class in another article, or this Spaceship (?) class in this blog post.

"Of course!" I thought. "If a class has only one reason to change, it's nicer! It's small, it's light, it's great! I understand development!"

When I came back to work the following Monday, I looked at the PHP classes I was working on. The Customer class. The CheckoutController class. The Shipment class. What's a reason to change for the Shipment class? Well, maybe our client, who asked us to build an e-commerce, will want to change the Shipment class at one point?

What will change? I wasn't sure, but every single method looked like they could have one reason to change. All these responsibilities in the same class! What I was thinking?

I fragmented my Shipment class, creating many more classes with one method each: everything was light, teared apart, shredded. It looked great. I felt like I was following the path of the Giants, the Evangelists, the Words of the Gods themselves. Who's the "Senior", now?

This project became a mess of classes all coupled to each other. It was difficult to understand, difficult to scale, and difficult to maintain. Exactly the contrary of what you would expect from the SRP!

What did I do wrong?

This is how I experienced first hand the Single Responsibility Principle, and, since then, I saw many beginners trying to do the same. This is one of the reasons I would like to speak with you about the SRP.

Let's ask these questions, and try to find the answers:

  • What is the SRP?
  • Why is the SRP so often misunderstood?
  • What other principles the SRP is based on?
  • Should we continue to use the SRP?

I hope you're comfy on your chair, your favorite beverage at hand, ready to dive in, because we're going now!

What's the Single Responsibility Principle?

The Sacrosanct Definition

Let's go back to the definition we all have to write in a blog article at least once in our lifetime. It gives you good luck.

A class should have only one reason to change.

In there we have Single (one reason), but what about Responsibility? It's linked to "change". If a class has one responsibility, it has only one reason to change. Many responsibilities, many reasons to change.

This is from Robert Martin whose charisma always enlighten the stage. Then, I need to write, to bring good fortune on my family on three generations, that "it's a very important key for good software designs, and everybody should remember its definition and say it out loud three times each evening for the light to shine on our code".

Really? Is it that simple?

The Misconceptions

As my years of experience were increasing, I realized that everybody had its own personal definition of a "responsibility", or a "reason to change". Many were asking these questions:

  • What is a reason to change? Is debugging a reason to change? Refactoring?
  • Why one reason to change is better than more reasons to change?
  • Can we really apply one reason to change for every single class, for any technology or language used, for any business domain?
  • One reason to change makes our classes "small". Does a "big" class violates the SRP?

Violation. This is a powerful word, and when we speak about the SRP, nothing is powerful enough.

If you begin to search for answers on the Internet, you'll have a crazy lot of different opinions, sometimes contradicting each other, especially when you look at the comments. The SRP is famous, therefore you'll find an incredible noise around it. On top, many examples will try to make their points by fragmenting classes to one-method-class.

Let's stop the pain. Coming back to the source of the principle would be a smarter thing to do. Let's read the words of Robert Martin himself.

In there you'll stumble upon more examples (the Rectangle class?) without any context (the Employee class?), stating that it's obvious for these classes to be fragmented, because too many responsibilities, you know.

Now, in favor of Martin or anybody who wrote about the subject, you can't really use complex examples to illustrate your point without losing your readers. Still, with this definition which emphasis uniqueness ("one"), it looks like every single class should be one function each. Two, if you're generous.

To be totally fair, Martin is clarifying some points, while explaining the SRP: no, debugging and refactoring are no "reason to change". Yes, you should decouple depending on the context, or you might add unnecessary complexity. This principle has many rules the definition itself doesn't really state.

When I understood my mistakes to create one method per class when I was a younger me, I still couldn't really make sense, clearly, of the SRP. What to do, in that case?

Since Martin didn't really answer my question, I decided to go deeper. To the roots of the SRP themselves!

It's where our quest will become more interesting.

The Roots of the Single Responsibility Principle

Less coupling, more cohesion

Even if the Single Responsibility Principle speaks about "class", it's not about class. The principles we'll see now could be applied despite the paradigm. OOP, functional, procedural, it doesn't matter.

If we don't speak about classes, what are we speaking about? Let's use this definition of Edward Yourdon and Larry Constantine, two software engineers:

A module is a lexically contiguous sequence of program statements, bounded by boundary elements, having an aggregate identifier. Another way of saying this is that a module is a bounded, contiguous group of statements having a single name by which it can be referred to as a unit.

This is from Structured Design, referenced as an inspiration of the SRP by Martin. The concept of module will serve us well for what's following.

If the definition looks obscure to you, a module is simply a named block with some code in it. It could be a function, a class, a namespace, a package, a micro-service, even a file.

Decomposition

The principle of decomposition, also called factoring, goes way beyond computing. It touches the essence itself of our work: problem solving.

When you have a problem to solve, it's often better to decompose it in smaller part. Let's say that you want to develop the next big video game which will make you rich. It looks daunting at first: where to begin? What do you have to do? Is it difficult?

We can decompose the problem in sub-problem:

  • You'll need some graphics for your video game.
  • You'll need some code, too.
  • You'll need to design your levels, or your open world.
  • You'll need to create music and sound effects.

Now, you've created more problems, but they feel already more manageable. At least, it will give you some ideas about your next steps. Then, you can break every single one of these sub-problems even further: you need to decide of the kind of graphics you want, what language you'll use to program it, and so on.

At every step, you'll make some decisions, depending on your goals. These decisions will be very different if you want to create a breakout clone or a full RPG in 3D. The context will heavily drive your decisions.

Every single sub-problem should solve part of your main problem, such as, when every sub-problems are solved, the main problem is solved. Otherwise, your decomposition is wrong. You missed something.

Why breaking down problems to make them more manageable? Why can we take decisions more easily afterward, even if we were a bit lost when stating the main problem?

You, me, us, as humans, are not very good to cram a lot of information in our heads. If we try to, it will be very complex to think about, reason, or solve everything at once. You might even experience the common disease of a developer: headaches, imposter syndrome, and stress.

By studying each aspect of the problem in isolation, we increase dramatically our chances to solve the problem itself.

It is [intelligent thinking] that one is willing to study in depth an aspect of one's subject matter in isolation [...]. It is what I called sometimes the "separation of concerns" which [...] is yet the only available technique for effective ordering of one's thoughts.
Edsger W. Dijkstra - Source

This concept of decomposition is very useful, and can be applied in many domains. It's useful for our codebase, too. We need to decompose it to have a good grasp on what it's doing, to maintain it, and to extend it. Therefore, like we decompose our problems, we need to decompose our code in different parts.

This brings us to an obvious question: how do we do that?

Behold Cohesion and Coupling!

Cohesion

Let's take back the book Structured Design for another definition:

Cohesion: the degree of functional relatedness of processing elements within a single module.

After decomposing your problem, you need to put back together elements which are related to each other, which belong together. To take an analogy, you do that all the time in real life (depending how messy you are). You put the knives with the knives, the fork with the forks, the towels with the towels. You won't put your dirty clothes with your plates; they don't belong together.

This decomposition and recomposition of your problems can drive your architecture. You can then begin to code, creating modules where each element belong to each other. If your module is a class, you need methods (behavior) and data in that class related to each other. If it's a namespace, you need classes related to each other in the namespace. And so on.

This allows you to reason easily at the level of your module. The benefits?

  • You don't need to reason about your whole codebase while working on a functionality, you only need to reason in the boundaries of your module.
  • When you have a bug, you know in what module to search.

To understand what coherence is, let's see what's not coherent. Take anything which has "utils", "helpers" or "misc" in their name. Again, it could be a package, a class, a function, it doesn't matter. By definition, they are a mix bag of functionalities which don't belong together. That's why they have such generic names. Don't hesitate to decompose these messy containers and recompose them to increase cohesion if you can.

Cohesion is linked to the concept of coupling. That's great, because it's the next part.

Coupling

What does Structured Design say about coupling?

Coupling as an abstract concept - the degree of interdependence between modules - may be operationalized as the probability that in coding, debugging, or modifying one module, a programmer will have to take into account something about another modules.

This touch one of the main problem in software development: consequence of changes, even simple, can spread like crazy in the entire codebase. You change a behavior, and many other behaviors suddenly change too. The consequences range from systems crashing to silent bugs ruining your data.

As I was saying, coupling and cohesion are linked:

  1. If you have a good cohesion in your module, everything belongs together.
  2. If everything which belongs together are together, changing the module won't affect other module.
  3. If changing something in module A affects module B, it means that this something from module A should be in module B.

"That's great!", you might think. "But now I'm even more confused. How do we achieve high cohesion and low coupling between our modules?".

Another source quoted by Martin as a big inspiration for the SRP is the paper On the Criteria To Be Used in Decomposing Systems into Modules. This introduces the concept of information hiding, coined for the first time by this paper. To achieve less coupling, each module should know the minimum about each other.

Now, how do we hide information between our modules? Using abstractions with well-defined interfaces. I'm not speaking about the interface construct here, the stuff you define in many OOP language using the keyword interface. I'm speaking about the way each module interact with each other.

For example, public methods and properties of a class are its interface; if everything is public, nothing is hidden. A micro-service has an interface too: its API.

What I mean by "well-defined interface" is the necessary functionalities the module expose to other modules, no more, no less. If you can modify everything of a module from another module, you'll create cascade of changes in your application.

On the other side of the spectrum, if your modules can't use other modules at all, your software won't do much.

The benefits of high cohesion and low coupling are huge:

  • As we said, you'll have a clear mental model, a clear representation of your codebase. It will be easier to modify and maintain it.
  • Different teams can work on different part of the codebase, without them stepping on each other toes. After all, if your high level modules have high coherence and low coupling, changing one module won't affect the others.
  • If you really did a good job, you could even use part of your codebase on a different one. This is difficult to achieve, and it shouldn't be a priority; just a nice bonus.

The Problems of the Single Responsibility Principle

SRP doesn't fit to any situation

It Provides One Solution Without Stating the Problem

Precisely defining the problem is useful to know exactly what we are speaking about. Then, it's easier to communicate about these problems, because everybody is on the same page.

The definition given to decomposition, cohesion and coupling are not ambiguous; the problems they try to solve either. A "reason to change" can be everything and anything. Worst, it's only one solution. What about the others? As De Parnas was writing:

The effectiveness of a “modularization” is dependent upon the criteria used in dividing the system into modules.

"One reason to change" is only one criteria we can use for modularization, but it's not the only one. "Three reasons to change" could be another. "These things go together because the business treat them as a unit" is another one.

It's the Wrong Abstraction

The SRP is basically trying to summarize three very important concepts in one sentence, and it does it poorly.

An abstraction is good when it hides the details you don't need. However, the SRP hide the important ones. You need to be aware of decomposition, coherence, and coupling to design your software. You need to be aware that your functions, your classes, or whatever you use to decompose your codebase need to have as much cohesion and as less coupling as possible.

Nowadays, if we have a cohesion problem, we say that it's a violation of the SRP. If we decomposed the problem poorly, we call the violation of the SRP. If you have too much coupling, you're being violent with the SRP. We lost a lot of precision here. As a result, we don't really know anymore what we are speaking about.

Making it black and white (this is a SRP violation vs this is not a SRP violation) is the wrong approach, too. Coupling and cohesion are scales, not booleans. That's why we speak about "high" and "low" cohesion, "high" and "low" coupling.

If you need to retain something from this article, it's the following: the goal is not to be 100% decoupled and 100% cohesive, it's doing our best to avoid unnecessary coupling and making our modules as cohesive as possible.

What's the alternative to SRP?

By stating precisely the problem and the possible solutions, in a very general case, it's our job then to find solutions going into the specific. Your decisions will depend on the context. By order of importance:

  1. The business model and the problem domain of your company.
  2. The outcomes the software should have.
  3. The technology used, and the reason you use them.

There's no silver bullet here. You need to look at the environment and the context you're working in, and find ways to make your design maintainable and scalable in this precise context. These are the hard questions to solve.

Asking these questions might be a good starting point:

  1. Here's the problem we're trying to solve with this software. How should we decompose this problem? What are the sub-problems?
  2. What kind of relationships these problems have? Is this problem depending on the solution of this one?
  3. After decomposing the problem, what design decisions can we make to support this decomposition? What interface should have our modules?
  4. What new challenges do we face while designing? While coding? Is it possible to represent the solution of the problems using an abstraction in a cohesive way?
  5. What could make this module more cohesive? How to call it, to show this cohesion?
  6. I see some coupling between two modules. Is this coupling necessary? Why?
  7. This part is difficult to understand. Should we isolate it in its own class or function?

Everything goes from the problems you have, often the business problems. Our software need to be useful in the real world, and the real world is messy. We'll always have these functionalities which could go into multiple classes, or modules, or whatever.

Try to make sense of it, code your solutions, and come back to it later when you'll have more knowledge about the context, to improve your design.

Striving for Preciseness

This is what this article is about: we need to be precise when we state the problem, or we won't understand how to solve it. Precise in our thoughts, in our communication, and in our codebase.

So, what did we learn in this article?

  • The Single Responsibility Principle is ambiguous and lack preciseness.
  • We should decompose the problems we're trying to solve, even before coding.
  • Coupling and cohesion are not booleans. We can't achieve total cohesion with no coupling, but we should try to maximize the first while minimizing the second.
  • We should organize our code depending on the decomposition of the problem, with well-defined interfaces respectful of the cohesion and coupling we want between our modules.

When I finally understood all of that, I was relieved: no need to ask myself if "this" or "that" has "one reason to change". I'm thinking in terms of relationships between components, in the business and therefore in my code, and I make incremental and, if possible, deferred decisions till I understand more the problems and their context.

Related Resources

Top comments (0)