The fine line between underdesign and overdesign
If you want to provide results to the business, then it is only sensible to avoid unnecessary work. Anything that does not contribute to the result must be scrapped. When you’re in the software business, this means that we need to work on features that provide value to the end user. It also means that you want to deliver each individual feature as quickly as possible. But even more, you want to do this in the future as well. If you want to keep delivering features quickly in the future, then it is important that you don’t block future possibilities today.
In the past, I used to think that it is possible to make a good guess about what’s coming. I learned that this is not realistic, no matter what you try. Practice is almost always different from what you expect. If you anticipate future changes, then it normally turns out that those features are not required, or different features (which may interfere with your preparations) are required first.
Don’t overdesign
If you try to be prepared for the unknown future, then you’re not spending your time in the most efficient way:
- You’re putting effort in setting up a design that is not required just yet. You could have finished the current feature faster with a simpler design;
- The new flexibility also comes with a cost, since it is not the most simple design possible. Therefore, it makes changes harder to make all the time, even while the new functionality is not used yet.
So, in short, remove elements from the design if they are not required. For example, there is no point in designing a builder or factory if it is simpler for the callers to call the constructor straight away. The core insight here is: design patterns solve problems, but always at some cost. You don’t want to pay the price if you don’t have the problem in the first place.
Don’t neglect design either
What then? Should we then refrain from putting any thoughts into good design? Is it all just a waste of time? I’m convinced that this is not true either. You see, the main objective of design (and code quality in general) is to make change possible. The first requirement of making change possible, is that you are able to tell what to change and where, and what the consequences are. That is precisely what good design enables.
The fine line
If the code is a mess, then your deliveries will become unreliable, but if you do too much design, then you’re also wasting time.
I think the balance is at the point of just enough design and continuous refactoring to keep the code in extensible shape.
For example, if you have a huge class with many members, then this is to me a sign that several refactoring opportunities were missed. Same case when the code contains a lot of duplication. When you need to work in such a part of the code, the first think I would do is to make it habitable.
When the code is in good enough shape, then it is not difficult to add new functionality in predictable manner. At this point, it is also possible to introduce design patterns as needed. Remember that direct constructor call? If the new functionality requires multiple implementations of the class, then it is not a hard step to introduce a factory here, or extract an interface and define a new implementation. It’s just part of the job of delivering the functionality.
What’s more, these refactoring steps are straightforward because they start with a simple situation. Applying a design pattern normally introduces some additional indirection. But introducing indirection is usually predictable in terms of effort, so the associated risk is low. And you’re prepared for the unknown, without wasting any effort.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. – Antoine de Saint-Exupéry, Airman’s Odyssey