1

Edit based on responses so far
I am starting with an edit so as to save future contributors from going down the same path others have.

I am only interested in contributions that stick to the exact definition of SRP I have quoted below. So no “you can think of SRP as…” type elaborations, as that takes away from the objectivity.

First, a bit of background to give more context to the question.

To keep the discussion focused, I will only be talking about classes here, not functions, or modules, or any other construct. So where you read any of these other constructs, think 'class' instead.

The definition
Let's start with how Robert C. Martin (Bob) puts it:

The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change.

He goes on to say:

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person... Why? Because we don’t want to get the COO fired because we made a change requested by the CTO.

As a side note, if anyone is to be fired here, it's the person who decided that unit tests were not required - ensuring nothing is broken following a change is exactly the 'concern' of unit-tests run on the pipeline.

Back to SRP...

I find it pragmatically impossible to always adhere to it, and unless I am mistaken, it certainly appears to be what Bob is intending we do; apply it everywhere. Sure there are some classes where you can easily implement this pattern. In fact the typical marketing done in favour of SRP would use examples like "Report generator should not also format a report" and so on... Sure. Agree. Easy. Why wouldn't you separate out those two pieces of functionality!?

But think of other very-common situations you are likely to encounter during development. For example, writing a websocket client. Here's a sample implementation: https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket?view=net-7.0
It lets you connect AND disconnect AND read AND write AND other stuff. Clearly it (along with a thousand other classes across .Net and other runtimes/frameworks) violates SRP. Bob describes change in terms of "actors", which when talking about a class, boils down to consumers of that class.

With a websocket client, there might be some parts of your program that only need to read from it, while others need it for writing. A change request from one part can have a bearing on another part, so based on the definition of SRP (I urge you to pause and read the Edit at the top), the websocket client violates SRP.
Now, you may be inclined to argue that the websocket client can be thought of as self-contained, bounded, a singular entity, or some such. However these are all concepts relating to SoC, not SRP (as per its definition), and I would urge you to refer back to my edit at the top once again.

Finally, the question:
Is there anything beyond SoC (Separation of Concern) that SRP provides? If so, it would help to provide an example, that clearly describes a problem not solvable using SoC alone. Preferably something with minimal code and realistic, so no God class or such that you really ought not to be doing anyway as a developer.

Ref: https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

Ash
  • 219

4 Answers4

12

I find it pragmatically impossible to always adhere to it, and unless I am mistaken, it certainly appears to be what Bob is intending we do; apply it everywhere.

I'm not surprised you find this pragmatically impossible, by literal definition of what pragmatism means.

Pragmatists don't do things "always", that's dogmatists. Pragmatists work on a case-by-case basis.

For example, writing a websocket client. [..] It lets you connect AND disconnect AND read AND write AND other stuff.

What is a websocket? It's a line of full-duplex communication, which entails that both sides of the connection are able to send and receive messages. Wikipedia link. This justifies why you have both a write and a read method.

Why do we connect to a socket? So that we can open up this line of communication.

Why do we disconnect a socket? Because we previously connected to it and no longer wish for it to be connected.

All of this describes a single responsibility, which we call the websocket. Everything you mention works towards a single goal: to allow full-duplex communication, which is why we created websockets in the first place.

Is there anything beyond SoC that SRP provides?

It is folly to consider these terms as scientifically precise. There is no singular objective measurement of what SRP and SOC is. It is a guideline intended to point at a bad situation and how to avoid it. It is not a provably unique solution, not a solution that is provably distinct from all other solutions.

Overlap is very common. Not just with SRP, but with all of SOLID (and similar paradigms). It's hard to point at one letter and provide an example to which the other letters don't also apply, but that is not the purpose of these principles to begin with.

I feel like you're succumbing to dogma and an underlying assumption that these labels are scientifically and objectively precise. They're not. Don't try to apply them as definitive criteria. Instead, understand what the spirit of the advice is, and apply that spirit when you are developing. Don't try to chase the dragon itself, it's not productive.

Flater
  • 58,824
5

It is a matter of perspective.

SoC is a general design principle that allows parallel development by minimizing dependencies. It is applicable to multiple methodologies.

SRP is part of a set of rules directed at object oriented design.

Where SoC says "this is how you approach anything", SRP provides a rule to test whether a class adheres to SoC in an OO context.

They do boil down to the same thing. One is looking at things from outside and the other is looking at things from within a class. The latter makes sense when you are building a class tree in an OO setting.

As OO gets old and people are no longer willing to pay money for an OO course or a book on it, you will likely see offerings of shiny new methodologies that come with their own set of acronyms. And chances are SoC will be in there somewhere as well by yet another name.

[Edit]

OK, since the OP keeps coming back to the question "What is it good for?", here are some pointers.

