How to Write Deprecated Code

10 Jul 2013

Or "Write code like it's going out of style"

Software requirements change, it's just a fact of life. A project is either changing, or going stale. In order to deal with this these changes, many people have come up with systems that can help developers to deal with change. We are constantly trying to write a system that will not only satisfy the current requirements, but also give us a leg up on the next batch of changes that is already in the pipeline. Conventional wisdom (read: hubris) makes us believe that we can identify future pain points and program accordingly. We tend to believe that while some details may change, the vast majority of the code will remain the same through new incarnations and that we will reap the rewards of our foresight. Unfortunately, this is the wrong approach to take when writing software, and new use cases will stretch our code to the point of breaking.

Anticipating Change

One of the hardest things to realize (and admit) when we are writing software is that we have no idea what will change. We may understand some of the really technical parts, like what would need to change to move from one data store to another (this probably won't happen often), we will often be surprised at how difficult it is to pin which assumption product will pull out from under us next (this will definitely happen often). The reason for this is that the application domain extremely complex. As developers, we probably like programming and we make the mistake of looking at the code as our primary concern. To be fair, we need to recognize that the complexities of the domain are large and we need to fit our code to them and not vice-versa. Unless you are a domain expert, you are going to need to give this idea up.

This does not mean, however, that it is impossible to anticipate change, just that it is nearly impossible for us to anticipate change. One of the major problems when working with people from other fields is that you tend to think whatever you're doing is most important. In order to build a successful product, we have to be interested and involved in the well-being of other people in the business and our customers. If you have your finger on the pulse of your users, you can simply ask and find out where in the application change is likely to occur and how you can design to respond to those demands.

Preparing for the Inevitable

So, if our requirements are going to change, and we are unlikely to know how it will happen, how do we write code that we can respond gracefully to changes? The answer is always write code with a plan of how you could deprecate part or all of it. If you always assume that it will be cost effective to try and save your code, it always will be because the difficulty in extracting out the fundamental components will be prohibitive. This also has the odd effect of making you feel proud as a developer because of the longevity of your system, whether or not it has blocked the adoption of a more effective tool in the meantime.

Invalidating Assumptions

We need to realize that we are going to make incorrect assumptions. I have seen a system where all primary keys are assumed to be a single integer (there are valid reasons for them not to be). While most code will not make assumptions like that, we all write software that enforces things like required fields and other things that could easily be changed in the future for some legitimate business reason. This doesn't mean that we can't make assumptions, because that would mean we couldn't really do much of anything, but it does mean that we need to own the fact that it might all come crumbling down, and you need to plan your code accordingly so you don't get crushed.

Fork Like There's No Tomorrow

Everybody knows that you shouldn't copy and paste code, but most people don't realize that it's not an absolute law of software. Sometimes you will need to add some functionality to a system in some but not all of its use-cases. You'll have to resist the urge to add yet another parameter or field and sprinkle conditional branches through the code to make the same code behave differently in different cases.

We need to come to terms with the fact that even if we created an object that could service the entire project, it would be so complex and unruly that our maintainability will have been forfeit. While I do not recommend wanton copying and pasting of code, it will almost always be better to refactor the commonalities and have two functions (plus helpers). Combined with static analysis and dead code removal, this offers a much better chance at success than modifying existing code.

Opt-In not Opt-Out

We should always assume that any for any code we write, there will be a perfectly legitimate reason for opting out of our system. We need to provide a way for our code to be reused in small, useful chunks without pulling extra dependencies. The best way to do this is to try to write code that could work in as many different situations as possible, and to use types from the standard library as parameters and return types instead of crafting our own. Even if you are writing your own list or map or bloom filter for whatever reason, if there is a standard interface, use it.

Make Dead Code Easy to Spot

Static analyzers are a powerful tool and can help us as developers to see things that we don't have the time or the memory to see on our own. Even if you are using one, if your hand-rolled dependency injector doesn't have tooling support, you are going to have a difficult time realizing when code has died or proving that it has. While configurations are fine, there is nothing like a hard-coded reference to ensure your type doesn't get deleted before it's been retired.