Chapter 4. Improve Code by Removing It
We ascribe beauty to that which is simple; which has no superfluous parts; which exactly answers its end...
Ralph Waldo Emerson
Some of the most exciting improvements I remember making to code involved removing vast chunks of it. Let me tell you, it’s a good feeling.
You can improve a system by adding new code. You can also improve a system by removing code.
It was almost certainly the programmers’ indulging their own personal vices. Something like:
It was a fun bit of extra code, and the programmer wanted to write it. (Hint: Write code because it adds value, not because it amuses you, or you’d enjoy trying to write it.)
Someone thought it was a feature that would be needed in the future, so decided to code it now, whilst they thought about it. (Hint: That isn’t YAGNI. If you don’t need it right now, don’t write it right now.)
But it was only a small thing; not a massive “extra” feature. It was easier to just implement it now, rather than go back to the customer to see whether it was really required. (Hint: It always takes longer to write and to maintain extra code. And the customer is actually quite approachable. A small extra bit of code snowballs over time to a large piece of work that needs maintenance.)
The programmer invented extra requirements that were not documented in the story that justified the extra feature. The requirement was actually bogus. (Hint: Programmers do not set system requirements; the customer does.)
Now, we had a well-understood lean development process, very good developers, and procedural checks in place to avoid this kind of thing. And unnecessary extra code still snuck in.
That’s quite a surprise, isn’t it?
It’s Not Bad, It’s Inevitable
Even if you can avoid adding unnecessary new features, dead pieces of code will still spring up naturally during your software development. Don’t be embarrassed about it! They come from a number of unavoidable accidental sources, including:
Features are removed from an application’s user interface, but the backend support code is left in. It’s never called again. Instant code necrosis. Often it’s not removed “because we might need it in the future, and leaving it there isn’t going to hurt anyone.”
Data types or classes that are no longer being used tend to stay put in the project. It’s not easy to tell that you’re removing the last reference to a class when working in a separate part of the project. You can also render parts of a class obsolete: for example, reworking methods so a member variable is no longer needed.
Legacy product features are rarely removed. Even if your users no longer want them and will never use them again, removing product features never looks good. It would put a dent in the awesome list of tick-box features. So we incur perpetual product testing overhead for features that will never be used again.
The maintenance of code over its lifetime causes sections of a function to not be executable. Loops may never iterate because code added above them negates an invariant, or conditional code blocks are never entered. The older a codebase gets, the more of this we see. C helpfully provides the preprocessor as a rich mechanism for writing non-executable spaghetti.
Wizard-generated UI code inserts hooks that are frequently never used. If a developer accidentally double-clicks on a control, the wizard adds backend code, but the programmer never goes anywhere near the implementation. It’s more work to remove these kinds of autogenerated code blocks than to simply ignore them and pretend that they don’t exist.
Many function return values are never used. We all know that it’s morally reprehensible to ignore a function’s error code, and we would never do that, would we? But many functions are written to do something and return a result that someone might find useful. Or might not. It’s not an error code, just a small factoid. Why go through extra effort to calculate the return value, and write tests for it, if no one ever uses it?
Much “debug” code is necrotic. A lot of support code is not needed once the initial implementation has been completed. It is unsightly scaffolding that hides the beautiful architecture underneath. It’s not unusual to see reams of inactive diagnostic printouts and invariant checks, testing hook points, and the like, that will never be used again. They clutter up the code and make maintenance harder.
Does this really matter? Surely we should just accept that dead code is inevitable, and not worry about it too much if the project still works. What’s the cost of unnecessary code?
It is undeniable that unnecessary code, like any other code, requires maintenance over time. It costs time and money.
Extra code also makes it harder to learn the project, and requires extra understanding and navigating.
Classes with one million methods that may, or may not, be used are impenetrable and only encourage sloppy use rather than careful programming.
Even if you buy the fastest machine money can buy, and the best compiler toolchain, dead code will slow down your builds, making you less productive.
It is harder to refactor, simplify, or optimise your program when it is bogged down by zombie code.
Dead code won’t kill you, but it will make your life harder than it needs to be.
Remove dead code wherever possible. It gets in the way and slows you down.
Waking the Dead
How can you find dead code?
The best approach is to pay attention whilst working in the codebase. Be responsible for your actions, and ensure that you always clean up after your work. Regular code reviews do help to highlight dead code.
If you’re serious about rooting out unused code sections, there are some great code coverage tools that will show you exactly where the problems are.footnote::[You might even already have them—look at the warning options provided by your compiler.] Good IDEs, especially when used with statically typed languages, can automatically highlight unused code. For public APIs, many IDEs have a “find references” feature that can show whether a function is ever called.
To identify dead features, you can instrument your product and gather metrics on what customers actually use. This is useful for making all sorts of business decisions, rather than just identifying unused code.
There is no harm in removing dead code. Amputate it. It’s not like you’re throwing it away. Whenever you realise that you need an old feature again, it can easily be fetched from your version control system.
It is safe to remove code that you might need in the future. You can always get it back from version control.
There is a counter argument to that simple (and true) view, though: how will a new recruit know that the removed code is available in version control if they don’t know that it existed in the first place? What’s going to stop them writing their own (buggy or incomplete) version instead? This is a valid concern. But similarly, what would stop them rewriting their own version if they simply didn’t notice the code fragment was already located elsewhere?
As in previous chapters, remember to remove dead code as a single step; do not conflate it in a version control check-in that also adds functionality. Always separate your “spring cleaning” work from other development tasks. This makes the version history clearer, and also makes revivifying removed code a breeze.
Code cleanup should always be made in separate commits to functional changes.
Dead code happens in even the best codebases. The larger the project, the more dead code you’ll have. It’s not a sign of failure. But not doing something about it when you find dead code is a sign of failure. When you discover code that is not being used, or find a code path that cannot be executed, remove that unnecessary code.
When writing a new piece of code, don’t creep the specification. Don’t add “minor” features that you think are interesting, but no one has asked for. They’ll be easy enough to add later, if they are required. Even if it seems like a good idea. Don’t do it.
Questions . How can you identify “dead code” that is not run in your program? . If you temporarily remove code that is not currently required (but may be needed in the future) should you leave it commented out (so it is still visible) in the source tree, or just delete it completely (as it will be stored in the revision history)? Why? . Is the removal of legacy (unused) features always the right thing to do? Is there any inherent risk in removing sections of code? How can you determine the right time to remove unused features? . What percentage of your current project’s codebase do you think is unnecessary? Does your team have a culture of adding things they like or that they think will be useful?
Write Less Code! Talks about removing duplication at the micro level: whittling away unnecessary lines of code.
Wallowing in Filth How to navigate a route into problematic code so you can spot what needs to be removed.
Coping with Complexity Removing dead code reduces complexity in your software.
Effective Version Control Removing dead code does not mean it’s lost forever. You can retrieve it from version control if you make a mistake.
Look for dead and unnecessary code in the files you are working in. Remove it!