Undesigning sucks. Disassembling an architecture that I built months ago following the requirements provided at the time is never a nice experience. There’s a bit of a sense of failure, mixed with a feeling of time waste. But requirements and user needs change over time, and software should follow along.
To overcome situations like these I try to avoid getting in bed with a too specific architecture. I prefer to work bottom up. I write specific code first and then generalize along the way, without going overboard.
Designs are always playing catch up to changing real world requirements. So even if we found a perfect abstraction by miracle, it comes tagged with an expiry date because #1 — The House wins in the end. The best quality of a Design today is how well it can be undesigned.RDX source
I tend to apply the following tactics to avoid over-engineering:
- favor composition instead of inheritance
- write code that, when crashing, it crashes as early as possible
- create helper functions/objects to group common functionalities but make them very simple
- write an example of an API before creating it (trick by Krzysztof Zabłocki)
- avoid smart code
- refactor in micro steps
- celebrate when I delete code
Reusable software is a great concept until you sacrifice too much flexibility. And when requirements change often, or when you work on a product that needs quick iterations, flexibility is key.