Apprenticing at a place that taught working developers how to get better meant that I learned a bunch of high level software ideas very early in my career. I had no idea how to make an object closed to modification but open for extension (the “Open-closed principle”) but the “Single-Responsibility Principle” (SRP) was one of the few that I could implement. So I kept it around and, like a trusted friend, it has served me well.
I’m not sure when it occurred to me that this principle is fractal in nature. Fractals, of which the Mandelbrot set is an example, have the curious property that it is hard to tell what zoom level one is viewing the fractal at. I could magnify the Mandelbrot set all I want and still see the same type of structures at every level. I think of SRP like that.
Whatever level you are at: Line, method/function, class/namespace, layer, or application, the SRP still applies. Of course a line of code should have a single purpose and extending that idea to a method isn’t too hard a leap but that doesn’t mean it’s easy to maintain. Methods tend to accumulate exceptions, branches, and additional responsibilities over time. This makes the code harder to reason about and creates or hides bugs (in addition to slowing down development). Developing is a constant battle to stop behavior from leaking all over the place but concepts don’t just leak out of methods, they infect layers too. I’ve been in a lot of controllers that know way too much about the models they use and views they prepare. The “Model View Controller” (MVC) pattern is very popular but the boundaries tend to erode and, before you know it, a ‘simple’ change involves changes to all three layers and invariably one misses something in the one controller for that special case and we have the birth of a nasty bug. At this point it becomes obvious that some sort of refactoring is needed but the concept has seeped so far out of its bounds that this would be a large change… and time is always scarce. Once SRP is broken, it’s very hard to get back.
The whole (micro) services movement can be thought of as an application of SRP to the project level. A good application should only do one set of very connected things. When you need to start watermarking the PDFs that your application creates, perhaps you should consider a watermarking service. Or is that over-complication for a small part of the app? This is a crux of all software principles: Is the added modularity worth the indirection? How does one know if watermarking will become super popular with customers and demand lots of features and complexity? Well you don’t, so start with the minimum needed separation: A nice class/namespace that encapsulates all the watermarking functionality. If these borders are enforced then, if this functionality takes off, pulling out the watermarking section into a service will be much easier to accomplish.
One of the problems with software movements is that they tend to get over-applied. Facebook needs many separate internal applications (micro/macro-services) but an average enterprise application may not. Every time you follow SRP, you are separating functionality and creating an indirection that another future developer will have to reason through. Really any attempt at modularity must consider how to keep the code traceable and reasonable when introducing a new method/class/layer/application for the sake of keeping different things separate and similar concepts in the same place.