There is no clear answer to that. Although the question is narrow, the explanations aren't.
For me, it is something like Occam's Razor if you want to. It is an ideal where I try to measure my current code against. It is hard to nail it down in plain and simple words. Another metaphor would be »one topic« which is as abstract, i.e. hard to grasp, as »single responsibility«. A third descripton would be »dealing with one level of abstraction«.
What does that mean practically?
Lately I use a style of coding which consists mostly of two phases:
Phase I is best described as creative chaos. In this phase I write down code as thoughts are flowing - i.e. raw and ugly.
Phase II is the complete opposite. It is like cleaning up after a hurricane. This takes the most work and discipline. And then I look at the code from a designer's perspective.
I am working mostly in Python now, which allows me to think of objects and classes later. First Phase I - I write only functions and spread them almost by random in different modules. In Phase II, after I got things going, I have a closer look at what module deals with which part of the solution. And while skimming through the modules, topics are emergent to me. Some functions are thematically related. These are good candidates for classes. And after I turned functions into classes - which is nearly done with indentation and adding self
to the parameter list in python ;) - I use SRP
like Occam's Razor to strip out functionality to other modules and classes.
A current example may be writing small export functionality the other day.
There was the need for csv, excel and combined excel sheets in a zip.
The plain functionality was each done in three views (=functions).
Each function used a common method for determining filters and a second method to retrieve the data. Then in each function the preparation of the export took place and was delivered as a Response from the server.
There were too many levels of abstraction mixed up:
I) dealing with incoming/outgoing request/response
II) determining filters
III) retrieving data
IV) transformation of data
The easy step was to use one abstraction (exporter
) to deal with the layers II-IV in a first step.
The only remain was the topic dealing with requests/responses.
At the same level of abstraction is extracting request parameters which is okay. So I had for this view one "responsibility".
Second, I had to break up the exporter, which as we saw consisted of at least three other layers of abstraction.
Determining filter criteria and actual retrival are nearly on the same level of abstraction (the filters are needed to get the right subset of the data). These levels were put into something like an data access layer.
In the next step I broke the actual export mechanisms apart: Where writing to a temporal file was needed, I broke that into two "responsibilities": one for the actual writing of the data to disk and another part which dealt with the actual format.
Along the forming of the classes and modules, things became clearer, what belonged where. And always the latent question, whether the class does too much.
How do you determine which responsibilities each class should have, and how do you define a responsibility in the context of SRP?
It is hard to give a recipe to follow. Of course I could repeat the cryptic »one level of abstraction« - rule if that helps.
Mostly for me it is a kind of "artistic intuition" which leads to the current design; I model code like an artist may sculpt clay or does painting.
Imagine me as a Coding Bob Ross ;)