How to Make Code Age Well
日本語はこちら。
Anyone who has been working in the industry long enough has seen code that has not "aged well".
What do I mean by "aged well"?
I'm referring to old code that is becoming increasingly difficult to maintain and extend. Some real life examples from my experience include:
Having to "compile an old version of Ruby" to run a legacy application on new hardware that the official Ruby does not support anymore.
Having to install a special font to write a particular language that is no longer being developed (Yes, such languages exist).
Keep an old Solaris machine running under your desk to run a legacy program.
As much as these circumstances are not ideal, they are not uncommon and it's sometimes unavoidable.
However, I feel there are small things that the original engineer(s) could have done to make it easier for future engineers to maintain the code or at least keep any changes to a minimum. In this post, I will discuss a few of these things that I have found to be helpful.
1. Try to lock your dependencies
In this day and age, many software projects have dependencies and these dependencies can cause huge pains in terms of maintenance. A particular library may suddenly disappear from GitHub, the API they expose may change, etc. The infrastructure you rely on (e.g. OS) can also be considered a dependency. An OS update could cause your software to become incompatible.
This is why it's important to try to lock your dependencies. For example, you could
Use containers (e.g. Docker) to lock the OS and library dependencies.
Vendor your dependencies to lock the library dependencies.
Use a dependency manager to lock the library dependencies.
These points are not mutually exclusive, rather they could and probably should be done together.
I once had to build an ETL system that leveraged an opensource software built on JRuby. The prerequisite for the software to run were quite strict. It required a particular version of JVM and some particular versions of Gems (Ruby libraries). Upgrade any one of them and the software would break.
To avoid this breakage, we used a combination of the above points. For containers, we used Docker. We used an image that had the required JVM version pre-installed and wrote a Dockerfile that installed the required Gems which was used to generate the final image. All the ETL instances would run this image. It's been in production for over 3 years now and we have not had to touch the image.
By locking your dependencies, you can avoid sudden breakages due to poorly configured dependencies or sudden infrastructure changes.
2. Make it configurable
Everyone knows that any software will eventually need to be changed and many of the changes require you to touch the code. As all software engineers know, changing the code comes with risks. The more you change, the more likely you are to introduce bugs. Thus, it is important that we limit the amount of code changes we make to the bare minimum.
Making the software configurable is one way to achieve this. Most major frameworks (be it server-side, frontend or mobile apps) include some kind of way to set configurations like database connections, hostname, deploy environments (production, staging, etc) but I recommend going one step further and include things that are ought to change. Let me give an example here too.
The previously mentioned ETL system had an additional requirement of supporting dropping of certain columns before loading into the destination (due to PII restrictions). We could have easily hardcoded the column names to drop. But it felt highly likely that we would have new columns to drop in the future. We instead "externalized" the list of column names in a separate file and made the software read that file prior to dropping.
The benefit should be obvious. We didn’t not need to touch the code to add new columns, it required a mere text file update.
This is what I mean by making things configurable. Any changes you can anticipate, you should consider making them configurable by externalizing them instead of baking them into the code.
The more you do, the better it ages
Hope you liked this idea of "making code age well". The more effort you spend, I'm pretty sure it will make the code age well and save you a lot of time down the line.
I can only hope that you don't end up having to start compiling old versions of Ruby 😄