Writing Reliable Software with Less Code
Author | Kevin Nygaard |
Published | |
Bugs are undesirable program behaviors, but precisely extracting them is difficult in non-trivial projects. Keeping programs small, straightforward, and self-contained reduces the chance of bugs and increases the ease of their discovery and removal.
Contents
1 What are Bugs?
In computer programs, bugs are unintended or undesired behaviors which ultimately manifest from two sources: misunderstandings and typos. For example, sorting a list ten times in a row is a bug – once is sufficient. Similarly, reversing the sort order of a list by accidentally typing a greater-than sign rather than a less-than sign is also a bug.1
For a small program that only sorts lists, ten identical lines and an inverted output are obvious mistakes. However, for a large program spread across many different teams, modules, files, and functions, such mistakes easily go unnoticed. The ten sorts may be in ten different files, slowly accreted over time as the project evolved. Similarly, the inversion may have been "fixed" by passing different arguments to the sort function, rather than fixing the typo in the implementation itself.
Such bugs reduce performance and reduce code quality – bad for users, and bad for developers. Across millions of users, a wasted millisecond on a frequent activity adds up to days of needlessly lost time.2 And a messy codebase breeds lethargy and spirals into unmaintainability.3 But it gets worse – bugs turn into features.
2 It's a Feature
Unless a bug is ruthlessly squashed at its inception, it quickly becomes one with the program. And despite good intentions, "fixing" a long-standing "bug" can result in more bugs. For example, a programmer discovers the ten times sorting bug, and after careful inspection, factors out the repeated sorts to a single location. For the sort ordering bug, the programmer discovers widespread use of the workaround and so prudently creates a new function with the fix and deprecates the old one. On paper, these changes are sound; but in reality, the code breaks.
The sort algorithm was unstable, permuting equally ranked items in an unpredictable, yet deterministic, manner. Others unknowingly depended on this behavior for a particular ordering of a given set of inputs: an internal data structure, user data saved on disk, etc. Now with only a single sort, the same list orders differently than before, causing problems.4
For the typo, creating a new function and deprecating the old one creates future maintenance problems: now there are two code paths, subtly different, and both are in use. Unbroken code using the deprecated path is forced to change either now, anticipating the eventual removal, or later, when the function is actually removed. And if the deprecated function is never removed, the codebase never shrinks – it just doubles the amount of code to maintain.
While the programmer plays this Whack-A-Mole game, even more insidious bugs creep forth. Some of the modules were multi-threaded, and the extra sorts prevented the compiler from reorganizing certain memory accesses. With the additional delay missing and the optimizer unconstrained, the code now subtly fails one in a thousand times, dependent on system load and thread scheduling.5
Fixing one bug uncovered many others, but were they really bugs? The program was working (albeit inefficient and confusing), but now the "improved" program is markedly worse. Arguably, fixing the original bug was the bug, by definition. It was so inextricably woven into the program, it became part of the specification. Untangling and extracting these unwanted dependencies is difficult, but when impossible, the bug behavior must be, in a twisted sense of fate, reverse engineered and reproduced.
3 Less Code, Less Problems
Because of the metamorphic nature of bugs, assume the pessimistic view that everything is a bug.6 Each bug is influenced by every other bug in the program: an exponential relationship. In this view, bug reduction is simple: use less code. Removing a single line of code not only removes a bug, but also exponentially reduces bug complexity. Therefore, the smallest and simplest program has the fewest bugs.
Notice the solution is use less code, not write less code. Deferring to libraries and frameworks introduces significant amounts of code, even if only a single function is used.7 That single "bug" imported into the program depends on every other "bug" in the library. The operating system, essentially a glorified library, introduces tremendous amounts of code.8 This also applies to things surrounding the program itself, such as compilers, code generators, and programming environments – they're all bugs.
However, once a program is released, the solution moves from use less code to change less code. As show earlier, distinguishing the "good" bugs from the "bad" bugs is difficult; a single change can have rippling effects, and therefore should be minimized. For the same reason, freeze development tools and environments – update nothing. With minimal changes, the program becomes consistent and reliable over time.
Additional features, the crutch of marketing and the drug of consumers (and programmers!), must be scrupulously evaluated. They are expensive, even the "easy" ones; they add code (ie bugs) and exponentially increase complexity (ie more bugs).9 Having a "take one leave one" policy helps keep features from growing indefinitely, as does imposing hard limits on code size and memory usage. Ultimately, features trade off with program stability and reliability; users and designers decide which is more important for the product.
Thanks for reading this brief article on improving software quality. I hope it was helpful. Developing robust programs is difficult, but possible by reducing dependencies, keeping code small and tight, and annihilating bugs on sight. Comments and feedback are welcome via email. ✚
Footnotes
- ↑ A typo caught by the compiler is not a bug, it is a prevented bug. For this reason, enable all checks in the compiler and turn them into errors so they cannot be ignored. Disable checks you find overly pedantic or unhelpful, but also clearly document precisely why you did so.
- ↑ The pointless waste of everyone's time due to programmer laziness cannot be overemphasized. In Eric Raymond's The Art of Unix Programming, he lists the Rule of Economy as: "programmer time is expensive; conserve it in preference to machine time." However, he fails to recognize that machine time is programmer time. While the computer is spinning on poor code, who is waiting on the result? He also eschews "reinventing the wheel," exhorting programmers to spend their time solving new problems. However, this implies that in the span of a century, we already have solutions as perfect as the wheel. See chapters 1 and 16.
- ↑ In Dave Thomas's and Andy Hunt's The Pragmatic Programmer, they call this state of neglect "broken windows." See Topic 3.
- ↑ Unstable sorting algorithms are not bugs; they are fast and efficient tools with specific behaviors. Like any tool, understanding its capabilities and limitations is key to effective use.
- ↑ Underspecified memory accesses are bugs and some of the hardest bugs to find. But particularly in multi-threaded applications, technically incorrect code can work by dumb luck, or rendered probabilistically correct by system dynamics. Such code is a time bomb, a bug waiting to happen. But if the code never changes, how would you know there's a problem unless you went looking for one?
- ↑ The optimistic view, ie nothing is a bug, is counterproductive. If bugs don't exist, then programs are perfect and flawless; adding more features only makes programs even better. With the pessimistic view, programs don't get better, they just get less worse.
- ↑ Libraries and frameworks over promise and under deliver. Supposedly they save time by outsourcing domain specific knowledge, but when there's a problem, the programmer ends up acquiring the knowledge anyway debugging and digging through library code. Additionally, (dynamic) libraries waste memory on code that never runs, are impervious to link-time optimizations, and load slowly at runtime, that is, if they even exist in the face of OS misconfigurations and upgrades.
- ↑ In the 1980s, programs were self-contained and self-booting; they were responsible for configuring the hardware instead of an always-running operating system. Everything fit on a disk holding less than a megabyte: the drivers, a minimal operating system, and the program to run. For comparison, Google's homepage uses twice that size – a third of which is JavaScript – for a search box and a couple of buttons. The complexity of modern hardware and peripherals has hampered self-booting software, but it still exists: eg MemTest86, Linux Live CDs, Darik's Boot and Nuke (DBAN), etc. This technique leads to sustainable, high-quality software with minimal bugs.
- ↑ In Steve Maguire's Writing Solid Code, he also agrees with fixing bugs quickly and selecting features warily. See chapter 8.
See Also
Further Resources
- Maguire, Steve. Writing Solid Code. 1993.
- Raymond, Eric S. The Art of Unix Programming. 2003.
- Thomas, Dave et al. The Pragmatic Programmer. 2020.