The fine line between overdesign and underdesign

When working on an evolving code base, it often happens that the code evolves while the design stays behind. This results in stopgap changes, which makes later changes more and more difficult.

I also came across situations where developers really want to avoid this, but gave the design so much thought that lots of unneeded ceremony is added.

By taking a number of principles into account, it is possible to maintain a design that is appropriate, where it stays easy to add features also in the long run.

Read more: The fine line between overdesign and underdesign

Underdesign

Underdesign, or lack of design, happens when a simple program evolves and no consideration is given that the initial design is not so fit for purpose anymore. Generally, this can be seen in the number of anti-patterns that occur throughout the code, added to make new features work without thinking about introducing new abstractions to fit the domain. When abstractions do not fit the problem domain, all kinds of nasty things happen to the design. Like:

  • Primitives everywhere. As classes are missing, primitives need to be passed around. So classes can have lots of parameters, many of which are not related. Likewise, functions get more and more parameters.
  • Since methods need to process so many parameters, they become larger more and more complex.
  • The more this design evolves, the more vague classes become. They start to attract functionality that does not belong there, but was added because the data was already in that class.
  • Worst of all is the amount of duplication. The same logic occurs over and over again in the code base, only with slightly different implementations. They all need to be kept in sync all the time.

If an abstraction is missing, then its associated responsibility has no single place in the code. Instead, it must be worked out from each location where it is needed. So all potential call sites need to implement similar logic over and over again. Therefore I think that duplication is a good sign of underdesign.

Overdesign

The other side is overdesign. I’ve seen this happen when people have been working in a legacy code base with poor design. They’re determined to do better than that, so they think their design through very well. They also have the experience that code with a bad design gets worse all the time and they certainly don’t want to experience that again, so they give the software design as much thought as they can.

Sadly, this might result in overdesign. Such a design has features like:

  • Generalizations that add no value. Just in case it might be needed sometime, then it is available. But in the current code base, it is a drag because it makes it more difficult to get things done.
  • Design patterns are applied in the wrong circumstances. If there is currently only one instance of an object? Always apply the singleton pattern! And only factories can ever call a constructor. Or even better, create a builder for that. After all, a builder satisfies all requirements, so you never have to think about it, and you’re most future-safe.

So, in contrast to underdesign, overdesign exhibits too much generalization; it contains more abstractions than necessary. In my experience, this is usually in the form of misplaced application of design patterns.

Take the builder design pattern for example. If it is applied in the wrong place, then you’ll see that the builder class has no customization methods, as they are simply redundant. Replacing the builder by a factory method, or even straight constructor call, will do no harm.

If I had to choose between underdesign and overdesign, I’d immediately choose the latter. It is much easier to get to a decent design from there. But still, overdesign is not harmless, because the code is more complex than necessary, so it is also more difficult to change. Also, a lot of time is spent on introducing unnecessary abstractions, so it is more expensive to develop.

Good design

But what does a good design look like? It should capture neither of the distinguishing characteristics of underdesign and overdesign.

In my opinion, the main characteristic of a good design is simplicity. Nothing needs to be added, but also nothing can be removed either. This does not mean that the number of classes is minimal, but that the classes and interfaces themselves are simple. They have few instance variables and methods, and each method accepts only a few parameters.

Simplicity also makes it much easier to reason about the code, because it easily fits in your mind.

Would that mean that the design is not future-proof? I don’t think so. Simplicity makes it easy to refactor the code base when necessary. For example, if interfaces are small, then it is easy to provide new implentations. If classes have a strictly defined responsibility, then it is easy to reuse it in new situations. This means that simplicity makes it possible to develop the code base with little effort.

Leave a Reply

Your email address will not be published. Required fields are marked *