SRP (Single Responsibility Principle) is something I hear a lot of developers agree is a good thing, but when I read their code, they violate it without realising, or don't see the use in their particular case.
A particularly prominent example I find in our code bases is Permissioning and Caching. These two requirements can often slip into classes slowly - especially if requirements are not clear, or change as the task progresses. A slightly contrived example is this:
This class is fairly small, but it is already showing the symptoms of doing too many things; it is dealing with caching, as well as posting jobs. While this is not a major problem at the moment, it is also easier to nip the problem in the bud - before a load of new requirements/changes arrive and complicate things.
We start off by changing our class to take it's dependencies in via constructor parameters (Dependency Injection, the 'D' in SOLID):
So the usage of the JobPostingService goes from this:
Next, we take the JobWebService class and extract & implement an interface of it's methods:
And finally, create a new class which only deals with caching the results of a JobService, by wrapping calls to another instance:
This class passes all Post() calls to the other implementation, but caches the results of calls to GetLiveJobs(), and we have added a time-out as an optional constructor parameter. This wrapping calls to another implementation is called The Decorator Pattern.
As the JobPostingService class no longer has to cache the results of calls to JobService itself, we can delete all the caching related code:
And our usage changes again, from this:
We have now successfully extracted all the various pieces of functionality into separate classes, which has gained us the ability to test individual features (caching can be tested with a fake IJobService and checked to see when calls go through to the service), and the ability to adapt more easily to new requirements. Talking of which...
New Requirement: The third party webservice is not always available, allow use of a fallback webservice.
Now you could go and modify the JobPostingService class to have a second webservice parameter:
But what happens when a third service is added? and a fourth? Surely there is another way?
As luck would have it, we can use the IJobService interface to create a single class which handles all the logic for switching between the two services:
This class takes in a number of IJobServices and will try each one in turn to post jobs, and when listing jobs, gets the results from all services. In the same manner as the CachedJobService, we have a single class which can easily be tested without effecting any of the other functionality.
The really interesting point comes when we decide when to use caching? do you cache each service passed to the FailoverJobService:
Or do you cache the FailoverJobService itself:
Hopefully this article has explained 1/5th (maybe a little more, we did do Dependency Injection after all!) of the SOLID principles, and how it can be useful to keep your code as small and modular as possible.