Over the last few month, I was reminded – twice, painfully each time – about the impacts of good vs. bad software design choices, especially the impact those choices can have downstream. Ironically, it is not only – or even mainly – the creators and primary users of the software who are impacted, but others unforeseen at design time.
Installing an Operating System
Anyone who has installed an operating system on their laptop or server – or even smartphone – is familiar with a series of questions, choices and answers, as they configure the operating system. In technology parlance, this is known as “menu-driven”.
The goal of the menu is to gather enough information from the user to get to the desired installed end-state.
In a recent project, I needed to design a process for automatic server installation of a particular operating system for a customer. This OS, too, has a menu-driven interface. However, recognizing that sometimes customers want to do so non-interactively – it hardly is efficient to set up more than one server, let alone thousands, by keying in the answers, and leads to many human errors – there is a way to pre-seed the answers in a file and give it to the installation process. The file even is called a “preseed” file.
However, the designed of the preseed system failed to understand that the purpose of the “preseed” is to get to a certain state, rather than follow a particular process. Thus, rather than the preseed delineating the state, it gives the answers to the questions.
This leads to a lot of issues:
- Sensitivity to the order of the lines in the file
- Difficult to debug problems
- Poorly documented options since there are far too many potential permutations
- Inability to test without actually running through the menu, i.e. the slow install
- A lot of wasted time
The purpose of the menu never was to create a process, it just happened to do so to get to a given state. The designers of preseed successfully automated the menu process, rather than successfully providing the desired goal, i.e. the end state.
A poor design choice led to a lot of pain.
A similar problem occurred in a different area: software design.
Software often is composed of libraries. Libraries are chunks of code with (hopefully, but unfortunately not often enough…) clearly defined entry points and exit points. Libraries serve one or both of two purposes:
- Simplify complex processes: Let’s use encryption as the most extreme example. Getting it right is very hard, getting it even slightly wrong can leak secure data, even after enormous effort. Using an encryption library allows the developer to rely on someone else’s well-proven and reviewed work.
- Reduce redundant efforts: If everyone writing a Web application has to process the request headers, it is quite wasteful for each person doing so to write code that extracts the long header string and converts it into a usable format. A simple library should do that.
In an open-source project with which I am involved, two different libraries to provide standard Web application REST interfaces are supported.
When making a change, suddenly tests started failing, but only when run in a certain order. As anyone who has been involved with software knows, tests are the bedrock upon which reliability and especially reliable changes stand.
The problem, apparently, was in how one of the libraries was implemented.
When each Web request comes in, a
request object is created. Each library in turn needed to augment that
request with its special capabilities. In doing so, it had two choices:
- Augment each
requestby adding those capabilities.
- Change the basic request template (or prototype) so that each future
requestwill gain those capabilities by default.
At first blush, the second option seems the better one. After all, why not change the
prototype so that everything just inherits the expanded capabilities? Indeed, this is just what the library creator did.
However, if the designer had thought a little longer, they would have realized that doing so will impact every other library or program that depends on the standard
This is exactly the problem that occurred here. The creator of the library could not conceive at creation time that someone might use his library along with other libraries, or perhaps more demanding code. Yet, that is exactly what happened.
The developer fell prey to the “640k out to be enough for everyone” trap. He saw his usage as representative of everyone’s, and thus cut out the ability to expand the usage far beyond what he had originally envisioned.
When designing a product, especially software, think long and hard about the impacts of your design choices. Often, at each stage, you will have the choice between two (sometimes more) options. Before picking one, spend just a few minutes asking the following questions:
- What are the restrictions of one vs. the other?
- What are the performance impacts of each?
- How hard is each to implement, and how hard will it be to change in the future (a.k.a. pay down the technical debt)?
- Who and what scenarios could be impacted beyond how I currently envision it?
If you are unsure, ask us. We would be happy to help.