Last week I started talking about a few of the practices that go into writing solid, maintainable code. Throwing together layer upon layer of hacks in order to get an application or feature out the door more quickly can be very tempting. Often it will even appear to have been the right choice, at least for a little while. After all, if it works for you and your customer/boss gives the nod, you can always go back later and take care of those bugs you swept under the rug, right? The fact is, though, that most of the time that rug never gets looked under again. Problems are left to fester, because there’s more profit and satisfaction in developing new software than maintaining existing code. Writing code to be more easily maintained, and as a by-product need less maintenance in the first place, is a difficult skill. It requires a greater expenditure of time and an adherence to discipline that can seem overly restricting, but the time and headache that you save yourself later make it all worthwhile.
Shortcuts aren’t the only pitfall waiting to ensnare would-be code-monkeys as they hack their way through a jungle of logic. More dangerous by far is the trap that you bring with you: cleverness. See, the dirty little secret about writing computer software is that a lot of it is dead boring. Your typical programmer is an intelligent person that needs a certain level of mental challenge and stimulation in order to maintain interest in his or her job. Simply getting the job done isn’t always enough; there has to be a puzzle to solve or a new tool to explore or the simple joy of hearing your peers say, “How did you do that?!” Unfortunately, opportunities for these career-validating experiences can be few and far between, particularly when so many potential employers think the world really needs is another e-commerce website from which to sell widgets.
In an environment like that, when confronted with the dull reality of the job, many programmers will make the terrible mistake of being clever. If the end product is boring, we’ll make the implementation of that product “interesting”. Instead of using a known, existing tool that does what we need we’ll build our own from scratch. When we could get the job done simply by using known design patterns, we’ll instead opt to spend hours upon hours weaving tangled threads of logic into tortured knots. And the whole time we’ll tell ourselves all kinds of lies about how we’re just trying to make our code more elegant or optimized, when all we really want is something to sink our mental teeth into. Don’t fall into this trap. The satisfaction you get in the short term from admiring your own cleverness will tarnish very quickly in the face of the extra debugging sessions brought on by an over-complicated design and implementation. Take it from me: it’s not worth it.
One of the most common mistakes that I see when reading code, whether it was written by a novice fresh out of school, a veteran with dozens of languages under his or her belt, or even myself just five years ago, is the assumption that things will work as expected. Error codes will be left unchecked. Exceptions will be left uncaught. Return values will be left unvalidated. I suppose I can’t blame them, really. Building a complex piece of software is difficult, even when everything goes right. Taking into account every way that any given line of code can fail can be overwhelming to those not accustomed to it. Plus, since each instruction in your code runs in series like a monstrous line of falling dominoes, the slightest error in your logic can have far-reaching repercussions.
The only way to make sure that your software is stable, that it does the job that it was meant to do, is to accept and embrace its imperfection. With every line of code you write, you need to ask yourself questions. How do you expect the instruction to run? What will it do if it fails? If you give it a weird input value that it wasn’t expecting? If it throws an exception that interrupts the normal execution thread? If you lose connection to your external resources? Can you recover from a failure and keep going? How do you alert someone that a failure has occurred so that it can be tracked down later?
A lot of this comes back to one of the points that I made last week, about respecting your interfaces. Codifying all of the different pre-conditions and post-conditions of a code module will take you a long way towards understanding all of the different ways that it can fail, which should then be codified themselves. Breaking your code down into small, reusable chunks is a big help with this, as there’s a lot less that you need to understand at once in order to cement that interface. A program is far easier to write and maintain when it’s made up entirely of small black boxes, whose ins, outs, and faults are all completely understood and documented, held together by thin and simple glue.
And the thrilling conclusion…
Next week, barring a stroke of genius, will finish up this little series with the last two pearls of wisdom I have to share on the matter. I’ve been light on technical detail, I know, but one of my goals is to keep this accessible to people just starting out. Good habits are easier to learn at the start, before you have to unlearn all the bad habits you inevitably pick up. Got any bad coding habits of your own that you’ve managed to break? Tell me about them!
A note about being boring: it takes twice as much cleverness to debug code as to write it. Thus, if you’re writing code more than half as clever as you are, you’re doing something wrong. You won’t be able to debug your own code!
I want to add something about writing effective documentation, but it’s a bad habit i’m still constrained by. Documentation is easy to write. Effective documentation is critical and very difficult (for me at least)…