Kyle Sletten

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.