Over the last few years, my experience of being a software dev has changed massively. I've not changed the language I code in, I've not changed what I'm trying to achieve, the change has purely been a move from coding on my own, to coding as part of a team.
Previously, my number one concern was "Does this code produce the right end result". Today, I balance this with the need for clean and reliable code. Now that I work as part of a team, I see that code needs to do more than just work. It needs to work reliably, it needs to be resilient in the face of unanticipated change, and it needs to be trustworthy in the eyes of my colleagues.
This should be the first of a couple of blog posts looking at different ways I'm striving to write resilient code. My four planned posts are:
- Single Responsibility (this one)
- Command-Query Separation
- Tell Don't Ask
- The Law of Demeter
If anyone can suggest anything to add to this list (Open-Closed principle and Design By Contracts are on the edge of my mind somewhere), please let me know in the comments below.
So now I strive to make my code resilient. Resilient code can mean different things to different people, but I believe it all boils down to restricting and making explicit the effect on your system of changing any given piece of code. When I come back to a piece of code, I should be able to quickly determine what happens if I change it or remove it.
The Single Responsibility principle is one of the simplest and most commonly used techniques to ensure changes to your code are contained. Simply put, a class or method should do one thing and one thing only; it should encapsulate that responsibility completely and cleanly. The consequence of this is that it should have only one reason to change. (Incidentally, apologies if I'm starting to sound like @UncleBobMartin here, he did come up with this idea!)
But why? What's the problem if we have a single method that does two things? Doesn't that lead to APIs with fewer methods that are easier to use?
A Worst Case Scenario
Let's take a contrived example - a UserRepository class inside a fictional payroll application. I have a method called "GetUserListOrderedBySurname" that is responsible for returning that I use to populate a particular drop-down list in my application. The user interface changes and the drop-down list is removed, I find the only use of this GetUserListBySurname method was to populate this drop-down list, so it too is removed.
The next month I find out no-one has been paid because as well as getting my users, the method was also calculating their DaysWorked field and writing this back to the database.
Of course you'd hope that testing would pick up this sort of thing, but it illustrates how when a piece of code does more than one thing, you're asking for trouble.
What To Aim For
Like anything as abstract as a "coding principle", it can be hard to know what you're aiming for. Here are my top 7 signs that you're in the right area.
- No God Classes - You don't have any "God classes" in your application - the ones you open and know you've got a ton of scrolling to do.
- Short Methods - If a method doesn't fit on a single screen, it's too long. A method, just like a class, should do one thing only. Exception handling is one thing. Building a collection is one thing. Iterating a collection is one thing. You get the idea.
- Few Dependencies - Each class in your application has a controlled, small number of dependencies - keep an eye on your "using" or "includes" statements, if you're doing dependency injection properly you can also clearly see this in your constructor arguments.
- No Flotsam and Jetsam Namespaces - You don't have any namespaces that have become dumping grounds for random bits of code that aren't at all coherent. "OurToolHere.Crosscutting.Utilities" assembly, I'm looking at you!
- Minimal Use of Regions - You don't use regions in your production code except to de-emphasise cross-cutting concerns, such as logging and argument validation.
- One(ish) Constructor Per Class - If you have more than one or two constructors on a class, this may be a bad sign.
- Excessive Use of Method Overloads - Similar to the constructors thing, too many method overloads can indicate a method trying to be too many things to too many consumers. I'm finding I use overloads less and less.
Class-Level Single Responsibility
The most commonly discussed application of single responsibility seems (to me at least) to be around classes. A recent example that comes to mind is the division between a CreditCard class that holds information relating to a credit card, and a CreditCardValidator class that ;validates a CreditCard class.
It's tempting to merge these two responsibilities - why shouldn't a CreditCard class be able to validate itself? But in the implementation, some questions came out that seemed better answered by splitting these responsibilities:
- When do you validate a CreditCard class? Only when you first create the card, every time a modification is made, when it's persisted to the database, etc?
- What feedback should a validator give?
- How closely tied to your UI is your validator, to give the appropriate feedback to the user?
- Can a card have different validation requirements connected to the state of other parts of the system, e.g. whether or not it's already been transacted against?
Remember, a class should only have one reason to change - a CreditCard class should only need to change when the way we want to store credit card information changes. If a bank decides to introduce new valid card number prefixes, this is a completely different reason to change, and should not impact on the storing of card details.
Service Oriented Architecture
So we've talked about the idea that a piece of code should do only one thing. This same idea can be applied right through the software development process, from the application level, the package or namespace level, and of course right down to the class and method level we've already seen.
At the application Level, I suspect we should be relating the idea of "Single Responsibility" with that of "Service Oriented Architecture". A well designed application should arise from the requirements of a single user group. You don't want your managers expecting stats from the same application that your end users will be using, do you? As soon as you have a need to share functionality between applications, you can use Service Oriented Architecture to extract out new service-based-applications that can be independently modified, deployed, tested, etc.
A Final Disclaimer
One final word of caution - none of these "coding principles" can be doggedly pursued to the ends of the earth, there are trade-offs that need to be made all the time. Sometimes you need to just use your common sense, and avoid the temptation to sprout that one single-line method into a new class just to satisfy "single responsibility". If applying any of these principles to a given piece of code produces dumb results, maybe there's something wrong somewhere else in your code?
That's all for now folks, take care.