This article is about software design, and makes the case that flexibility for flexibility sake should never be your goal. There is a very delicate balance between design and implementation in order to provide both usability and capability when it comes to software. Flexibility is often held up as a axiom, but flexibility should be provided only to the extend that it is actually needed by the end user.
The design principle “simple is better” or “less is more” is essential to good software simply because it is so easy for programmers to make arbitrarily elaborate designs. The software realm has no physicality to it, no forces to limit overly elaborate design. If we were to visualize most software programs as a physical building, they would be miles tall, some floors the size of a football field, but the entire thing balancing on a pin point. It has sections of other buildings design for other purposes simply copied and grafted onto the outside. Elaborate complicated Rube-Goldberg-esque contraption continues to work because nothing ever “wears out” forcing the design to be simpler. However, the one thing dooms overly complex software designs is maintainability. Software that is poorly designed becomes difficult to migrate to new platforms, and eventually becomes so slow to port, so slow to meet customers changing requirements, that the software eventually is dropped and replaced by something newer.
Keeping software simple in order to be maintainable requires following a couple rules:
- Use a well known and documented programming language. The more programmers that know the language, the less costly it will be to maintain.
- Don’t invent any new languages. Programmers love to make small scripting language that string together commands so that someone else can change the sequence of commands “without programming”. What they miss is that this new script is itself a programming language — but a particularly poorly supported one.
- Remove outdated functionality. For example: if you program has special support for “floppy disks” remove it, because people do not use floppy disks any more. Make sure there is a manual work around, but refuse to maintain the outdated functionality ”even if it still works.”
- Remove commented out lines. This is really easy: a commented line is a line of code that no longer executes, but is still present in the source files. It is not actually part of the program, but it complicates the source making it harder for a programmer to read and maintain. If it is not needed, it should be completely removed.
- Design principles like encapsulation are assumed here: all the principles in the “Design Patterns” book, but these patterns must be applied judiciously in order to keep the design simple. Rote implementation of such rules can lead to very elaborate and unnecessarily complicated approaches.
The biggest source of complexity is “Flexibility.” From a software programmers point of view: more flexibility is always desirable. Programmers routinely encounter situations where a particular capability was not flexible enough, and required a significant redesign. This drives programmers to long for enough flexibility to “meet any possible future change in requirements.
Here is a physical analogy: Imagine that a car designed with a steering wheel that could be installed into the back seat of a car, as well as the normal location up front. From the designer’s point of view, this is really great because there will be no difficulty in case someone wants to have a car with a steering wheel in the back seat. Such a requirement might exist in the future, so from the designer’s perspective it seems like a prudent thing to go ahead an design the steering so that it might be installed in the back seat as well. Furthermore this mechanism might be designed to install multiple steering units, for example two steering wheels in the front, or a steering wheel in the front as well as a steering wheel in the back. Who has not experienced the situation while driving that you wanted someone else to take over for a moment driving? Clearly, having multiple steering wheels would be better, right?
The fallacy in this is that nobody today actually buys cars with a steering wheel in the back, or with multiple steering wheels. The fact is that a mechanism that could be installed in the back, or multiply installed, would involve more parts, and would be costlier to build. This would increase the cost of the car, and make it uncompetitive. It is fairly easy to ask someone how much they would pay for a second steering wheel, and the answer is going to be pretty close to zero (and possibly negative). The physicality of the car eliminates that additional complexity in the design, but not so in software. After software is designed, the parts are free and have no weight or cost after that. It is nearly impossible for anyone to see what is actually being used as parts for the software, so many many elaborate and needlessly complex things are part of a typical software product.
I have been in many design meetings where unrealistic hypothetical situations drove design decisions. They will say things like:
- What if someone wants a steering wheel in the back seat? They should be able to. or
- What if someone wants to position the steering wheel so that the driver is facing backward? They should be able to.
These positions seem ridiculous because we all have a lot of familiarity with cars, but these hypothetical questions are hard to answer in the software realm. These hypothetical questions might be:
- What if someone want to make a web service request using email? They should be able to. and
- What if someone wants to make a web service request to a machine that does not support TCP/IP? They should be able to
It is much harder to recognize these latter position as bogus.
My answer to this is: design only to the requirements that actual customers have asked for. Never embellish with additional flexibility for eventualities you can’t be sure exist.
To put this plainly: design the best car you can with a single steering wheel. Have a customer sit in the mock-up, and position the wheel in the best possible position. Make the driving of the car (which is a solitary task) as effective as possible. If a customer shows up that requests a steering wheel in the back, and shows you some money, ONLY THEN should you extend the design to enable this. Agile software methodology calls this YAGNI: you aren’t gonna need it.
In software, a design is very easy to extend, but very very difficult to trim unnecessary parts from. If you are making a car, there is a long and costly process between the design and the factory that produces cars, so if you think you might need a “turbo-charger” on the car in the future, it is a good idea to think about it ahead of time, so that a “turbo charger” can be added without redesigning the car. If there is a 10% chance that a car model will need a turbo charger, and it will cost $10M to redesign the production line later to accommodate the part, you can easily see that it would be worth investing to design the turbo charger up front, if that design costs less than $1M (which it most probably does).
Software is different: in many ways to can change (by extension) the design at any time, and there is no additional cost. Another way to say this is: it cost the same amount to design something to meet the requirement after the requirement exist, as it is to meet the requirement before it exists. If you make a design for a requirement that has a 10% chance of appearing, then you are effectively wasting 90% of that effort. If would be far smarter to wait, and implement the design after the requirement appears, because 9 times out of ten it will not appear, and if that is the case you save effort. On that 1 time out of 10 that it does appear, then there is no significant additional cost. For example, a capability to import a file format might cost $50K to implement up front, and maybe $52K to implement later. Since there are 9 chances out of ten that you don’t need it, there is no reason to implement it up front.
What I am trying to say here is that our normal intuition about the value of designing up front is wrong most of the time when it comes to software because it is not like a physical production process.
On many many programming projects I have seen programmers implement flexibility just in case someone wants to change something. In 99% of the cases, that flexibility is NEVER USED. It often can’t be used because that flexibility has never been tested. That is the source of the waste: the programmer is designing for a flexibility which has not be requested by the customer, and which is not being tested, and so it actually not be used without significant additional work.
Instead of wasting time designing for capabilities that might appear good software is designed for the requirement that exist today. 100% of the time should be spend satisfying requirements which the customer has actually requested. A good programming job should be done to meet this exact set of requirement. It might be more flexible to design a steering wheel that can be mounted in any position, but there will be tradeoffs. I would rather have a perfect steering wheel in one position, than a wheel which is not perfect, but could be used in more than one position.
So the fallacy of flexibility is that programmers will actually invent new requirements that do not exist for a current product and this is justified in the name of “flexibility” for the case that there is a new requirement in the future that we can not anticipate today. Software is better served by designing it for exactly the requirements we have today. Software is easy to change, and so if there is a new requirement tomorrow, we can easily change it at that time.