I don't find it useful these days to debate about what does and doesn't constitute a single responsibility or a single reason to change. I would propose a Minimum Grief Principle in its place:
Minimum Grief Principle: code should either seek to minimize its probability of requiring changes or maximize the ease of being changed.
How's that? Shouldn't take a rocket scientist to figure out why this can help reduce maintenance costs and hopefully it shouldn't be a point of endless debate, but as with SOLID in general, it's not something to apply blindly everywhere. It's something to consider while balancing trade-offs.
As for the probability of requiring changes, that goes down with:
- Good testing (improved reliability).
- Involving only the bare minimum code required to do something specific (this can include reducing afferent couplings).
- Just making the code badass at what it does (see Make Badass Principle).
As for the difficulty of making changes, it goes up with efferent couplings. Testing introduces efferent couplings but it improves reliability. Done well, it generally does more good than harm and is totally acceptable and promoted by the Minimum Grief Principle.
Make Badass Principle: classes that are used in many places should be awesome. They should be reliable, efficient if that ties to their quality, etc.
And the Make Badass Principle is tied to Minimum Grief Principle, since badass things will find a lower probability of requiring changes than things which suck at what they do.
I would have started by pointing to the paradox mentioned above, and
then indicate that the SRP is highly dependent on the level of
granularity you want to consider and that if you take it far enough,
any class containing more than one property or one method violates it.
From an SRP standpoint a class which barely does anything would certainly have only one (sometimes zero) reasons to change:
explicit Float(float val);
float get() const;
void set(float new_val);
That practically has no reasons to change! It's better than SRP. It's ZRP!
Except I would suggest it is in blatant violation of the Make Badass Principle. It's also absolutely worthless. Something which does so little cannot hope to be badass. It has too little information (TLI). And naturally when you have something which is TLI, it cannot do anything really meaningful, not even with the information it encapsulates, so it has to leak it to the outside world in hopes that someone else will actually do something meaningful and badass. And that leakiness is okay for something which is just meant to aggregate data and nothing more, but that threshold is the difference as I see between "data" and "objects".
Of course something which is TMI is bad as well. We might put our entire software in one class. It can even just have one
run method. And someone might even argue that it now has one very broad reason to change: "This class will only need to be changed if the software needs improvement." I'm being silly, but of course we can imagine all the maintenance issues with that.
So there's a balancing act as to the granularity or coarseness of the objects you design. I often judge it by how much information you have to leak to the outside world, and how much meaningful functionality it can perform. I often find the Make Badass Principle helpful there to find the balance while combining it with the Minimum Grief Principle.