Today I'd like to talk about software degradation. Software is not something you typically think about degrading, unlike other built projects. Software is just ones and zeros - there's no belts to wear out, no seals to crack, no surfaces to erode. But, in my experience software can degrade just as easily as a bridge, aircraft, or car engine, and sometimes even more quickly.
I'm not talking about hardware obsolescence - you might be able to get an application from a 5.25" floppy to run on a modern machine, if you could find a drive for it! Eventually software requirements will inevitably outpace outdated systems. I'm referring specifically to what happens to just software that is left "unattended" for a period of time.
Recently I was tasked with fixing some bugs in a product that my company had released within the past year. It is a Java application, written over the course of a year or so by a team of developers, with dependencies on other libraries and products we have developed. It has not been under active development for a few months, but has had a handful of patch releases. In other words, it's just like any other reasonably complex software product out there.
I was digging into the code and remarking to one of my colleagues that the whole thing felt "rusty". We were joking, of course, but it got me thinking about how software truly can seem to rust. Here are some reasons why I think that is, along with some remedies I'd suggest to help prevent it.
Problem: Project is no longer actively developed
This one should be fairly obvious - no software product is developed forever. Developers leave the project to work on other things, and knowledge is gradually lost.
Solution: Document everything
Again, the solution should be fairly obvious: document. If you write it down, chances are you won't have to track down the one guy who worked on this project five years ago and who probably doesn't remember anything about it anyway. And when I say everything, I mean everything. Document your classes and methods, obviously. But also document the non-obvious things. How would a new developer check out and build this project from scratch? What dependencies does it need? What design decisions were made while developing? What bugs have been identified, and which have been fixed? What does it do now, and what could it do in the future?
Problem: Dependencies go stale or change
I'm not talking about dependency issues such as the whole leftpad fiasco (which could be its own blog entry) - third party libraries are undoubtably useful and necessary to avoid repeating effort. But, let's say your application is dependent upon a handful of third-party GUI libraries, uses a certain version of Java, and communicates with another in-house product. The third-party libraries could change or be deprecated without notice. Or, you need upgrade Java versions, and something in your app won't work with the latest version of Java. Or, the in-house application API changed, and no one told you. Any of these could lead to your application being broken.
Solution: Be smart about dependency management, and run end-to-end tests
The particular solution to this problem will depend on your project's needs. You might utilize a build and dependency management system such as Maven, but be aware of the implications - you can typically peg to a specific revision of a library, but keep in mind that version might not be around forever. Consider bringing it in-house and hosting it in-house if possible. Perhaps more importantly, develop a set of end-to-end integration tests that verify the entire functioning our your software system, and actually monitor the test results! Tests are no good to anyone if they're ignored.
Problem: Fixing bugs causes more bugs
So a customer has discovered a bug, and you need to release a patch to fix the problem. You find the cause of the bug, fix it and release the patch, only to have the customer report back that now three other things are broken. Uh oh.
Solution: Test all the things
This is pretty much the reason why we write unit tests. Tests and continuous integration systems are great tools for ensuring one small change here doesn't break something else over there. Of course, sometimes you inherit legacy code that is untested. It's generally not practical to write an entire test suite around a large legacy codebase, not to mention refactor the code so it is easily tested. But, you can write tests around code that you touch. Write tests that verify the buggy behavior, fix the problem, then verify the tests pass. If you're concerned that a bug fix in one area of code might break something elsewhere, you might need to write tests around the related code or possibly refactor and attempt to reduce any tight coupling.
These are just a few of my thoughts on brittle and eroded code, and some of the ways I've tried to prevent and / or fix it. Working Effectively with Legacy Code by Michael Feathers is an excellent book that deals with these topics, among others, and I highly recommend it. What's been your experience with degraded code? What have you found to be effective when dealing with it?