The ingredients for boosting the overall lifetime of software you develop include:
- Ensuring that the source code is readable. While this attribute can be somewhat subjective, there are many things you can do to ensure readability that transcend the specific programming language(s) you’re using. Readability reduces maintenance costs, and maintenance costs account for 80% or more of the cost of software during its lifetime. Reduce maintenance costs, and you reduce overall costs, making it more difficult to justify a rewrite. Consistent naming conventions, consistent indentation policy, using meaningful identifiers, and a host of other techniques will go a long way toward improving readability.
- Ensuring that the code is maintainable. In addition to readability, you should take steps to ensure that it is easy to make a change to the code without breaking something else, requiring a cascading sequence of other changes to the code to match, subtly changing the control flow because you didn’t use braces around controlled blocks of code, etc. Repeating the same code in many places is asking for trouble, if you find a problem in the repeated code that requires a change to every copy of the code. Organize the code in layers, with clear, well-defined APIs between them, so that it’s easier to deal with independent, decoupled areas of the code. Again, maintenance is a very high percentage of the overall cost. If your code is brittle when small changes are made, it will more likely be declared “legacy” sooner.
- Ensuring that the code is portable. Even if your initial project goals are OS-specific and/or hardware-specific, you never know when the code might need to be ported to another OS or another hardware platform. Writing portable code involves avoiding assumptions about details of the target platform. Parameterize these details using header files, configuration files, etc. If you must make target-specific assumptions, make use of compiler checks and asserts to ensure that the code fails to build and/or run if the assumptions are not valid on a new target platform. Of course, some projects are actually designed for a specific OS or hardware, so there’s going to be some non-portable code in there. In that situation, layer the design, so that non-portable code is isolated and accessed through a well-defined API. Then, when the code needs to be ported, it’s very clear what code needs to be changed.
- Ensuring that the code is scalable. Scalability includes choosing appropriate data structures, algorithms, parallelism techniques, etc. that will scale up well when the amount of data, number of users, etc. increase dramatically. Too often, software developers think small, and don’t initially consider what will happen when the number of inputs, the number of files, the size of a file, the number of servers, the number of users, etc. takes off vertically. These days, lack of scalability can often justify “legacy” status and a complete redesign.
- Ensuring that the code is testable. If you can’t test it, you won’t know if you’re ready to ship, or if you’ve broken something with a recent change. Design your code to be easily testable, keep test cases up-to-date, make the tests easy to run against each build, etc. Otherwise, if your code is too costly to test, it might get labeled as “legacy” sooner.
- Ensuring that the code is fault-tolerant. Bad things happen, even to good developers and to good code. Dealing gracefully with error conditions and exceptional circumstances is important. Sometimes, the right thing to do is log the error and terminate. Sometimes, the right thing to do is work around the problem and keep running. Either way, think about, design, document, implement, and test these scenarios. Software doesn’t run in an ideal world, and the days of blaming the user for a program blowing up are long gone.
- Ensuring that the code is secure. There are many things that can and should be done to make the code less vulnerable to attacks/exploits. If security isn’t built into the software up-front, it will more likely end up on the “legacy” pile sooner.
- Ensuring that the code is extensible. Adding a new small feature should not require making massive changes throughout the entire project. Using an architecture that allows for the (safe) addition of features using plug-ins, table entries, etc. goes a long way toward simplifying the process of adding new features. On the other hand, architectures that require many changes throughout the code base to a one feature are likely headed for an early “legacy” designation.
- Ensuring that the code is well-documented, inside and out. Sure, I hear the arguments that no one reads the documentation, and the documentation is always out of sync with the actual code. But the documentation should be kept in sync with the code, and needs to be available for those who will maintain the code. Poor, outdated, or non-existent documentation can lead to premature “legacy” status for the code.
- Avoiding hitching your project wagon to obscure dependencies. Choosing an oddball language, an ancient library or framework, an unpopular architecture, etc. can doom your project to early “legacy” status. Stick with mainstream, well-supported stuff. And always make sure you have contingency plans, should any things you depend on suddenly disappear, have their license terms changed, lose support, etc.
Maximizing these attributes of your project can length the overall life of your code.