- On May 14, 2015
Software developers throw around this word, “overengineering,” quite a bit. “That code was overengineered.” “This is an overengineered solution.” Strangely enough, though, it’s hard to find an actual definition for the word online! People are always giving examples of overengineered code, but rarely do they say what the word actually means.
The dictionary just defines it as a combination of “over” (meaning “too much”) and “engineer” (meaning “design and build”). So per the dictionary, it would mean that you designed or built too much.
Wait, designed or built too much? What’s “too much”? And isn’t design a good thing?
Well, yeah, most projects could use more design. They suffer from underengineering. But once in a while, somebody really gets into it and just designs too much. Basically, this is like when somebody builds an orbital laser to destroy an anthill. An orbital laser is really cool, but it (a) costs too much (b) takes too much time and (c) is a maintenance nightmare. I mean, somebody’s going to have to go up there and fix it when it breaks.
The tricky part is–how do you know when you’re overengineering? What’s the line between good design and too much design?
Well, my criteria is this: When your design actually makes things more complex instead of simplifying things, you’re overengineering. An orbital laser would hugely complicate the life of somebody who just needed to destroy some anthills, whereas some simple ant poison would greatly simplify their life by removing their ant problem (if it worked).
This isn’t to say that all complexity is caused by overengineering. In fact, most complexity is caused by underengineering. If you have to choose, the safer side to be on is overengineering. But that’s kind of like saying it’s safer to be facing away from an atom bomb blast than toward it. It’s true (because it protects your eyes more), but really, either way, it’s going to suck pretty bad.
The best way to avoid overengineering is just don’t design too far into the future. Overengineering tends to happen like this: “Okay, I need some code to reverse a string. Well, might as well make a whole sytem for rearranging and modifying the letters in a string, since we might need that some day.” Essentially, somebody imagined a requirement that they had no idea whether or not was actually needed. They designed too far into the future, without actually knowing the future.
Now, if that developer really did know he’d need such a system in the future, it would be a mistake to design the code in such a way that the system couldn’t be added later. It doesn’t need to be there now, but you’d be underengineering if you made it impossible to add it later.
With overengineering, the big problem is that it makes it difficult for people to understand your code. There’s some piece built into the system that doesn’t really need to be there, and the person reading the code can’t figure out why it’s there, or even how the whole system works (since it’s now so complicated). It also has all the other flaws that designing too far into the future has, such as locking you into a particular design before you can actually be certain it’s the right one.
There are lots of common ways to overengineer. Probably the most common ways are: making something extensible that won’t ever need to be extended, and making something way more generic than it needs to be.
A good example of the first (making something extensible that doesn’t need to be) would be making a web server that could support an unlimited number of other protocols in addition to HTTP. That’s kind of silly, because if you’re a web server, then you’re sending HTTP. You’re not an “every possible protocol” server.
However, in that same situation, underengineering would be not allowing for any future extension of the HTTP standard. That’s something that does need to be extensible, because it really might change.
The issue here is “How likely it is that this thing’s going to change?” If you can be 99.999% certain that some part of your system is never going to change (for example, the letters available in the English language probably won’t be changing much–that’s a fairly good certainty) you don’t need to make that part of the system very extensible. (Even so, it’s still good to leave a tiny little room to expand, in the very rare chance that somebody adds something like, say, the Euro symbol to the language.)
There are just some things you have to assume won’t change (like, “We will never be serving any other protocol than HTTP”)–otherwise your system just gets too complex, trying to take into account every possible unknown future change, when there probably won’t even be any future differences. This is the exception, rather than the rule (you should assume most things will change), but you have to have a few stable, unchanging things to build your system around.
The second way (making something too generic) goes like this: Imagine that the Bugzilla Project suddenly went insane, and instead of saying that Bugzilla was a “bug tracking system”, we decided to make it into a “generic system for managing data in a database through forms.” It would become terribly complex, and it would also stop being a very good bug tracker, because it would be trying to be “everything to everyone” instead of just focusing on adding good bug-tracking features. That would definitely be overengineering–we’re just trying to track bugs, but suddenly we’re a generic form system? Yep, sounds like “orbital lasers” to me.
In addition to being too generic on the whole-program level, individual components of the program can also be too generic. A function that processes strings doesn’t also have to process integers and arrays, if you’re never going to be getting arrays and integers as input.
You don’t have to overengineer in a huge way, either, to mess up your system. Little by little, tiny bits of overengeering can stack up into one huge complex mass.
Good design is design that leads to simplicity in implementation and maintenance, and makes it easy to understand the code. Overengineered design is design that leads to difficulty in implementation, makes maintenance a nightmare, and turns otherwise simple code into a twisty maze of complexity. It’s not nearly as common as underengineering, but it’s still important to watch out for.