If it (a class) has multiple responsibilities, then

  • You may find yourself breaking multiple things when working on one thing because it would be hard to avoid impact on feature B when changing feature A in the same class.
  • Your code would be less flexible, swapping out one implementation of a feature for another would be impossible as long as that feature is tied to other features you can not afford to lose.
  • It would be hard to find where you need to be to apply a fix or change in behavior because the class name would inevitably be inapt.
  • You and your co-workers would be more likely to get in each other's way, leading to disgruntlement and merge conflicts.
  • It would make the product harder to understand, to build a map in your mind of how the parts work together. This makes it harder to find people capable and willing to work on it and thus more expensive and more time consuming to maintain.
Martin Maat
  • 18,652
4

When Martin talks about "change originating from a single person" he is not talking about runtime state changes initiated from other software components. He means requirement changes coming from different persons or departments in an organization. The kind of change when you have to modify some parts of the code.

The financial departments will have some requirements while the marketing department will have different requirements. These requirements tend to change independently, and therefore a single class should not implement requirements from multiple departments.

It means letting the separation of concerns which exist inside an organization be reflected in the software architecture.

It wouldn't apply to a low-level component like a socket library. It is not like the HR department requested the Connect method and the CFO requested the Disconnect method.

JacquesB
  • 61,955
  • 21
  • 135
  • 189
2

The concepts of Separation of Concerns, Cohesion, and the Single Responsibility Principal are all complimentary. The key is to understand that change, and more importantly enabling change without breaking other things, is the driving force behind the SRP. Ensuring your classes adhere to Separation of Concerns and are Cohesive all support this goal.

Separation of Concerns focuses on separating software into distinct sections/modules/components. The goal is modularity and code reuse.

Cohesion is an abstract measure of how well the data and methods of a class support the class's intended purpose in the system.

Robert Martin, towards the end of his blog post, elaborates on this relationship between the Single Responsibility Principal, Separation of Concerns and Cohesion:

Imagine you took your car to a mechanic in order to fix a broken electric window. He calls you the next day saying it’s all fixed. When you pick up your car, you find the window works fine; but the car won’t start. It’s not likely you will return to that mechanic because he’s clearly an idiot.

That’s how customers and managers feel when we break things they care about that they did not ask us to change.

This is the reason we do not put SQL in JSPs. This is the reason we do not generate HTML in the modules that compute results. This is the reason that business rules should not know the database schema. This is the reason we separate concerns.

Another wording for the Single Responsibility Principle is:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

If you think about this you’ll realize that this is just another way to define cohesion and coupling. We want to increase the cohesion between things that change for the same reasons, and we want to decrease the coupling between those things that change for different reasons.

The first paragraph in the quote above illustrates what I believe are his main motivations for the SRP — to change or fix something without breaking something else. He specifically mentions "separate concerns" and "cohesion" in this series of paragraphs, because you cannot create a system you can change safely without separating concerns and making sure your classes are cohesive.

Martin rephrases his own invention in a different way to emphasize the connection to change:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

The goal of SRP is to design a system that can be changed without things devolving to bug whack-a-mole. In order to do that, you need to separate your concerns to create a modular application, and create cohesive classes, which provides a balance between separating things and keeping things together.

I've always had a hunch that the Single Responsibility Principal is the mortar that holds the "Separation of Concerns" brick together with the "Cohesion" brick. These three concepts are intertwined, but accomplish different goals in application design.

To see where these two concepts can result in different designs, consider situations where enabling change is desirable, but code reuse is not.

Let's say you have a data access class connected to a RDBMS. This involves two main concerns: interacting with the database and mapping query results to objects. By just considering separation of concerns, you could justify two different classes: a mapper and a SQL gateway. Now you want to swap out a SQL database for a web service. Since the web service returns different data structures than a SQL database, changing from a SQL gateway to a web service gateway requires you to change the mapper, too.

But is this separation useful? A change in one component requires a change in another component. Do you ever need to reuse the SQL mapping in other classes? The answers to these questions depend on the system design. If you need to reuse data mappings, then placing that logic in it's own class makes sense. If you only have a single class using this data mapping, and every time you change the gateway you need to change the mapper, then you gain nothing by separating those two concerns. This is where the single responsibility principal offers additional advice that can simplify the design:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Yes, it's the same quote mentioned above. Because it's the really important part.

If the mapper changes every time you change the gateway in your data access class, the SRP says, "then don't separate it!" The idea is if you don't separate these things, then you are less likely to introduce a bug when changing one of the components.

SoC says to separate things to make them reusable. SRP says to separate things when they change for different reasons, and don't separate things when they change for the same reason so you don't accidentally introduce a bunch of bugs.