Posted On: 2024-03-04
In programming, there are many different ways to solve the same problem, but not all of them are good. Sometimes the difference is a matter of tidiness (ie. vague variable names, dead code, etc.), other times it's something more fundamental (ie. brittle design, superfluous complexity, etc.) With the proper tools the former is usually quick to resolve, but the latter can take much longer - easily doubling the amount of development time spent on a task. For a developer that's passionate about their code, it can be tempting to always spend the extra time to "do things right" - but that is not always the best choice from a practical perspective. Knowing when to invest in making good code, versus when to leave working code alone is an important skill for a developer - and in today's post I'll cover some of my strategies for making this important decision.
Good code takes time - but part of that time is simply getting the code to work: knowing what it needs to do and how to accomplish that. As such, there's inherent cost-savings in making code good when you first work on it: anyone who needs to make (major) revisions has to understand those same details (the what and the how), so getting it done "right" the first time saves everyone time.
Often, however, you'll be faced with code that works, but is rather far from being good. Perhaps it took a few tries just to get it to work, and now there's a mix of multiple different attempts strewn about. Perhaps the requirements changed while you were working on it, and the original design doesn't fit anymore. Perhaps it's someone else's mess that you're trying to clean up now.
Whatever the cause, the code needs to be reworked, and, as stated earlier, doing it "right" this time should (in theory) save everyone time. Unfortunately, the present isn't always the best time to pay the cost of good design - maybe deadlines don't permit it, maybe the requirements might change, or maybe it will be cut entirely. In those cases, it's best to focus on quick and easy ways to improve the code's clarity (ie. tidying it up, documenting how it works, etc.), but leave larger changes for future maintainers. They probably won't thank you - the code still needs to be rewritten after all - but it's better than leaving them wondering why there are a hundred lines of code that don't seem to do anything at all.
For many practical applications of software, the final result is not the direct responsibility of the code. Instead, code serves as a sort of scaffold or design platform that supports other disciplines' efforts (artwork, data analysis, etc.) When the work of that other discipline is also the responsibility of the programmer (ie. on small teams, in agile development, etc.) it can be easy to conflate the success of programming tasks with the quality of the other disciplines' output.
Avoiding this trap is essential to meaningful progress. A well-designed system will make future changes faster and easier, so the work that goes into building good code is not a waste. Even if it doesn't look finished right now, it's still fair to call complete, good code "done" when it is.
On any task of any scope and any (apparent) importance, there is always one simple truth: it can always be cut. No matter what you're working on, or how essential it is to the project at large, there is always the possibility that it may be thrown away. Design the systems and budget your time based on what makes sense with what you know, but don't assume that the code you write will exist in perpetuity.
A deadline is an arbitrary scheduling construct that organizations invent to make themselves (or others) behave in certain ways. I find this framing is particularly useful for programming: programming work is notoriously hard to estimate, leading to deadlines that often feel completely divorced from the actual time costs of the work.
The specific benefit that deadlines provide programmers is that they discretize large chunks of time. It can be difficult to reason about large, uncertain time costs (ie. 5-25 hours to improve code), so it can be helpful to group tasks together into a shared deadline (ie. if you need 5-25 hours to improve the code in one area, but have four other 2-10 hour tasks remaining, you'll want to plan for the possibility that they won't all get done by a 40-hour deadline.)
Returning to the topic of reworking existing code, there's one particularly challenging cause of working (but not good) code: simply being wrong about what would be good in the first place. You may have the best intentions, discipline, and skills, but for whatever reason, a design that looks good in theory can always fall apart in practice.
When weighing the costs and benefits of spending time to "do it right (this time)", it's important to keep this possibility in mind. Detecting a failing design early can save an significant amount of time (it's one form of failing faster), but regardless of when you detect it, you will always have a choice: to change approach and commit more time to the effort, or to go back to the working (non-good) code. The Sunk Cost Fallacy can make it difficult to choose the latter, but sometimes it's the best choice for the project.
These are just a few things that I weigh in my mind when assessing how best to spend my time. The list is something I've been (unconsciously) building over the years, and even now I am finding new things that help me make better decisions. I hope my sharing these strategies is useful for others as well - particularly those who struggle to strike a balance between code quality and making progress.